fix: resolve include_extras parameter validation in resolve_dependencies
- Fix extra dependencies being filtered out by Python version checks - Add proper handling for extra markers in dependency parsing - Update parameter descriptions and documentation - Add comprehensive examples and demo script - Test with requests[socks], django[argon2,bcrypt], setuptools[test]
This commit is contained in:
parent
146952f404
commit
114a7d8d5a
151
EXTRAS_FIX_SUMMARY.md
Normal file
151
EXTRAS_FIX_SUMMARY.md
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
# Fix for include_extras Parameter Validation Issue
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Fixed a critical issue where the `include_extras` parameter in the `resolve_dependencies` tool was not working correctly due to incorrect Python version filtering that was removing extra dependencies from consideration.
|
||||||
|
|
||||||
|
## Root Cause Analysis
|
||||||
|
|
||||||
|
The issue was in the `_is_requirement_applicable` method in `pypi_query_mcp/core/dependency_parser.py`. When filtering requirements by Python version, the method was evaluating markers like `extra == "socks"` in an environment that didn't include the `extra` variable, causing these requirements to be filtered out incorrectly.
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
1. **Python version filtering was too aggressive**: The `filter_requirements_by_python_version` method was filtering out ALL requirements with markers that couldn't be evaluated, including extra requirements.
|
||||||
|
|
||||||
|
2. **Marker evaluation environment was incomplete**: When evaluating markers like `extra == "socks"`, the environment didn't include the `extra` variable, so the evaluation always returned `False`.
|
||||||
|
|
||||||
|
3. **Incorrect filtering logic**: Extra dependencies should not be filtered by Python version at all - they should be handled separately based on user selection.
|
||||||
|
|
||||||
|
### Before the Fix
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In _is_requirement_applicable method
|
||||||
|
def _is_requirement_applicable(self, req: Requirement, python_version: Version) -> bool:
|
||||||
|
if not req.marker:
|
||||||
|
return True
|
||||||
|
|
||||||
|
env = {
|
||||||
|
"python_version": str(python_version),
|
||||||
|
# ... other environment variables
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return req.marker.evaluate(env) # This returns False for extra == "socks"
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to evaluate marker for {req}: {e}")
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result**: Requirements like `PySocks!=1.5.7,>=1.5.6; extra == "socks"` were filtered out because `extra == "socks"` evaluated to `False` in an environment without the `extra` variable.
|
||||||
|
|
||||||
|
## The Fix
|
||||||
|
|
||||||
|
Added a check to exclude extra requirements from Python version filtering:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def _is_requirement_applicable(self, req: Requirement, python_version: Version) -> bool:
|
||||||
|
if not req.marker:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If the marker contains 'extra ==', this is an extra dependency
|
||||||
|
# and should not be filtered by Python version. Extra dependencies
|
||||||
|
# are handled separately based on user selection.
|
||||||
|
marker_str = str(req.marker)
|
||||||
|
if "extra ==" in marker_str:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Rest of the method unchanged...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why This Fix Works
|
||||||
|
|
||||||
|
1. **Preserves extra requirements**: Extra dependencies are no longer filtered out by Python version filtering
|
||||||
|
2. **Maintains correct Python version filtering**: Non-extra requirements are still properly filtered by Python version
|
||||||
|
3. **Separates concerns**: Extra handling is kept separate from Python version filtering, as it should be
|
||||||
|
4. **Backwards compatible**: Doesn't break existing functionality
|
||||||
|
|
||||||
|
## Testing Results
|
||||||
|
|
||||||
|
### Before the Fix
|
||||||
|
```python
|
||||||
|
# Example: requests[socks] with Python 3.10
|
||||||
|
result = await resolve_package_dependencies("requests", include_extras=["socks"], python_version="3.10")
|
||||||
|
print(result["summary"]["total_extra_dependencies"]) # Output: 0 ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
### After the Fix
|
||||||
|
```python
|
||||||
|
# Example: requests[socks] with Python 3.10
|
||||||
|
result = await resolve_package_dependencies("requests", include_extras=["socks"], python_version="3.10")
|
||||||
|
print(result["summary"]["total_extra_dependencies"]) # Output: 1 ✅
|
||||||
|
print(result["dependency_tree"]["requests"]["dependencies"]["extras"])
|
||||||
|
# Output: {'socks': ['PySocks!=1.5.7,>=1.5.6; extra == "socks"']} ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples of Correct Usage
|
||||||
|
|
||||||
|
### Basic Examples
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Requests with SOCKS proxy support
|
||||||
|
await resolve_package_dependencies("requests", include_extras=["socks"])
|
||||||
|
|
||||||
|
# Django with password hashing extras
|
||||||
|
await resolve_package_dependencies("django", include_extras=["argon2", "bcrypt"])
|
||||||
|
|
||||||
|
# Setuptools with testing tools
|
||||||
|
await resolve_package_dependencies("setuptools", include_extras=["test"])
|
||||||
|
|
||||||
|
# Flask with async and dotenv support
|
||||||
|
await resolve_package_dependencies("flask", include_extras=["async", "dotenv"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### How to Find Available Extras
|
||||||
|
|
||||||
|
1. **Check the package's PyPI page** - Look for available extras in the description
|
||||||
|
2. **Use the `provides_extra` field** - Available in package metadata
|
||||||
|
3. **Check package documentation** - Often lists available extras
|
||||||
|
4. **Look for requirements with `extra ==`** - In the `requires_dist` field
|
||||||
|
|
||||||
|
### Common Mistakes to Avoid
|
||||||
|
|
||||||
|
❌ **Wrong**: Using generic extra names
|
||||||
|
```python
|
||||||
|
# These don't exist for requests
|
||||||
|
await resolve_package_dependencies("requests", include_extras=["dev", "test"])
|
||||||
|
```
|
||||||
|
|
||||||
|
✅ **Right**: Using package-specific extra names
|
||||||
|
```python
|
||||||
|
# These are actual extras for requests
|
||||||
|
await resolve_package_dependencies("requests", include_extras=["socks", "use-chardet-on-py3"])
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. **`pypi_query_mcp/core/dependency_parser.py`**: Fixed `_is_requirement_applicable` method
|
||||||
|
2. **`pypi_query_mcp/server.py`**: Updated documentation for `include_extras` parameter
|
||||||
|
3. **`pypi_query_mcp/tools/dependency_resolver.py`**: Updated docstring for `include_extras` parameter
|
||||||
|
4. **`tests/test_dependency_resolver.py`**: Added comprehensive test cases
|
||||||
|
5. **`examples/extras_usage_demo.py`**: Added demonstration of correct usage
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
The fix has been validated with:
|
||||||
|
|
||||||
|
- ✅ Real PyPI packages (requests, django, setuptools, flask)
|
||||||
|
- ✅ Various extra combinations
|
||||||
|
- ✅ Python version filtering still works for non-extra requirements
|
||||||
|
- ✅ Transitive dependency resolution with extras
|
||||||
|
- ✅ Edge cases (non-existent extras, extras with no dependencies)
|
||||||
|
- ✅ Backwards compatibility with existing code
|
||||||
|
|
||||||
|
## Performance Impact
|
||||||
|
|
||||||
|
- **Minimal**: Only adds a simple string check for `"extra =="` in markers
|
||||||
|
- **Positive**: Reduces unnecessary marker evaluations for extra requirements
|
||||||
|
- **No regression**: All existing functionality continues to work as before
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This fix resolves the core issue with `include_extras` parameter validation while maintaining all existing functionality. Users can now successfully resolve optional dependencies using the correct extra names as defined by each package.
|
205
examples/extras_usage_demo.py
Normal file
205
examples/extras_usage_demo.py
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Demonstration of PyPI Query MCP Server extras functionality.
|
||||||
|
|
||||||
|
This script shows how to properly use the include_extras parameter
|
||||||
|
to resolve optional dependencies for Python packages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from pypi_query_mcp.tools.dependency_resolver import resolve_package_dependencies
|
||||||
|
from pypi_query_mcp.core.pypi_client import PyPIClient
|
||||||
|
|
||||||
|
|
||||||
|
async def show_available_extras(package_name: str):
|
||||||
|
"""Show what extras are available for a package."""
|
||||||
|
print(f"\n📦 Available extras for {package_name}:")
|
||||||
|
|
||||||
|
async with PyPIClient() as client:
|
||||||
|
package_data = await client.get_package_info(package_name)
|
||||||
|
|
||||||
|
info = package_data.get("info", {})
|
||||||
|
provides_extra = info.get("provides_extra", [])
|
||||||
|
requires_dist = info.get("requires_dist", []) or []
|
||||||
|
|
||||||
|
if provides_extra:
|
||||||
|
print(f" Provides extras: {', '.join(provides_extra)}")
|
||||||
|
else:
|
||||||
|
print(" No provides_extra field found")
|
||||||
|
|
||||||
|
# Find extras from requires_dist
|
||||||
|
extras_in_deps = set()
|
||||||
|
for req in requires_dist:
|
||||||
|
if "extra ==" in req:
|
||||||
|
# Extract extra name from requirement like: pytest>=6.0.0; extra=='test'
|
||||||
|
import re
|
||||||
|
match = re.search(r'extra\s*==\s*["\']([^"\']+)["\']', req)
|
||||||
|
if match:
|
||||||
|
extras_in_deps.add(match.group(1))
|
||||||
|
|
||||||
|
if extras_in_deps:
|
||||||
|
print(f" Extras with dependencies: {', '.join(sorted(extras_in_deps))}")
|
||||||
|
else:
|
||||||
|
print(" No extras with dependencies found")
|
||||||
|
|
||||||
|
|
||||||
|
async def demo_extras_resolution():
|
||||||
|
"""Demonstrate extras resolution with various packages."""
|
||||||
|
|
||||||
|
# Examples of packages with well-known extras
|
||||||
|
examples = [
|
||||||
|
{
|
||||||
|
"package": "requests",
|
||||||
|
"extras": ["socks"],
|
||||||
|
"description": "HTTP library with SOCKS proxy support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "django",
|
||||||
|
"extras": ["argon2", "bcrypt"],
|
||||||
|
"description": "Web framework with password hashing extras"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "setuptools",
|
||||||
|
"extras": ["test"],
|
||||||
|
"description": "Package development tools with testing extras"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "flask",
|
||||||
|
"extras": ["async", "dotenv"],
|
||||||
|
"description": "Web framework with async and dotenv support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for example in examples:
|
||||||
|
package_name = example["package"]
|
||||||
|
extras = example["extras"]
|
||||||
|
description = example["description"]
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print(f"🔍 Example: {package_name}")
|
||||||
|
print(f"📋 Description: {description}")
|
||||||
|
print(f"🎯 Testing extras: {extras}")
|
||||||
|
|
||||||
|
# Show available extras
|
||||||
|
await show_available_extras(package_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Resolve without extras
|
||||||
|
print(f"\n📊 Resolving {package_name} WITHOUT extras...")
|
||||||
|
result_no_extras = await resolve_package_dependencies(
|
||||||
|
package_name=package_name,
|
||||||
|
python_version="3.10",
|
||||||
|
include_extras=[],
|
||||||
|
max_depth=1 # Limit depth for demo
|
||||||
|
)
|
||||||
|
|
||||||
|
# Resolve with extras
|
||||||
|
print(f"📊 Resolving {package_name} WITH extras {extras}...")
|
||||||
|
result_with_extras = await resolve_package_dependencies(
|
||||||
|
package_name=package_name,
|
||||||
|
python_version="3.10",
|
||||||
|
include_extras=extras,
|
||||||
|
max_depth=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compare results
|
||||||
|
print(f"\n📈 Results comparison:")
|
||||||
|
print(f" Without extras: {result_no_extras['summary']['total_extra_dependencies']} extra deps")
|
||||||
|
print(f" With extras: {result_with_extras['summary']['total_extra_dependencies']} extra deps")
|
||||||
|
|
||||||
|
# Show actual extras resolved
|
||||||
|
main_pkg = next(iter(result_with_extras['dependency_tree'].values()), {})
|
||||||
|
extras_resolved = main_pkg.get('dependencies', {}).get('extras', {})
|
||||||
|
|
||||||
|
if extras_resolved:
|
||||||
|
print(f" ✅ Extras resolved successfully:")
|
||||||
|
for extra_name, deps in extras_resolved.items():
|
||||||
|
print(f" - {extra_name}: {len(deps)} dependencies")
|
||||||
|
for dep in deps[:2]: # Show first 2
|
||||||
|
print(f" * {dep}")
|
||||||
|
if len(deps) > 2:
|
||||||
|
print(f" * ... and {len(deps) - 2} more")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ No extras resolved (may not exist or have no dependencies)")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def demo_incorrect_usage():
|
||||||
|
"""Demonstrate common mistakes with extras usage."""
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print("❌ Common Mistakes with Extras")
|
||||||
|
print("='*60")
|
||||||
|
|
||||||
|
mistakes = [
|
||||||
|
{
|
||||||
|
"package": "requests",
|
||||||
|
"extras": ["dev", "test"], # These don't exist for requests
|
||||||
|
"error": "Using generic extra names instead of package-specific ones"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "setuptools",
|
||||||
|
"extras": ["testing"], # Should be "test" not "testing"
|
||||||
|
"error": "Using similar but incorrect extra names"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for mistake in mistakes:
|
||||||
|
package_name = mistake["package"]
|
||||||
|
extras = mistake["extras"]
|
||||||
|
error_desc = mistake["error"]
|
||||||
|
|
||||||
|
print(f"\n🚫 Mistake: {error_desc}")
|
||||||
|
print(f" Package: {package_name}")
|
||||||
|
print(f" Incorrect extras: {extras}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await resolve_package_dependencies(
|
||||||
|
package_name=package_name,
|
||||||
|
python_version="3.10",
|
||||||
|
include_extras=extras,
|
||||||
|
max_depth=1
|
||||||
|
)
|
||||||
|
|
||||||
|
total_extras = result['summary']['total_extra_dependencies']
|
||||||
|
print(f" Result: {total_extras} extra dependencies resolved")
|
||||||
|
if total_extras == 0:
|
||||||
|
print(f" ⚠️ No extras resolved - these extras likely don't exist")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f" ❌ Error: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main demonstration function."""
|
||||||
|
print("🚀 PyPI Query MCP Server - Extras Usage Demo")
|
||||||
|
print("=" * 60)
|
||||||
|
print()
|
||||||
|
print("This demo shows how to properly use the include_extras parameter")
|
||||||
|
print("to resolve optional dependencies for Python packages.")
|
||||||
|
|
||||||
|
await demo_extras_resolution()
|
||||||
|
await demo_incorrect_usage()
|
||||||
|
|
||||||
|
print(f"\n{'='*60}")
|
||||||
|
print("✨ Demo completed!")
|
||||||
|
print()
|
||||||
|
print("💡 Key takeaways:")
|
||||||
|
print(" 1. Always check what extras are available for a package first")
|
||||||
|
print(" 2. Use the exact extra names defined by the package")
|
||||||
|
print(" 3. Check package documentation or PyPI page for available extras")
|
||||||
|
print(" 4. Not all packages have extras, and some extras may have no dependencies")
|
||||||
|
print()
|
||||||
|
print("📚 To find available extras:")
|
||||||
|
print(" - Check the package's PyPI page")
|
||||||
|
print(" - Look for 'provides_extra' in package metadata")
|
||||||
|
print(" - Check package documentation")
|
||||||
|
print(" - Look for requirements with 'extra ==' in requires_dist")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
@ -81,6 +81,13 @@ class DependencyParser:
|
|||||||
if not req.marker:
|
if not req.marker:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# If the marker contains 'extra ==', this is an extra dependency
|
||||||
|
# and should not be filtered by Python version. Extra dependencies
|
||||||
|
# are handled separately based on user selection.
|
||||||
|
marker_str = str(req.marker)
|
||||||
|
if "extra ==" in marker_str:
|
||||||
|
return True
|
||||||
|
|
||||||
# Create environment for marker evaluation
|
# Create environment for marker evaluation
|
||||||
env = {
|
env = {
|
||||||
"python_version": str(python_version),
|
"python_version": str(python_version),
|
||||||
|
@ -305,7 +305,10 @@ async def resolve_dependencies(
|
|||||||
Args:
|
Args:
|
||||||
package_name: The name of the PyPI package to analyze (e.g., 'pyside2', 'django')
|
package_name: The name of the PyPI package to analyze (e.g., 'pyside2', 'django')
|
||||||
python_version: Target Python version for dependency filtering (e.g., '3.10', '3.11')
|
python_version: Target Python version for dependency filtering (e.g., '3.10', '3.11')
|
||||||
include_extras: List of extra dependency groups to include (e.g., ['dev', 'test'])
|
include_extras: List of extra dependency groups to include. These are optional
|
||||||
|
dependency groups defined by the package (e.g., ['socks'] for requests,
|
||||||
|
['argon2', 'bcrypt'] for django, ['test', 'doc'] for setuptools). Check the
|
||||||
|
package's PyPI page or use the provides_extra field to see available extras.
|
||||||
include_dev: Whether to include development dependencies (default: False)
|
include_dev: Whether to include development dependencies (default: False)
|
||||||
max_depth: Maximum recursion depth for dependency resolution (default: 5)
|
max_depth: Maximum recursion depth for dependency resolution (default: 5)
|
||||||
|
|
||||||
@ -373,7 +376,9 @@ async def download_package(
|
|||||||
package_name: The name of the PyPI package to download (e.g., 'pyside2', 'requests')
|
package_name: The name of the PyPI package to download (e.g., 'pyside2', 'requests')
|
||||||
download_dir: Local directory to download packages to (default: './downloads')
|
download_dir: Local directory to download packages to (default: './downloads')
|
||||||
python_version: Target Python version for compatibility (e.g., '3.10', '3.11')
|
python_version: Target Python version for compatibility (e.g., '3.10', '3.11')
|
||||||
include_extras: List of extra dependency groups to include (e.g., ['dev', 'test'])
|
include_extras: List of extra dependency groups to include. These are optional
|
||||||
|
dependency groups defined by the package (e.g., ['socks'] for requests,
|
||||||
|
['argon2', 'bcrypt'] for django). Check the package's PyPI page to see available extras.
|
||||||
include_dev: Whether to include development dependencies (default: False)
|
include_dev: Whether to include development dependencies (default: False)
|
||||||
prefer_wheel: Whether to prefer wheel files over source distributions (default: True)
|
prefer_wheel: Whether to prefer wheel files over source distributions (default: True)
|
||||||
verify_checksums: Whether to verify downloaded file checksums (default: True)
|
verify_checksums: Whether to verify downloaded file checksums (default: True)
|
||||||
|
@ -35,7 +35,8 @@ class DependencyResolver:
|
|||||||
Args:
|
Args:
|
||||||
package_name: Name of the package to resolve
|
package_name: Name of the package to resolve
|
||||||
python_version: Target Python version (e.g., "3.10")
|
python_version: Target Python version (e.g., "3.10")
|
||||||
include_extras: List of extra dependencies to include
|
include_extras: List of extra dependency groups to include (e.g., ['socks'] for requests,
|
||||||
|
['test', 'doc'] for setuptools). These are optional dependencies defined by the package.
|
||||||
include_dev: Whether to include development dependencies
|
include_dev: Whether to include development dependencies
|
||||||
max_depth: Maximum recursion depth (overrides instance default)
|
max_depth: Maximum recursion depth (overrides instance default)
|
||||||
|
|
||||||
@ -242,7 +243,8 @@ async def resolve_package_dependencies(
|
|||||||
Args:
|
Args:
|
||||||
package_name: Name of the package to resolve
|
package_name: Name of the package to resolve
|
||||||
python_version: Target Python version (e.g., "3.10")
|
python_version: Target Python version (e.g., "3.10")
|
||||||
include_extras: List of extra dependencies to include
|
include_extras: List of extra dependency groups to include (e.g., ['socks'] for requests,
|
||||||
|
['test', 'doc'] for setuptools). These are optional dependencies defined by the package.
|
||||||
include_dev: Whether to include development dependencies
|
include_dev: Whether to include development dependencies
|
||||||
max_depth: Maximum recursion depth
|
max_depth: Maximum recursion depth
|
||||||
|
|
||||||
|
@ -83,24 +83,145 @@ class TestDependencyResolver:
|
|||||||
"""Test dependency resolution with extra dependencies."""
|
"""Test dependency resolution with extra dependencies."""
|
||||||
mock_package_data = {
|
mock_package_data = {
|
||||||
"info": {
|
"info": {
|
||||||
"name": "test-package",
|
"name": "mock-test-package-12345",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"requires_python": ">=3.8",
|
"requires_python": ">=3.8",
|
||||||
"requires_dist": ["requests>=2.25.0", "pytest>=6.0.0; extra=='test'"],
|
"requires_dist": ["requests>=2.25.0", "pytest>=6.0.0; extra=='test'"],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Mock for transitive dependencies
|
||||||
|
mock_requests_data = {
|
||||||
|
"info": {
|
||||||
|
"name": "requests",
|
||||||
|
"version": "2.25.0",
|
||||||
|
"requires_python": ">=3.6",
|
||||||
|
"requires_dist": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_pytest_data = {
|
||||||
|
"info": {
|
||||||
|
"name": "pytest",
|
||||||
|
"version": "6.0.0",
|
||||||
|
"requires_python": ">=3.6",
|
||||||
|
"requires_dist": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
with patch("pypi_query_mcp.core.PyPIClient") as mock_client_class:
|
with patch("pypi_query_mcp.core.PyPIClient") as mock_client_class:
|
||||||
mock_client = AsyncMock()
|
mock_client = AsyncMock()
|
||||||
mock_client_class.return_value.__aenter__.return_value = mock_client
|
mock_client_class.return_value.__aenter__.return_value = mock_client
|
||||||
mock_client.get_package_info.return_value = mock_package_data
|
|
||||||
|
# Setup mock to return different data based on package name
|
||||||
|
def mock_get_package_info(package_name):
|
||||||
|
if package_name.lower() == "mock-test-package-12345":
|
||||||
|
return mock_package_data
|
||||||
|
elif package_name.lower() == "requests":
|
||||||
|
return mock_requests_data
|
||||||
|
elif package_name.lower() == "pytest":
|
||||||
|
return mock_pytest_data
|
||||||
|
else:
|
||||||
|
return {"info": {"name": package_name, "version": "1.0.0", "requires_dist": []}}
|
||||||
|
|
||||||
|
mock_client.get_package_info.side_effect = mock_get_package_info
|
||||||
|
|
||||||
result = await resolver.resolve_dependencies(
|
result = await resolver.resolve_dependencies(
|
||||||
"test-package", include_extras=["test"]
|
"mock-test-package-12345", include_extras=["test"], max_depth=2
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["include_extras"] == ["test"]
|
assert result["include_extras"] == ["test"]
|
||||||
assert "dependency_tree" in result
|
assert "dependency_tree" in result
|
||||||
|
|
||||||
|
# Verify that extras are properly resolved and included
|
||||||
|
assert result["summary"]["total_extra_dependencies"] == 1
|
||||||
|
main_pkg = result["dependency_tree"]["mock-test-package-12345"]
|
||||||
|
assert "test" in main_pkg["dependencies"]["extras"]
|
||||||
|
assert len(main_pkg["dependencies"]["extras"]["test"]) == 1
|
||||||
|
assert "pytest" in main_pkg["dependencies"]["extras"]["test"][0]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_resolve_dependencies_with_extras_and_python_version(self, resolver):
|
||||||
|
"""Test that extras work correctly with Python version filtering."""
|
||||||
|
mock_package_data = {
|
||||||
|
"info": {
|
||||||
|
"name": "test-package",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"requires_python": ">=3.8",
|
||||||
|
"requires_dist": [
|
||||||
|
"requests>=2.25.0",
|
||||||
|
"typing-extensions>=4.0.0; python_version<'3.10'",
|
||||||
|
"pytest>=6.0.0; extra=='test'",
|
||||||
|
"coverage>=5.0; extra=='test'",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mock for transitive dependencies
|
||||||
|
mock_requests_data = {
|
||||||
|
"info": {
|
||||||
|
"name": "requests",
|
||||||
|
"version": "2.25.0",
|
||||||
|
"requires_python": ">=3.6",
|
||||||
|
"requires_dist": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_pytest_data = {
|
||||||
|
"info": {
|
||||||
|
"name": "pytest",
|
||||||
|
"version": "6.0.0",
|
||||||
|
"requires_python": ">=3.6",
|
||||||
|
"requires_dist": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mock_coverage_data = {
|
||||||
|
"info": {
|
||||||
|
"name": "coverage",
|
||||||
|
"version": "5.0.0",
|
||||||
|
"requires_python": ">=3.6",
|
||||||
|
"requires_dist": [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch("pypi_query_mcp.core.PyPIClient") as mock_client_class:
|
||||||
|
mock_client = AsyncMock()
|
||||||
|
mock_client_class.return_value.__aenter__.return_value = mock_client
|
||||||
|
|
||||||
|
# Setup mock to return different data based on package name
|
||||||
|
def mock_get_package_info(package_name):
|
||||||
|
if package_name.lower() == "test-package":
|
||||||
|
return mock_package_data
|
||||||
|
elif package_name.lower() == "requests":
|
||||||
|
return mock_requests_data
|
||||||
|
elif package_name.lower() == "pytest":
|
||||||
|
return mock_pytest_data
|
||||||
|
elif package_name.lower() == "coverage":
|
||||||
|
return mock_coverage_data
|
||||||
|
else:
|
||||||
|
return {"info": {"name": package_name, "version": "1.0.0", "requires_dist": []}}
|
||||||
|
|
||||||
|
mock_client.get_package_info.side_effect = mock_get_package_info
|
||||||
|
|
||||||
|
# Test with Python 3.11 - should not include typing-extensions but should include extras
|
||||||
|
result = await resolver.resolve_dependencies(
|
||||||
|
"test-package", python_version="3.11", include_extras=["test"], max_depth=2
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["include_extras"] == ["test"]
|
||||||
|
assert result["python_version"] == "3.11"
|
||||||
|
|
||||||
|
# Verify that extras are properly resolved
|
||||||
|
assert result["summary"]["total_extra_dependencies"] == 2
|
||||||
|
main_pkg = result["dependency_tree"]["test-package"]
|
||||||
|
assert "test" in main_pkg["dependencies"]["extras"]
|
||||||
|
assert len(main_pkg["dependencies"]["extras"]["test"]) == 2
|
||||||
|
|
||||||
|
# Verify Python version filtering worked for runtime deps but not extras
|
||||||
|
runtime_deps = main_pkg["dependencies"]["runtime"]
|
||||||
|
assert len(runtime_deps) == 1 # Only requests, not typing-extensions
|
||||||
|
assert "requests" in runtime_deps[0]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_resolve_dependencies_max_depth(self, resolver):
|
async def test_resolve_dependencies_max_depth(self, resolver):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user