Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A way to pre-configure a map of package names to module names #333

Merged
merged 4 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions deptry/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

import logging
from pathlib import Path
from typing import TYPE_CHECKING

import click

from deptry.compat import metadata
from deptry.config import read_configuration_from_pyproject_toml
from deptry.core import Core

if TYPE_CHECKING:
from collections.abc import Mapping

DEFAULT_EXCLUDE = ("venv", r"\.venv", r"\.direnv", "tests", r"\.git", r"setup\.py")


Expand Down Expand Up @@ -37,6 +41,46 @@ def convert(
COMMA_SEPARATED_TUPLE = CommaSeparatedTupleParamType()


class CommaSeparatedMappingParamType(click.ParamType):
"""
This class is used to uniformly handle configuration parameters that can be either passed as a comma-separated pair
string, or as a Mapping of strings to tuples of strings. Items in a pair string are separated by an equal sign,
where multiple values are separated by a pipe: key1=value1,key2=value2|value3.
For example, the value for a parameter can be a comma-separated pair string when passed as a command line argument,
or as a mapping of string to tuples of strings when passed through pyproject.toml.
"""

name = "mapping"

def convert(
self,
value: str | Mapping[str, tuple[str, ...]],
param: click.Parameter | None,
ctx: click.Context | None,
) -> Mapping[str, tuple[str, ...]]:
converted: Mapping[str, tuple[str, ...]]
if isinstance(value, str):
map_: dict[str, tuple[str, ...]] = {}
for item in value.split(","):
pair = tuple(item.split("=", 1))
if len(pair) != 2:
error_text = (
f"package name and module names pairs should be concatenated with an equal sign (=): {item}"
)
raise ValueError(error_text)
package_name = pair[0]
module_names = tuple(pair[1].split("|"))
map_[package_name] = module_names
converted = map_
else:
converted = value

return converted


COMMA_SEPARATED_MAPPING = CommaSeparatedMappingParamType()


def configure_logger(_ctx: click.Context, _param: click.Parameter, value: bool) -> None:
log_level = logging.DEBUG if value else logging.INFO
logging.basicConfig(level=log_level, handlers=[logging.StreamHandler()], format="%(message)s")
Expand Down Expand Up @@ -201,6 +245,14 @@ def display_deptry_version(ctx: click.Context, _param: click.Parameter, value: b
help="""If specified, a summary of the dependency issues found will be written to the output location specified. e.g. `deptry . -o deptry.json`""",
show_default=True,
)
@click.option(
"--package-module-name-map",
"-pmnm",
type=COMMA_SEPARATED_MAPPING,
help="""Manually defined module names belonging to packages. For example; `deptry . --package-module-name-map package_1=module_a,package_2=module_b|module_c`.""",
default={},
show_default=False,
)
def deptry(
root: Path,
config: Path,
Expand All @@ -219,6 +271,7 @@ def deptry(
requirements_txt_dev: tuple[str, ...],
known_first_party: tuple[str, ...],
json_output: str,
package_module_name_map: Mapping[str, tuple[str, ...]],
) -> None:
"""Find dependency issues in your Python project.

Expand Down Expand Up @@ -246,4 +299,5 @@ def deptry(
requirements_txt_dev=requirements_txt_dev,
known_first_party=known_first_party,
json_output=json_output,
package_module_name_map=package_module_name_map,
).run()
12 changes: 8 additions & 4 deletions deptry/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from deptry.stdlibs import STDLIBS_PYTHON

if TYPE_CHECKING:
from collections.abc import Mapping
from pathlib import Path

from deptry.dependency import Dependency
Expand Down Expand Up @@ -51,6 +52,7 @@ class Core:
requirements_txt_dev: tuple[str, ...]
known_first_party: tuple[str, ...]
json_output: str
package_module_name_map: Mapping[str, tuple[str, ...]]

def run(self) -> None:
self._log_config()
Expand Down Expand Up @@ -105,13 +107,15 @@ def _find_issues(self, imported_modules: list[Module], dependencies: list[Depend

def _get_dependencies(self, dependency_management_format: DependencyManagementFormat) -> DependenciesExtract:
if dependency_management_format is DependencyManagementFormat.POETRY:
return PoetryDependencyGetter(self.config).get()
return PoetryDependencyGetter(self.config, self.package_module_name_map).get()
if dependency_management_format is DependencyManagementFormat.PDM:
return PDMDependencyGetter(self.config).get()
return PDMDependencyGetter(self.config, self.package_module_name_map).get()
if dependency_management_format is DependencyManagementFormat.PEP_621:
return PEP621DependencyGetter(self.config).get()
return PEP621DependencyGetter(self.config, self.package_module_name_map).get()
if dependency_management_format is DependencyManagementFormat.REQUIREMENTS_TXT:
return RequirementsTxtDependencyGetter(self.config, self.requirements_txt, self.requirements_txt_dev).get()
return RequirementsTxtDependencyGetter(
self.config, self.package_module_name_map, self.requirements_txt, self.requirements_txt_dev
).get()
raise IncorrectDependencyFormatError

def _get_local_modules(self) -> set[str]:
Expand Down
14 changes: 9 additions & 5 deletions deptry/dependency.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ class Dependency:
By default, we also add the dependency's name with '-' replaced by '_' to the top-level modules.
"""

def __init__(self, name: str, conditional: bool = False, optional: bool = False) -> None:
def __init__(
self, name: str, conditional: bool = False, optional: bool = False, module_names: tuple[str, ...] | None = None
) -> None:
"""
Args:
name: Name of the dependency, as shown in pyproject.toml
Expand All @@ -24,12 +26,14 @@ def __init__(self, name: str, conditional: bool = False, optional: bool = False)
self.is_conditional = conditional
self.is_optional = optional
self.found = self.find_metadata(name)
self.top_levels = self._get_top_levels(name)
self.top_levels = self._get_top_levels(name, module_names)

def _get_top_levels(self, name: str) -> set[str]:
top_levels = []
def _get_top_levels(self, name: str, module_names: tuple[str, ...] | None) -> set[str]:
top_levels: list[str] = []

if self.found:
if module_names is not None:
top_levels += module_names
elif self.found:
top_levels += self._get_top_level_module_names_from_top_level_txt()
if not top_levels:
top_levels += self._get_top_level_module_names_from_record_file()
Expand Down
4 changes: 3 additions & 1 deletion deptry/dependency_getter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from collections.abc import Mapping
from pathlib import Path

from deptry.dependency import Dependency
Expand All @@ -26,6 +27,7 @@ class DependencyGetter(ABC):
"""

config: Path
package_module_name_map: Mapping[str, tuple[str, ...]] = field(default_factory=dict)

@abstractmethod
def get(self) -> DependenciesExtract:
Expand Down
2 changes: 1 addition & 1 deletion deptry/dependency_getter/pdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ def _get_pdm_dev_dependencies(self) -> list[Dependency]:
except KeyError:
logging.debug("No section [tool.pdm.dev-dependencies] found in pyproject.toml")

return self._extract_pep_508_dependencies(dev_dependency_strings)
return self._extract_pep_508_dependencies(dev_dependency_strings, self.package_module_name_map)
19 changes: 15 additions & 4 deletions deptry/dependency_getter/pep_621.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@
import itertools
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING

from deptry.dependency import Dependency
from deptry.dependency_getter.base import DependenciesExtract, DependencyGetter
from deptry.utils import load_pyproject_toml

if TYPE_CHECKING:
from collections.abc import Mapping


@dataclass
class PEP621DependencyGetter(DependencyGetter):
Expand Down Expand Up @@ -42,18 +46,20 @@ def get(self) -> DependenciesExtract:
def _get_dependencies(self) -> list[Dependency]:
pyproject_data = load_pyproject_toml(self.config)
dependency_strings: list[str] = pyproject_data["project"]["dependencies"]
return self._extract_pep_508_dependencies(dependency_strings)
return self._extract_pep_508_dependencies(dependency_strings, self.package_module_name_map)

def _get_optional_dependencies(self) -> dict[str, list[Dependency]]:
pyproject_data = load_pyproject_toml(self.config)

return {
group: self._extract_pep_508_dependencies(dependencies)
group: self._extract_pep_508_dependencies(dependencies, self.package_module_name_map)
for group, dependencies in pyproject_data["project"].get("optional-dependencies", {}).items()
}

@classmethod
def _extract_pep_508_dependencies(cls, dependencies: list[str]) -> list[Dependency]:
def _extract_pep_508_dependencies(
cls, dependencies: list[str], package_module_name_map: Mapping[str, tuple[str, ...]]
) -> list[Dependency]:
"""
Given a list of dependency specifications (e.g. "django>2.1; os_name != 'nt'"), convert them to Dependency objects.
"""
Expand All @@ -64,7 +70,12 @@ def _extract_pep_508_dependencies(cls, dependencies: list[str]) -> list[Dependen
name = cls._find_dependency_name_in(spec)
if name:
extracted_dependencies.append(
Dependency(name, conditional=cls._is_conditional(spec), optional=cls._is_optional(spec))
Dependency(
name,
conditional=cls._is_conditional(spec),
optional=cls._is_optional(spec),
module_names=package_module_name_map.get(name),
)
)

return extracted_dependencies
Expand Down
19 changes: 14 additions & 5 deletions deptry/dependency_getter/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

import contextlib
from dataclasses import dataclass
from typing import Any
from typing import TYPE_CHECKING, Any

from deptry.dependency import Dependency
from deptry.dependency_getter.base import DependenciesExtract, DependencyGetter
from deptry.utils import load_pyproject_toml

if TYPE_CHECKING:
from collections.abc import Mapping


@dataclass
class PoetryDependencyGetter(DependencyGetter):
Expand All @@ -25,7 +28,7 @@ def get(self) -> DependenciesExtract:
def _get_poetry_dependencies(self) -> list[Dependency]:
pyproject_data = load_pyproject_toml(self.config)
dependencies: dict[str, Any] = pyproject_data["tool"]["poetry"]["dependencies"]
return self._get_dependencies(dependencies)
return self._get_dependencies(dependencies, self.package_module_name_map)

def _get_poetry_dev_dependencies(self) -> list[Dependency]:
"""
Expand All @@ -45,17 +48,23 @@ def _get_poetry_dev_dependencies(self) -> list[Dependency]:
with contextlib.suppress(KeyError):
dependencies = {**pyproject_data["tool"]["poetry"]["group"]["dev"]["dependencies"], **dependencies}

return self._get_dependencies(dependencies)
return self._get_dependencies(dependencies, self.package_module_name_map)

@classmethod
def _get_dependencies(cls, poetry_dependencies: dict[str, Any]) -> list[Dependency]:
def _get_dependencies(
cls, poetry_dependencies: dict[str, Any], package_module_name_map: Mapping[str, tuple[str, ...]]
) -> list[Dependency]:
dependencies = []
for dep, spec in poetry_dependencies.items():
# dep is the dependency name, spec is the version specification, e.g. "^0.2.2" or {"*", optional = true}
if dep != "python":
optional = cls._is_optional(spec)
conditional = cls._is_conditional(spec)
dependencies.append(Dependency(dep, conditional=conditional, optional=optional))
dependencies.append(
Dependency(
dep, conditional=conditional, optional=optional, module_names=package_module_name_map.get(dep)
)
)

return dependencies

Expand Down
7 changes: 6 additions & 1 deletion deptry/dependency_getter/requirements_txt.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ def _extract_dependency_from_line(self, line: str) -> Dependency | None:
line = line.replace(name, "")
optional = self._check_if_dependency_is_optional(line)
conditional = self._check_if_dependency_is_conditional(line)
return Dependency(name=name, optional=optional, conditional=conditional)
return Dependency(
name=name,
optional=optional,
conditional=conditional,
module_names=self.package_module_name_map.get(name),
)
else:
return None

Expand Down
1 change: 1 addition & 0 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def test__get_local_modules(
requirements_txt_dev=(),
known_first_party=known_first_party,
json_output="",
package_module_name_map={},
)._get_local_modules()
== expected
)
Expand Down