Skip to content

Commit

Permalink
feat(module): use get_distributions_from_package
Browse files Browse the repository at this point in the history
  • Loading branch information
mkniewallner committed Sep 15, 2024
1 parent 9868d4d commit 56cfa56
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 38 deletions.
55 changes: 34 additions & 21 deletions python/deptry/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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]:
"""
Expand All @@ -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
2 changes: 1 addition & 1 deletion python/deptry/violations/dep001_missing/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 6 additions & 4 deletions python/deptry/violations/dep002_unused/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions python/deptry/violations/dep003_transitive/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ 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,
]):
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
16 changes: 8 additions & 8 deletions python/deptry/violations/dep004_misplaced_dev/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tests/functional/cli/test_cli_pep_621.py
Original file line number Diff line number Diff line change
Expand Up @@ -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},
},
]

0 comments on commit 56cfa56

Please sign in to comment.