Files
Buffteks-Website/venv/lib/python3.12/site-packages/deptry/module.py
2025-05-08 21:10:14 -05:00

167 lines
6.4 KiB
Python

from __future__ import annotations
import logging
from dataclasses import dataclass, field
from importlib.metadata import PackageNotFoundError, metadata
from importlib.util import find_spec
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from deptry.dependency import Dependency
from deptry.imports.location import Location
@dataclass
class Module:
"""
Represents an imported module and its properties.
Attributes:
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.
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.
dev_top_levels: A list of development dependencies that contain this module in their
top-level module names. Can be multiple, similar to the attribute `top_levels`.
is_provided_by_dependency: Whether the module is provided by a listed dependency.
is_provided_by_dev_dependency: Whether the module is provided by a listed development dependency.
"""
name: str
standard_library: bool = False
local_module: bool = False
package: str | None = None
top_levels: list[str] | None = None
dev_top_levels: list[str] | None = None
is_provided_by_dependency: bool | None = None
is_provided_by_dev_dependency: bool | None = None
def __post_init__(self) -> None:
self._log()
def _log(self) -> None:
logging.debug("--- MODULE ---")
logging.debug(self.__str__())
logging.debug("")
def __repr__(self) -> str:
return f"Module '{self.name}'"
def __str__(self) -> str:
return "\n".join("{}: {}".format(*item) for item in vars(self).items())
@dataclass
class ModuleLocations:
module: Module
locations: list[Location] = field(default_factory=list)
class ModuleBuilder:
def __init__(
self,
name: str,
local_modules: set[str],
standard_library_modules: frozenset[str],
dependencies: list[Dependency] | None = None,
dev_dependencies: list[Dependency] | None = None,
) -> None:
"""
Create a Module object that represents an imported module.
Args:
name: The name of the imported module
local_modules: The list of local modules
standard_library_modules: The list of Python stdlib modules
dependencies: A list of the project's dependencies
dev_dependencies: A list of the project's development dependencies
"""
self.name = name
self.local_modules = local_modules
self.standard_library_modules = standard_library_modules
self.dependencies = dependencies or []
self.dev_dependencies = dev_dependencies or []
def build(self) -> Module:
"""
Build the Module object. First check if the module is in the standard library or if it's a local module,
in that case many checks can be omitted.
"""
if self._in_standard_library():
return Module(self.name, standard_library=True)
if self._is_local_module():
return Module(self.name, local_module=True)
package = self._get_package_name_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)
return Module(
self.name,
package=package,
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 = metadata(self.name)["Name"]
except PackageNotFoundError:
return self.name if self._is_package_installed() else None
else:
return name
def _is_package_installed(self) -> bool:
try:
return find_spec(self.name) is not None
except (ModuleNotFoundError, ValueError):
return False
def _get_corresponding_top_levels_from(self, dependencies: list[Dependency]) -> list[str]:
"""
Not all modules have associated metadata. e.g. `mpl_toolkits` from `matplotlib` has no metadata. However, it is
in the top-level module names of package matplotlib. This function extracts all dependencies which have 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.
"""
return [
dependency.name
for dependency in dependencies
if dependency.top_levels and self.name in dependency.top_levels
]
def _in_standard_library(self) -> bool:
return self.name in self.standard_library_modules
def _is_local_module(self) -> bool:
"""
Check if the module is a local directory with an __init__.py file.
"""
return self.name in self.local_modules
def _has_matching_dependency(self, package: 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
def _has_matching_dev_dependency(self, package: 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