"""Dependency parsing utilities for PyPI packages.""" import logging import re from typing import Any from packaging.requirements import Requirement from packaging.version import Version logger = logging.getLogger(__name__) class DependencyParser: """Parser for Python package dependencies.""" def __init__(self): self.parsed_cache: dict[str, list[Requirement]] = {} def parse_requirements(self, requires_dist: list[str]) -> list[Requirement]: """Parse requirements from requires_dist list. Args: requires_dist: List of requirement strings from PyPI metadata Returns: List of parsed Requirement objects """ requirements = [] for req_str in requires_dist or []: if not req_str or not req_str.strip(): continue try: req = Requirement(req_str) requirements.append(req) except Exception as e: logger.warning(f"Failed to parse requirement '{req_str}': {e}") continue return requirements def filter_requirements_by_python_version( self, requirements: list[Requirement], python_version: str ) -> list[Requirement]: """Filter requirements based on Python version. Args: requirements: List of Requirement objects python_version: Target Python version (e.g., "3.10") Returns: Filtered list of requirements applicable to the Python version """ filtered = [] try: target_version = Version(python_version) except Exception as e: logger.warning(f"Invalid Python version '{python_version}': {e}") return requirements for req in requirements: if self._is_requirement_applicable(req, target_version): filtered.append(req) return filtered def _is_requirement_applicable(self, req: Requirement, python_version: Version) -> bool: """Check if a requirement is applicable for the given Python version. Args: req: Requirement object python_version: Target Python version Returns: True if requirement applies to the Python version """ if not req.marker: return True # Create environment for marker evaluation env = { 'python_version': str(python_version), 'python_full_version': str(python_version), 'platform_system': 'Linux', # Default assumption 'platform_machine': 'x86_64', # Default assumption 'implementation_name': 'cpython', 'implementation_version': str(python_version), } try: return req.marker.evaluate(env) except Exception as e: logger.warning(f"Failed to evaluate marker for {req}: {e}") return True # Include by default if evaluation fails def categorize_dependencies( self, requirements: list[Requirement] ) -> dict[str, list[Requirement]]: """Categorize dependencies into runtime, development, and optional groups. Args: requirements: List of Requirement objects Returns: Dictionary with categorized dependencies """ categories = { 'runtime': [], 'development': [], 'optional': {}, 'extras': {} } for req in requirements: if not req.marker: # No marker means it's a runtime dependency categories['runtime'].append(req) continue marker_str = str(req.marker) # Check for extra dependencies if 'extra ==' in marker_str: extra_match = re.search(r'extra\s*==\s*["\']([^"\']+)["\']', marker_str) if extra_match: extra_name = extra_match.group(1) if extra_name not in categories['extras']: categories['extras'][extra_name] = [] categories['extras'][extra_name].append(req) continue # Check for development dependencies if any(keyword in marker_str.lower() for keyword in ['dev', 'test', 'lint', 'doc']): categories['development'].append(req) else: categories['runtime'].append(req) return categories def extract_package_names(self, requirements: list[Requirement]) -> set[str]: """Extract package names from requirements. Args: requirements: List of Requirement objects Returns: Set of package names """ return {req.name.lower() for req in requirements} def get_version_constraints(self, req: Requirement) -> dict[str, Any]: """Get version constraints from a requirement. Args: req: Requirement object Returns: Dictionary with version constraint information """ if not req.specifier: return {'constraints': [], 'allows_any': True} constraints = [] for spec in req.specifier: constraints.append({ 'operator': spec.operator, 'version': str(spec.version) }) return { 'constraints': constraints, 'allows_any': len(constraints) == 0, 'specifier_str': str(req.specifier) }