From e1b01abf9b3b7a90864d7d6417b7dc197a322b7c Mon Sep 17 00:00:00 2001 From: Mondo Diaz Date: Fri, 30 Jan 2026 15:34:19 -0600 Subject: [PATCH] Add PEP 440 version constraint matching for dependency resolution - Parse version constraints like >=1.9, <2.0 using packaging library - Find the latest version that satisfies the constraint - Support wildcard (*) to get latest version - Fall back to exact version and tag matching --- backend/app/dependencies.py | 124 ++++++++++++++++++++++++++++++++---- 1 file changed, 113 insertions(+), 11 deletions(-) diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 196a927..24569fe 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -10,11 +10,20 @@ Handles: - Conflict detection """ +import re import yaml from typing import List, Dict, Any, Optional, Set, Tuple from sqlalchemy.orm import Session from sqlalchemy import and_ +# Import packaging for PEP 440 version matching +try: + from packaging.specifiers import SpecifierSet, InvalidSpecifier + from packaging.version import Version, InvalidVersion + HAS_PACKAGING = True +except ImportError: + HAS_PACKAGING = False + from .models import ( Project, Package, @@ -304,6 +313,87 @@ def get_reverse_dependencies( ) +def _is_version_constraint(version_str: str) -> bool: + """Check if a version string contains constraint operators.""" + if not version_str: + return False + # Check for common constraint operators + return any(op in version_str for op in ['>=', '<=', '!=', '~=', '>', '<', '==', '*']) + + +def _resolve_version_constraint( + db: Session, + package: Package, + constraint: str, +) -> Optional[Tuple[str, str, int]]: + """ + Resolve a version constraint (e.g., '>=1.9') to a specific version. + + Uses PEP 440 version matching to find the best matching version. + + Args: + db: Database session + package: Package to search versions in + constraint: Version constraint string (e.g., '>=1.9', '<2.0,>=1.5') + + Returns: + Tuple of (artifact_id, resolved_version, size) or None if not found + """ + if not HAS_PACKAGING: + # Fallback: if packaging not available, can't do constraint matching + return None + + # Handle wildcard - return latest version + if constraint == '*': + # Get the latest version by created_at + latest = db.query(PackageVersion).filter( + PackageVersion.package_id == package.id, + ).order_by(PackageVersion.created_at.desc()).first() + if latest: + artifact = db.query(Artifact).filter(Artifact.id == latest.artifact_id).first() + if artifact: + return (artifact.id, latest.version, artifact.size) + return None + + try: + specifier = SpecifierSet(constraint) + except InvalidSpecifier: + # Invalid constraint, try as exact version + return None + + # Get all versions for this package + all_versions = db.query(PackageVersion).filter( + PackageVersion.package_id == package.id, + ).all() + + if not all_versions: + return None + + # Find matching versions + matching = [] + for pv in all_versions: + try: + v = Version(pv.version) + if v in specifier: + matching.append((pv, v)) + except InvalidVersion: + # Skip invalid versions + continue + + if not matching: + return None + + # Sort by version (descending) and return the latest matching + matching.sort(key=lambda x: x[1], reverse=True) + best_match = matching[0][0] + + artifact = db.query(Artifact).filter(Artifact.id == best_match.artifact_id).first() + if artifact: + return (artifact.id, best_match.version, artifact.size) + + return None + + def _resolve_dependency_to_artifact( db: Session, project_name: str, @@ -314,11 +404,17 @@ def _resolve_dependency_to_artifact( """ Resolve a dependency constraint to an artifact ID. + Supports: + - Exact version matching (e.g., '1.2.3') + - Version constraints (e.g., '>=1.9', '<2.0,>=1.5') + - Tag matching + - Wildcard ('*' for any version) + Args: db: Database session project_name: Project name package_name: Package name - version: Version constraint (exact) + version: Version or version constraint tag: Tag constraint Returns: @@ -337,17 +433,23 @@ def _resolve_dependency_to_artifact( return None if version: - # Look up by version - pkg_version = db.query(PackageVersion).filter( - PackageVersion.package_id == package.id, - PackageVersion.version == version, - ).first() - if pkg_version: - artifact = db.query(Artifact).filter( - Artifact.id == pkg_version.artifact_id + # Check if this is a version constraint (>=, <, etc.) or exact version + if _is_version_constraint(version): + result = _resolve_version_constraint(db, package, version) + if result: + return result + else: + # Look up by exact version + pkg_version = db.query(PackageVersion).filter( + PackageVersion.package_id == package.id, + PackageVersion.version == version, ).first() - if artifact: - return (artifact.id, version, artifact.size) + if pkg_version: + artifact = db.query(Artifact).filter( + Artifact.id == pkg_version.artifact_id + ).first() + if artifact: + return (artifact.id, version, artifact.size) # Also check if there's a tag with this exact name tag_record = db.query(Tag).filter(