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

173 lines
6.3 KiB
Python

from __future__ import annotations
import logging
import os
import sys
from dataclasses import dataclass
from typing import TYPE_CHECKING
from deptry.dependency_getter.builder import DependencyGetterBuilder
from deptry.exceptions import UnsupportedPythonVersionError
from deptry.imports.extract import get_imported_modules_from_list_of_files
from deptry.module import ModuleBuilder, ModuleLocations
from deptry.python_file_finder import get_all_python_files_in
from deptry.reporters import JSONReporter, TextReporter
from deptry.stdlibs import STDLIBS_PYTHON
from deptry.violations.finder import find_violations
if TYPE_CHECKING:
from collections.abc import Mapping
from pathlib import Path
from deptry.dependency_getter.base import DependenciesExtract
from deptry.violations import Violation
@dataclass
class Core:
root: tuple[Path, ...]
config: Path
no_ansi: bool
per_rule_ignores: Mapping[str, tuple[str, ...]]
ignore: tuple[str, ...]
exclude: tuple[str, ...]
extend_exclude: tuple[str, ...]
using_default_exclude: bool
ignore_notebooks: bool
requirements_files: tuple[str, ...]
using_default_requirements_files: bool
requirements_files_dev: tuple[str, ...]
known_first_party: tuple[str, ...]
json_output: str
package_module_name_map: Mapping[str, tuple[str, ...]]
pep621_dev_dependency_groups: tuple[str, ...]
experimental_namespace_package: bool
def run(self) -> None:
self._log_config()
dependency_getter = DependencyGetterBuilder(
self.config,
self.package_module_name_map,
self.pep621_dev_dependency_groups,
self.requirements_files,
self.using_default_requirements_files,
self.requirements_files_dev,
).build()
dependencies_extract = dependency_getter.get()
self._log_dependencies(dependencies_extract)
python_files = self._find_python_files()
local_modules = self._get_local_modules()
standard_library_modules = self._get_standard_library_modules()
imported_modules_with_locations = [
ModuleLocations(
ModuleBuilder(
module,
local_modules,
standard_library_modules,
dependencies_extract.dependencies,
dependencies_extract.dev_dependencies,
).build(),
locations,
)
for module, locations in get_imported_modules_from_list_of_files(python_files).items()
]
violations = find_violations(
imported_modules_with_locations,
dependencies_extract.dependencies,
self.ignore,
self.per_rule_ignores,
standard_library_modules,
)
TextReporter(violations, use_ansi=not self.no_ansi).report()
if self.json_output:
JSONReporter(violations, self.json_output).report()
self._exit(violations)
def _find_python_files(self) -> list[Path]:
logging.debug("Collecting Python files to scan...")
python_files = get_all_python_files_in(
self.root, self.exclude, self.extend_exclude, self.using_default_exclude, self.ignore_notebooks
)
logging.debug(
"Python files to scan for imports:\n%s\n", "\n".join(str(python_file) for python_file in python_files)
)
return python_files
def _get_local_modules(self) -> set[str]:
"""
Get all local Python modules from the source directories and `known_first_party` list.
A module is considered a local Python module if it matches at least one of those conditions:
- it is a directory that contains at least one Python file
- it is a Python file that is not named `__init__.py` (since it is a special case)
- it is set in the `known_first_party` list
"""
guessed_local_modules = {
path.stem for source in self.root for path in source.iterdir() if self._is_local_module(path)
}
return guessed_local_modules | set(self.known_first_party)
def _is_local_module(self, path: Path) -> bool:
"""Guess if a module is a local Python module."""
return bool(
(path.is_file() and path.name != "__init__.py" and path.suffix == ".py")
or (path.is_dir() and self._directory_has_python_files(path))
)
def _directory_has_python_files(self, path: Path) -> bool:
"""Check if there is any Python file in the current directory. If experimental support for namespace packages
(PEP 420) is enabled, also search for Python files in subdirectories."""
if self.experimental_namespace_package:
for _root, _dirs, files in os.walk(path):
for file in files:
if file.endswith(".py"):
return True
return False
return bool(list(path.glob("*.py")))
@staticmethod
def _get_standard_library_modules() -> frozenset[str]:
if sys.version_info[:2] >= (3, 10):
return sys.stdlib_module_names
try: # type: ignore[unreachable, unused-ignore]
return STDLIBS_PYTHON[f"{sys.version_info[0]}{sys.version_info[1]}"]
except KeyError as e:
raise UnsupportedPythonVersionError((sys.version_info[0], sys.version_info[1])) from e
def _log_config(self) -> None:
logging.debug("Running with the following configuration:")
for key, value in vars(self).items():
logging.debug("%s: %s", key, value)
logging.debug("")
@staticmethod
def _log_dependencies(dependencies_extract: DependenciesExtract) -> None:
if dependencies_extract.dependencies:
logging.debug("The project contains the following dependencies:")
for dependency in dependencies_extract.dependencies:
logging.debug(dependency)
logging.debug("")
if dependencies_extract.dev_dependencies:
logging.debug("The project contains the following dev dependencies:")
for dependency in dependencies_extract.dev_dependencies:
logging.debug(dependency)
logging.debug("")
@staticmethod
def _exit(violations: list[Violation]) -> None:
sys.exit(bool(violations))