diff --git a/python/deptry/module.py b/python/deptry/module.py index e4221a67..6e6dc4e6 100644 --- a/python/deptry/module.py +++ b/python/deptry/module.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field from typing import TYPE_CHECKING -from deptry.compat import importlib_metadata +from deptry.distribution import get_distributions_from_package if TYPE_CHECKING: from deptry.dependency import Dependency @@ -20,7 +20,7 @@ class Module: name: The name of the imported module. standard_library: Whether the module is part of the Python standard library. local_module: Whether the module is a local module. - package: The name of the package that contains the module. + packages: The names of the packages that contain the module. top_levels: A list of dependencies that contain this module in their top-level module names. This can be multiple, e.g. `google-cloud-api` and `google-cloud-bigquery` both have `google` in their top-level module names. @@ -33,7 +33,7 @@ class Module: name: str standard_library: bool = False local_module: bool = False - package: str | None = None + packages: list[str] | None = None top_levels: list[str] | None = None dev_top_levels: list[str] | None = None is_provided_by_dependency: bool | None = None @@ -97,31 +97,26 @@ def build(self) -> Module: if self._is_local_module(): return Module(self.name, local_module=True) - package = self._get_package_name_from_metadata() + packages = self._get_package_names_from_metadata() top_levels = self._get_corresponding_top_levels_from(self.dependencies) dev_top_levels = self._get_corresponding_top_levels_from(self.dev_dependencies) - is_provided_by_dependency = self._has_matching_dependency(package, top_levels) - is_provided_by_dev_dependency = self._has_matching_dev_dependency(package, dev_top_levels) + is_provided_by_dependency = self._has_matching_dependency(packages, top_levels) + is_provided_by_dev_dependency = self._has_matching_dev_dependency(packages, dev_top_levels) + return Module( self.name, - package=package, + packages=packages, top_levels=top_levels, dev_top_levels=dev_top_levels, is_provided_by_dependency=is_provided_by_dependency, is_provided_by_dev_dependency=is_provided_by_dev_dependency, ) - def _get_package_name_from_metadata(self) -> str | None: - """ - Most packages simply have a field called "Name" in their metadata. This method extracts that field. - """ - try: - name: str = importlib_metadata.metadata(self.name)["Name"] - except importlib_metadata.PackageNotFoundError: - return None - else: - return name + def _get_package_names_from_metadata(self) -> list[str] | None: + if distributions := get_distributions_from_package(self.name): + return list(distributions) + return None def _get_corresponding_top_levels_from(self, dependencies: list[Dependency]) -> list[str]: """ @@ -146,15 +141,33 @@ def _is_local_module(self) -> bool: """ return self.name in self.local_modules - def _has_matching_dependency(self, package: str | None, top_levels: list[str]) -> bool: + def _has_matching_dependency(self, packages: list[str] | None, top_levels: list[str]) -> bool: """ Check if this module is provided by a listed dependency. This is the case if either the package name that was found in the metadata is listed as a dependency, or if we found a top-level module name match earlier. """ - return package and (package in [dep.name for dep in self.dependencies]) or len(top_levels) > 0 + if len(top_levels) > 0: + return True + + if packages: + for dep in self.dependencies: + for package in packages: + if dep.name == package: + return True - def _has_matching_dev_dependency(self, package: str | None, dev_top_levels: list[str]) -> bool: + return False + + def _has_matching_dev_dependency(self, packages: list[str] | None, dev_top_levels: list[str]) -> bool: """ Same as _has_matching_dependency, but for development dependencies. """ - return package and (package in [dep.name for dep in self.dev_dependencies]) or len(dev_top_levels) > 0 + if len(dev_top_levels) > 0: + return True + + if packages: + for dep in self.dev_dependencies: + for package in packages: + if dep.name == package: + return True + + return False diff --git a/python/deptry/violations/dep001_missing/finder.py b/python/deptry/violations/dep001_missing/finder.py index 893b801a..64dfaa66 100644 --- a/python/deptry/violations/dep001_missing/finder.py +++ b/python/deptry/violations/dep001_missing/finder.py @@ -40,7 +40,7 @@ def find(self) -> list[Violation]: def _is_missing(self, module: Module) -> bool: if any([ - module.package is not None, + module.packages is not None, module.is_provided_by_dependency, module.is_provided_by_dev_dependency, module.local_module, diff --git a/python/deptry/violations/dep002_unused/finder.py b/python/deptry/violations/dep002_unused/finder.py index f364e39a..0dc3a01d 100644 --- a/python/deptry/violations/dep002_unused/finder.py +++ b/python/deptry/violations/dep002_unused/finder.py @@ -53,10 +53,12 @@ def _is_unused(self, dependency: Dependency) -> bool: return True def _dependency_found_in_imported_modules(self, dependency: Dependency) -> bool: - return any( - module_with_locations.module.package == dependency.name - for module_with_locations in self.imported_modules_with_locations - ) + for module_with_locations in self.imported_modules_with_locations: + if module_with_locations.module.packages: + for package in module_with_locations.module.packages: + if package == dependency.name: + return True + return False def _any_of_the_top_levels_imported(self, dependency: Dependency) -> bool: if not dependency.top_levels: diff --git a/python/deptry/violations/dep003_transitive/finder.py b/python/deptry/violations/dep003_transitive/finder.py index d103a102..1a1d1109 100644 --- a/python/deptry/violations/dep003_transitive/finder.py +++ b/python/deptry/violations/dep003_transitive/finder.py @@ -48,7 +48,7 @@ def find(self) -> list[Violation]: def _is_transitive(self, module: Module) -> bool: if any([ - module.package is None, + module.packages is None, module.is_provided_by_dependency, module.is_provided_by_dev_dependency, module.local_module, @@ -56,8 +56,8 @@ def _is_transitive(self, module: Module) -> bool: return False if module.name in self.ignored_modules: - logging.debug("Dependency '%s' found to be a transitive dependency, but ignoring.", module.package) + logging.debug("Dependency '%s' found to be a transitive dependency, but ignoring.", module.packages) return False - logging.debug("Dependency '%s' marked as a transitive dependency.", module.package) + logging.debug("Dependency '%s' marked as a transitive dependency.", module.packages) return True diff --git a/python/deptry/violations/dep004_misplaced_dev/finder.py b/python/deptry/violations/dep004_misplaced_dev/finder.py index 62af53d3..d2212ae6 100644 --- a/python/deptry/violations/dep004_misplaced_dev/finder.py +++ b/python/deptry/violations/dep004_misplaced_dev/finder.py @@ -40,15 +40,15 @@ def find(self) -> list[Violation]: continue logging.debug("Scanning module %s...", module.name) - corresponding_package_name = self._get_package_name(module) + corresponding_package_names = self._get_package_names(module) - if corresponding_package_name and self._is_development_dependency(module, corresponding_package_name): + if corresponding_package_names and self._is_development_dependency(module, corresponding_package_names): for location in module_with_locations.locations: misplaced_dev_dependencies.append(self.violation(module, location)) return misplaced_dev_dependencies - def _is_development_dependency(self, module: Module, corresponding_package_name: str) -> bool: + def _is_development_dependency(self, module: Module, corresponding_package_names: list[str]) -> bool: # Module can be provided both by a regular and by a development dependency. # Only continue if module is ONLY provided by a dev dependency. if not module.is_provided_by_dev_dependency or module.is_provided_by_dependency: @@ -57,16 +57,16 @@ def _is_development_dependency(self, module: Module, corresponding_package_name: if module.name in self.ignored_modules: logging.debug( "Dependency '%s' found to be a misplaced development dependency, but ignoring.", - corresponding_package_name, + corresponding_package_names, ) return False - logging.debug("Dependency '%s' marked as a misplaced development dependency.", corresponding_package_name) + logging.debug("Dependency '%s' marked as a misplaced development dependency.", corresponding_package_names) return True - def _get_package_name(self, module: Module) -> str | None: - if module.package: - return module.package + def _get_package_names(self, module: Module) -> str | None: + if module.packages: + return module.packages[0] if module.dev_top_levels: if len(module.dev_top_levels) > 1: logging.debug( diff --git a/tests/functional/cli/test_cli_pep_621.py b/tests/functional/cli/test_cli_pep_621.py index 0788fa14..82ac2fc2 100644 --- a/tests/functional/cli/test_cli_pep_621.py +++ b/tests/functional/cli/test_cli_pep_621.py @@ -71,6 +71,6 @@ def test_cli_with_pep_621(pip_venv_factory: PipVenvFactory) -> None: { "error": {"code": "DEP003", "message": "'bs4' imported but it is a transitive dependency"}, "module": "bs4", - "location": {"file": str(Path("src/main.py")), "line": 7, "column": 8}, + "location": {"file": str(Path("src/main.py")), "line": 9, "column": 8}, }, ]