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

feat: add support for known first party modules #257

Merged
merged 4 commits into from
Jan 2, 2023
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
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