Fix self-dependency detection to strip PyPI extras brackets
The circular dependency error '_pypi/psutil → _pypi/psutil' occurred because dependencies with extras like 'psutil[test]' weren't being recognized as self-dependencies. The comparison 'psutil[test] != psutil' failed. - Add _normalize_pypi_package_name() helper that strips extras brackets and normalizes separators per PEP 503 - Update _detect_package_cycle to use normalized names for cycle detection - Update check_circular_dependencies to use normalized initial path - Simplify self-dependency check in resolve_dependencies to use helper
This commit is contained in:
@@ -47,6 +47,22 @@ from .schemas import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_pypi_package_name(name: str) -> str:
|
||||||
|
"""
|
||||||
|
Normalize a PyPI package name for comparison.
|
||||||
|
|
||||||
|
- Strips extras brackets (e.g., "package[extra]" -> "package")
|
||||||
|
- Replaces sequences of hyphens, underscores, and dots with a single hyphen
|
||||||
|
- Lowercases the result
|
||||||
|
|
||||||
|
This follows PEP 503 normalization rules.
|
||||||
|
"""
|
||||||
|
# Strip extras brackets like [test], [dev], etc.
|
||||||
|
base_name = re.sub(r'\[.*\]', '', name)
|
||||||
|
# Normalize separators and lowercase
|
||||||
|
return re.sub(r'[-_.]+', '-', base_name).lower()
|
||||||
|
|
||||||
|
|
||||||
class DependencyError(Exception):
|
class DependencyError(Exception):
|
||||||
"""Base exception for dependency errors."""
|
"""Base exception for dependency errors."""
|
||||||
pass
|
pass
|
||||||
@@ -514,10 +530,16 @@ def _detect_package_cycle(
|
|||||||
Returns:
|
Returns:
|
||||||
Cycle path if detected, None otherwise
|
Cycle path if detected, None otherwise
|
||||||
"""
|
"""
|
||||||
pkg_key = f"{project_name}/{package_name}"
|
# Normalize names for comparison (handles extras like [test] and separators)
|
||||||
|
pkg_normalized = _normalize_pypi_package_name(package_name)
|
||||||
|
target_pkg_normalized = _normalize_pypi_package_name(target_package)
|
||||||
|
|
||||||
|
# Use normalized key for tracking
|
||||||
|
pkg_key = f"{project_name.lower()}/{pkg_normalized}"
|
||||||
|
|
||||||
# Check if we've reached the target package (cycle detected)
|
# Check if we've reached the target package (cycle detected)
|
||||||
if project_name == target_project and package_name == target_package:
|
# Use normalized comparison to handle extras and naming variations
|
||||||
|
if project_name.lower() == target_project.lower() and pkg_normalized == target_pkg_normalized:
|
||||||
return path + [pkg_key]
|
return path + [pkg_key]
|
||||||
|
|
||||||
if pkg_key in visiting:
|
if pkg_key in visiting:
|
||||||
@@ -619,12 +641,15 @@ def check_circular_dependencies(
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Normalize the initial path for consistency with _detect_package_cycle
|
||||||
|
normalized_path = f"{target_project.lower()}/{_normalize_pypi_package_name(target_package)}"
|
||||||
|
|
||||||
# For each new dependency, check if it would create a cycle back to our package
|
# For each new dependency, check if it would create a cycle back to our package
|
||||||
for dep in new_dependencies:
|
for dep in new_dependencies:
|
||||||
# Check if this dependency (transitively) depends on us at the package level
|
# Check if this dependency (transitively) depends on us at the package level
|
||||||
visiting: Set[str] = set()
|
visiting: Set[str] = set()
|
||||||
visited: Set[str] = set()
|
visited: Set[str] = set()
|
||||||
path: List[str] = [current_path]
|
path: List[str] = [normalized_path]
|
||||||
|
|
||||||
# Check from the dependency's package
|
# Check from the dependency's package
|
||||||
cycle = _detect_package_cycle(
|
cycle = _detect_package_cycle(
|
||||||
@@ -782,11 +807,11 @@ def resolve_dependencies(
|
|||||||
# Resolve each dependency first (depth-first)
|
# Resolve each dependency first (depth-first)
|
||||||
for dep in deps:
|
for dep in deps:
|
||||||
# Skip self-dependencies (can happen with PyPI extras like pytest[testing])
|
# Skip self-dependencies (can happen with PyPI extras like pytest[testing])
|
||||||
# Use case-insensitive comparison and normalize for PyPI naming conventions
|
# Use normalized comparison for PyPI naming conventions (handles extras, separators)
|
||||||
dep_proj_normalized = dep.dependency_project.lower()
|
dep_proj_normalized = dep.dependency_project.lower()
|
||||||
dep_pkg_normalized = re.sub(r'[-_.]+', '-', dep.dependency_package).lower()
|
dep_pkg_normalized = _normalize_pypi_package_name(dep.dependency_package)
|
||||||
curr_proj_normalized = proj_name.lower()
|
curr_proj_normalized = proj_name.lower()
|
||||||
curr_pkg_normalized = re.sub(r'[-_.]+', '-', pkg_name).lower()
|
curr_pkg_normalized = _normalize_pypi_package_name(pkg_name)
|
||||||
if dep_proj_normalized == curr_proj_normalized and dep_pkg_normalized == curr_pkg_normalized:
|
if dep_proj_normalized == curr_proj_normalized and dep_pkg_normalized == curr_pkg_normalized:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user