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

Expose and handle --config argument #245

Merged
merged 3 commits into from
Dec 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
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