Skip to content

Commit

Permalink
Expose and handle --config argument (#245)
Browse files Browse the repository at this point in the history
* feat: expose and handle configuration file

* test(cli): add tests for config in different path

* docs(usage): mention that `config` can be overridden
  • Loading branch information
mkniewallner authored Dec 27, 2022
1 parent efcfc03 commit 01f6b83
Show file tree
Hide file tree
Showing 23 changed files with 203 additions and 94 deletions.
20 changes: 10 additions & 10 deletions deptry/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from deptry.compat import metadata
from deptry.config import read_configuration_from_pyproject_toml
from deptry.core import Core
from deptry.utils import PYPROJECT_TOML_PATH


class CommaSeparatedTupleParamType(click.ParamType):
Expand Down Expand Up @@ -57,6 +56,14 @@ def display_deptry_version(ctx: click.Context, _param: click.Parameter, value: b
is_eager=True,
callback=configure_logger,
)
@click.option(
"--config",
type=click.Path(path_type=Path),
is_eager=True,
callback=read_configuration_from_pyproject_toml,
help="Path to the pyproject.toml file to read configuration from.",
default="pyproject.toml",
)
@click.option(
"--skip-obsolete",
is_flag=True,
Expand Down Expand Up @@ -178,17 +185,9 @@ 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(
"--config",
type=click.Path(),
is_eager=True,
callback=read_configuration_from_pyproject_toml,
help="Path to the pyproject.toml file to read configuration from.",
default=PYPROJECT_TOML_PATH,
expose_value=False,
)
def deptry(
root: Path,
config: Path,
ignore_obsolete: tuple[str, ...],
ignore_missing: tuple[str, ...],
ignore_transitive: tuple[str, ...],
Expand All @@ -213,6 +212,7 @@ def deptry(

Core(
root=root,
config=config,
ignore_obsolete=ignore_obsolete,
ignore_missing=ignore_missing,
ignore_transitive=ignore_transitive,
Expand Down
7 changes: 4 additions & 3 deletions deptry/config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import click

from deptry.utils import load_pyproject_toml


def read_configuration_from_pyproject_toml(ctx: click.Context, _param: click.Parameter, value: str) -> str | None:
def read_configuration_from_pyproject_toml(ctx: click.Context, _param: click.Parameter, value: Path) -> Path | None:
"""
Callback that, given a click context, overrides the default values with configuration options set in a
pyproject.toml file.
Expand All @@ -22,13 +23,13 @@ def read_configuration_from_pyproject_toml(ctx: click.Context, _param: click.Par
pyproject_data = load_pyproject_toml(value)
except FileNotFoundError:
logging.debug("No pyproject.toml file to read configuration from.")
return None
return value

try:
deptry_toml_config = pyproject_data["tool"]["deptry"]
except KeyError:
logging.debug("No configuration for deptry was found in pyproject.toml.")
return None
return value

click_default_map: dict[str, Any] = {}

Expand Down
13 changes: 8 additions & 5 deletions deptry/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
@dataclass
class Core:
root: Path
config: Path
ignore_obsolete: tuple[str, ...]
ignore_missing: tuple[str, ...]
ignore_transitive: tuple[str, ...]
Expand All @@ -45,7 +46,9 @@ class Core:
def run(self) -> None:
self._log_config()

dependency_management_format = DependencySpecificationDetector(requirements_txt=self.requirements_txt).detect()
dependency_management_format = DependencySpecificationDetector(
self.config, requirements_txt=self.requirements_txt
).detect()
dependencies_extract = self._get_dependencies(dependency_management_format)

all_python_files = PythonFileFinder(
Expand Down Expand Up @@ -88,13 +91,13 @@ 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().get()
return PoetryDependencyGetter(self.config).get()
if dependency_management_format is DependencyManagementFormat.PDM:
return PDMDependencyGetter().get()
return PDMDependencyGetter(self.config).get()
if dependency_management_format is DependencyManagementFormat.PEP_621:
return PEP621DependencyGetter().get()
return PEP621DependencyGetter(self.config).get()
if dependency_management_format is DependencyManagementFormat.REQUIREMENTS_TXT:
return RequirementsTxtDependencyGetter(self.requirements_txt, self.requirements_txt_dev).get()
return RequirementsTxtDependencyGetter(self.config, self.requirements_txt, self.requirements_txt_dev).get()
raise ValueError("Incorrect dependency manage format. Only poetry, pdm and requirements.txt are supported.")

def _get_local_modules(self) -> set[str]:
Expand Down
3 changes: 3 additions & 0 deletions deptry/dependency_getter/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path

from deptry.dependency import Dependency

Expand All @@ -17,6 +18,8 @@ class DependenciesExtract:
class DependencyGetter(ABC):
"""Base class for all dependency getter."""

config: Path

@abstractmethod
def get(self) -> DependenciesExtract:
"""Get extracted dependencies and dev dependencies."""
Expand Down
7 changes: 3 additions & 4 deletions deptry/dependency_getter/pdm.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ def get(self) -> DependenciesExtract:

return DependenciesExtract(pep_621_dependencies_extract.dependencies, dev_dependencies)

@classmethod
def _get_pdm_dev_dependencies(cls) -> list[Dependency]:
def _get_pdm_dev_dependencies(self) -> list[Dependency]:
"""
Try to get development dependencies from pyproject.toml, which with PDM are specified as:
Expand All @@ -38,7 +37,7 @@ def _get_pdm_dev_dependencies(cls) -> list[Dependency]:
"tox-pdm>=0.5",
]
"""
pyproject_data = load_pyproject_toml()
pyproject_data = load_pyproject_toml(self.config)

dev_dependency_strings: list[str] = []
try:
Expand All @@ -48,4 +47,4 @@ def _get_pdm_dev_dependencies(cls) -> list[Dependency]:
except KeyError:
logging.debug("No section [tool.pdm.dev-dependencies] found in pyproject.toml")

return cls._extract_pep_508_dependencies(dev_dependency_strings)
return self._extract_pep_508_dependencies(dev_dependency_strings)
14 changes: 6 additions & 8 deletions deptry/dependency_getter/pep_621.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,16 @@ def get(self) -> DependenciesExtract:

return DependenciesExtract(dependencies, [])

@classmethod
def _get_dependencies(cls) -> list[Dependency]:
pyproject_data = load_pyproject_toml()
def _get_dependencies(self) -> list[Dependency]:
pyproject_data = load_pyproject_toml(self.config)
dependency_strings: list[str] = pyproject_data["project"]["dependencies"]
return cls._extract_pep_508_dependencies(dependency_strings)
return self._extract_pep_508_dependencies(dependency_strings)

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

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

Expand Down
14 changes: 6 additions & 8 deletions deptry/dependency_getter/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ def get(self) -> DependenciesExtract:

return DependenciesExtract(dependencies, dev_dependencies)

@classmethod
def _get_poetry_dependencies(cls) -> list[Dependency]:
pyproject_data = load_pyproject_toml()
def _get_poetry_dependencies(self) -> list[Dependency]:
pyproject_data = load_pyproject_toml(self.config)
dependencies: dict[str, Any] = pyproject_data["tool"]["poetry"]["dependencies"]
return cls._get_dependencies(dependencies)
return self._get_dependencies(dependencies)

@classmethod
def _get_poetry_dev_dependencies(cls) -> list[Dependency]:
def _get_poetry_dev_dependencies(self) -> list[Dependency]:
"""
These can be either under;
Expand All @@ -39,15 +37,15 @@ def _get_poetry_dev_dependencies(cls) -> list[Dependency]:
or both.
"""
dependencies: dict[str, str] = {}
pyproject_data = load_pyproject_toml()
pyproject_data = load_pyproject_toml(self.config)

with contextlib.suppress(KeyError):
dependencies = {**pyproject_data["tool"]["poetry"]["dev-dependencies"], **dependencies}

with contextlib.suppress(KeyError):
dependencies = {**pyproject_data["tool"]["poetry"]["group"]["dev"]["dependencies"], **dependencies}

return cls._get_dependencies(dependencies)
return self._get_dependencies(dependencies)

@classmethod
def _get_dependencies(cls, poetry_dependencies: dict[str, Any]) -> list[Dependency]:
Expand Down
24 changes: 11 additions & 13 deletions deptry/dependency_specification_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import os
from enum import Enum
from pathlib import Path

from deptry.utils import load_pyproject_toml

Expand All @@ -24,7 +25,8 @@ class DependencySpecificationDetector:
"""

def __init__(self, requirements_txt: tuple[str, ...] = ("requirements.txt",)) -> None:
def __init__(self, config: Path, requirements_txt: tuple[str, ...] = ("requirements.txt",)) -> None:
self.config = config
self.requirements_txt = requirements_txt

def detect(self) -> DependencyManagementFormat:
Expand All @@ -42,18 +44,16 @@ def detect(self) -> DependencyManagementFormat:
f" file(s) called '{', '.join(self.requirements_txt)}' found. Exiting."
)

@staticmethod
def _project_contains_pyproject_toml() -> bool:
if "pyproject.toml" in os.listdir():
def _project_contains_pyproject_toml(self) -> bool:
if self.config.exists():
logging.debug("pyproject.toml found!")
return True
else:
logging.debug("No pyproject.toml found.")
return False

@staticmethod
def _project_uses_poetry() -> bool:
pyproject_toml = load_pyproject_toml()
def _project_uses_poetry(self) -> bool:
pyproject_toml = load_pyproject_toml(self.config)
try:
pyproject_toml["tool"]["poetry"]["dependencies"]
logging.debug(
Expand All @@ -69,9 +69,8 @@ def _project_uses_poetry() -> bool:
pass
return False

@staticmethod
def _project_uses_pdm() -> bool:
pyproject_toml = load_pyproject_toml()
def _project_uses_pdm(self) -> bool:
pyproject_toml = load_pyproject_toml(self.config)
try:
pyproject_toml["tool"]["pdm"]["dev-dependencies"]
logging.debug(
Expand All @@ -87,9 +86,8 @@ def _project_uses_pdm() -> bool:
pass
return False

@staticmethod
def _project_uses_pep_621() -> bool:
pyproject_toml = load_pyproject_toml()
def _project_uses_pep_621(self) -> bool:
pyproject_toml = load_pyproject_toml(self.config)
try:
pyproject_toml["project"]
logging.debug(
Expand Down
6 changes: 2 additions & 4 deletions deptry/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@
else:
import tomli as tomllib

PYPROJECT_TOML_PATH = "./pyproject.toml"


def load_pyproject_toml(pyproject_toml_path: str = PYPROJECT_TOML_PATH) -> dict[str, Any]:
def load_pyproject_toml(config: Path) -> dict[str, Any]:
try:
with Path(pyproject_toml_path).open("rb") as pyproject_file:
with config.open("rb") as pyproject_file:
return tomllib.load(pyproject_file)
except FileNotFoundError:
raise FileNotFoundError(f"No file `pyproject.toml` found in directory {os.getcwd()}") from None
5 changes: 3 additions & 2 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
_deptry_ can be configured with command line arguments or by adding a `[tool.deptry]` section to `pyproject.toml`. Explanation for the command line arguments can
be obtained by running `deptry --help`, and examples are given below. For configuration using `pyproject.toml`, see [Configuration with pyproject.toml](./pyproject-toml.md)


## Basic Usage

_deptry_ can be run with
Expand All @@ -16,9 +15,11 @@ deptry .

where `.` is the path to the root directory of the project to be scanned. All other arguments should be specified relative to this directory.

If you want to configure _deptry_ using `pyproject.toml`, or if your dependencies are stored in `pyproject.toml`, but it is located in another location than the one _deptry_ is run from, you can specify the location to it by using `--config <path_to_pyproject.toml>` argument.

## Dependencies extraction

To determine the project's dependencies, _deptry_ will scan the root directory for files in the following order:
To determine the project's dependencies, _deptry_ will scan the directory it is run from for files in the following order:

- If a `pyproject.toml` file with a `[tool.poetry.dependencies]` section is found, _deptry_ will assume it uses Poetry and extract:
- dependencies from `[tool.poetry.dependencies]` section
Expand Down
30 changes: 30 additions & 0 deletions tests/cli/test_cli_pyproject_different_directory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import shlex
import shutil
import subprocess
from pathlib import Path

import pytest
from _pytest.tmpdir import TempPathFactory

from tests.utils import run_within_dir


@pytest.fixture(scope="session")
def pep_621_dir_with_pyproject_different_directory(tmp_path_factory: TempPathFactory) -> Path:
tmp_path_proj = tmp_path_factory.getbasetemp() / "project_with_pyproject_different_directory"
shutil.copytree("tests/data/project_with_pyproject_different_directory", str(tmp_path_proj))
with run_within_dir(tmp_path_proj):
assert subprocess.check_call(shlex.split("pip install ."), cwd="a_sub_directory") == 0
return tmp_path_proj


def test_cli_with_pyproject_different_directory(pep_621_dir_with_pyproject_different_directory: Path) -> None:
with run_within_dir(pep_621_dir_with_pyproject_different_directory):
result = subprocess.run(
shlex.split("deptry --config a_sub_directory/pyproject.toml src"), capture_output=True, text=True
)
assert result.returncode == 1
assert (
"The project contains obsolete dependencies:\n\n\tisort\n\tmypy\n\tpytest\n\trequests\n\n" in result.stderr
)
assert "There are dependencies missing from the project's list of dependencies:\n\n\twhite\n\n" in result.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[project]
# PEP 621 project metadata
# See https://www.python.org/dev/peps/pep-0621/
name = "foo"
version = "1.2.3"
requires-python = ">=3.7"
dependencies = [
"toml",
"urllib3>=1.26.12",
"isort>=5.10.1",
"click>=8.1.3",
"requests>=2.28.1",
"pkginfo>=1.8.3",
]

[project.optional-dependencies]
dev = [
"black==22.10.0",
"mypy==0.982",
]
test = [
"pytest==7.2.0",
]

[build-system]
requires = ["setuptools>=61.0.0"]
build-backend = "setuptools.build_meta"

[tool.deptry]
ignore_obsolete = ["pkginfo"]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from project_with_src_directory.foo import a_local_method
Loading

0 comments on commit 01f6b83

Please sign in to comment.