pypi-query-mcp/pypi_query_mcp/core/dependency_parser.py
longhao 6b14ff6da5 feat: add advanced dependency resolution and package download tools
- Add DependencyParser for parsing and categorizing package dependencies
- Add DependencyResolver for recursive dependency tree analysis
- Add PackageDownloader for downloading packages with dependencies
- Add resolve_dependencies MCP tool for comprehensive dependency analysis
- Add download_package MCP tool for package collection
- Support Python version filtering and extra dependencies
- Include comprehensive test coverage for new functionality
- Add demonstration script for new features
- Update README with new capabilities and usage examples

Signed-off-by: Hal <hal.long@outlook.com>
2025-05-27 19:06:18 +08:00

180 lines
5.9 KiB
Python

"""Dependency parsing utilities for PyPI packages."""
import re
from typing import Any, Dict, List, Optional, Set, Tuple
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import Version
import logging
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)
}