Skip to content

Commit

Permalink
feat: add support for known first party modules (#257)
Browse files Browse the repository at this point in the history
* feat: add `known-first-party` option

* test: move `create_files_from_list_of_dicts` to `utils`

* test(core): add tests for `_get_local_modules`

* docs: document `known-first-party` option
  • Loading branch information
mkniewallner authored Jan 2, 2023
1 parent 1f0b0cf commit 6897cc4
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 16 deletions.
11 changes: 11 additions & 0 deletions deptry/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,15 @@ def display_deptry_version(ctx: click.Context, _param: click.Parameter, value: b
default=("dev-requirements.txt", "requirements-dev.txt"),
show_default=True,
)
@click.option(
"--known-first-party",
"-kf",
type=str,
multiple=True,
help="Modules to consider as first party ones.",
default=(),
show_default=True,
)
@click.option(
"--json-output",
"-o",
Expand All @@ -202,6 +211,7 @@ def deptry(
ignore_notebooks: bool,
requirements_txt: tuple[str, ...],
requirements_txt_dev: tuple[str, ...],
known_first_party: tuple[str, ...],
json_output: str,
) -> None:
"""Find dependency issues in your Python project.
Expand All @@ -228,5 +238,6 @@ def deptry(
skip_misplaced_dev=skip_misplaced_dev,
requirements_txt=requirements_txt,
requirements_txt_dev=requirements_txt_dev,
known_first_party=known_first_party,
json_output=json_output,
).run()
7 changes: 6 additions & 1 deletion deptry/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class Core:
ignore_notebooks: bool
requirements_txt: tuple[str, ...]
requirements_txt_dev: tuple[str, ...]
known_first_party: tuple[str, ...]
json_output: str

def run(self) -> None:
Expand Down Expand Up @@ -103,7 +104,11 @@ def _get_dependencies(self, dependency_management_format: DependencyManagementFo

def _get_local_modules(self) -> set[str]:
directories = [f for f in os.scandir(self.root) if f.is_dir()]
return {subdirectory.name for subdirectory in directories if "__init__.py" in os.listdir(subdirectory)}
guessed_local_modules = {
subdirectory.name for subdirectory in directories if "__init__.py" in os.listdir(subdirectory)
}

return guessed_local_modules | set(self.known_first_party)

def _log_config(self) -> None:
logging.debug("Running with the following configuration:")
Expand Down
18 changes: 18 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,24 @@ requirements_txt_dev = ["requirements-dev.txt", "requirements-tests.txt"]
deptry . --requirements-txt-dev requirements-dev.txt,requirements-tests.txt
```

#### Known first party

List of Python modules that should be considered as first party ones. This is useful in case _deptry_ is not able to automatically detect modules that should be considered as local ones.

- Type: `List[str]`
- Default: `[]`
- `pyproject.toml` option name: `known_first_party`
- CLI option name: `--known-first-party` (short: `-kf`)
- `pyproject.toml` example:
```toml
[tool.deptry]
known_first_party = ["bar", "foo"]
```
- CLI example:
```shell
deptry . --known-first-party bar --known-first-party foo
```

#### JSON output

Write the detected issues to a JSON file. This will write the following kind of output:
Expand Down
15 changes: 15 additions & 0 deletions tests/cli/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,21 @@ def test_cli_extend_exclude(dir_with_venv_installed: Path) -> None:
}


def test_cli_known_first_party(dir_with_venv_installed: Path) -> None:
with run_within_dir(dir_with_venv_installed):
result = subprocess.run(
shlex.split("poetry run deptry . --known-first-party white -o report.json"), capture_output=True, text=True
)

assert result.returncode == 1
assert get_issues_report() == {
"misplaced_dev": ["black"],
"missing": [],
"obsolete": ["isort", "requests"],
"transitive": [],
}


def test_cli_not_verbose(dir_with_venv_installed: Path) -> None:
with run_within_dir(dir_with_venv_installed):
result = subprocess.run(shlex.split("poetry run deptry . -o report.json"), capture_output=True, text=True)
Expand Down
70 changes: 68 additions & 2 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,77 @@
from __future__ import annotations

from pathlib import Path

import pytest

from deptry.core import Core
from tests.utils import create_files_from_list_of_dicts, run_within_dir


@pytest.mark.parametrize(
("known_first_party", "root_suffix", "expected"),
[
(
(),
"",
{"module_with_init"},
),
(
("module_without_init",),
"",
{"module_with_init", "module_without_init"},
),
(
("module_with_init", "module_without_init"),
"",
{"module_with_init", "module_without_init"},
),
(
("module_without_init",),
"module_with_init",
{"module_without_init", "subdirectory"},
),
],
)
def test__get_local_modules(
tmp_path: Path, known_first_party: tuple[str, ...], root_suffix: str, expected: set[str]
) -> None:
with run_within_dir(tmp_path):
paths = [
{"dir": "module_with_init", "file": "__init__.py"},
{"dir": "module_with_init", "file": "foo.py"},
{"dir": "module_with_init/subdirectory", "file": "__init__.py"},
{"dir": "module_with_init/subdirectory", "file": "foo.py"},
{"dir": "module_without_init", "file": "foo.py"},
]
create_files_from_list_of_dicts(paths)

assert (
Core(
root=tmp_path / root_suffix,
config=Path("pyproject.toml"),
ignore_obsolete=(),
ignore_missing=(),
ignore_transitive=(),
ignore_misplaced_dev=(),
skip_obsolete=False,
skip_missing=False,
skip_transitive=False,
skip_misplaced_dev=False,
exclude=(),
extend_exclude=(),
using_default_exclude=True,
ignore_notebooks=False,
requirements_txt=(),
requirements_txt_dev=(),
known_first_party=known_first_party,
json_output="",
)._get_local_modules()
== expected
)


def test_simple() -> None:
def test__exit_with_issues() -> None:
issues = {
"missing": ["foo"],
"obsolete": ["foo"],
Expand All @@ -19,7 +85,7 @@ def test_simple() -> None:
assert e.value.code == 1


def test_no_issues() -> None:
def test__exit_without_issues() -> None:
issues: dict[str, list[str]] = {}
with pytest.raises(SystemExit) as e:
Core._exit(issues)
Expand Down
14 changes: 1 addition & 13 deletions tests/test_python_file_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,7 @@
from pathspec.patterns.gitwildmatch import GitWildMatchPattern

from deptry.python_file_finder import PythonFileFinder
from tests.utils import run_within_dir


def create_files_from_list_of_dicts(paths: list[dict[str, str]]) -> None:
"""
Takes as input an argument paths, which is a list of dicts. Each dict should have two keys;
'dir' to denote a directory and 'file' to denote the file name. This function creates all files
within their corresponding directories.
"""
for path in paths:
Path(path["dir"]).mkdir(parents=True, exist_ok=True)
with open(Path(path["dir"]) / Path(path["file"]), "w"):
pass
from tests.utils import create_files_from_list_of_dicts, run_within_dir


def test_simple(tmp_path: Path) -> None:
Expand Down
12 changes: 12 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,15 @@ def get_issues_report(path: str = "report.json") -> dict[str, Any]:
report: dict[str, Any] = json.load(file)

return report


def create_files_from_list_of_dicts(paths: list[dict[str, str]]) -> None:
"""
Takes as input an argument paths, which is a list of dicts. Each dict should have two keys;
'dir' to denote a directory and 'file' to denote the file name. This function creates all files
within their corresponding directories.
"""
for path in paths:
Path(path["dir"]).mkdir(parents=True, exist_ok=True)
with open(Path(path["dir"]) / Path(path["file"]), "w"):
pass

0 comments on commit 6897cc4

Please sign in to comment.