173 lines
6.3 KiB
Python
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))
|