diff --git a/.gitignore b/.gitignore index d2b8df37..992dbc08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ docs/source +deptry.json # From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore diff --git a/deptry/cli.py b/deptry/cli.py index ce6700cf..b6055a9e 100644 --- a/deptry/cli.py +++ b/deptry/cli.py @@ -127,6 +127,14 @@ default=DEFAULTS["requirements_txt_dev"], show_default=True, ) +@click.option( + "--json-output", + "-o", + type=click.STRING, + help="""If specified, a summary of the dependency issues found will be written to the output location specified. e.g. `deptry . -o deptry.json`""", + default=DEFAULTS["json_output"], + show_default=True, +) def deptry( root: pathlib.Path, verbose: bool, @@ -143,6 +151,7 @@ def deptry( ignore_notebooks: bool, requirements_txt: str, requirements_txt_dev: str, + json_output: str, version: bool, ) -> None: """Find dependency issues in your Python project. @@ -198,6 +207,7 @@ def deptry( skip_misplaced_dev=config.skip_misplaced_dev, requirements_txt=config.requirements_txt, requirements_txt_dev=config.requirements_txt_dev, + json_output=json_output, ).run() diff --git a/deptry/cli_defaults.py b/deptry/cli_defaults.py index 0948223e..52ef086d 100644 --- a/deptry/cli_defaults.py +++ b/deptry/cli_defaults.py @@ -12,4 +12,5 @@ "skip_misplaced_dev": False, "requirements_txt": "requirements.txt", "requirements_txt_dev": "dev-requirements.txt,requirements-dev.txt", + "json_output": "", } diff --git a/deptry/core.py b/deptry/core.py index cd794efd..2e018aa5 100644 --- a/deptry/core.py +++ b/deptry/core.py @@ -12,6 +12,7 @@ from deptry.issue_finders.missing import MissingDependenciesFinder from deptry.issue_finders.obsolete import ObsoleteDependenciesFinder from deptry.issue_finders.transitive import TransitiveDependenciesFinder +from deptry.json_writer import JsonWriter from deptry.module import Module, ModuleBuilder from deptry.python_file_finder import PythonFileFinder from deptry.result_logger import ResultLogger @@ -33,6 +34,7 @@ def __init__( ignore_notebooks: bool, requirements_txt: str, requirements_txt_dev: List[str], + json_output: str, ) -> None: self.ignore_obsolete = ignore_obsolete self.ignore_missing = ignore_missing @@ -47,6 +49,7 @@ def __init__( self.skip_misplaced_dev = skip_misplaced_dev self.requirements_txt = requirements_txt self.requirements_txt_dev = requirements_txt_dev + self.json_output = json_output def run(self) -> None: @@ -65,6 +68,10 @@ def run(self) -> None: issues = self._find_issues(imported_modules, dependencies, dev_dependencies) ResultLogger(issues=issues).log_and_exit() + + if self.json_output: + JsonWriter(self.json_output).write(issues=issues) + self._exit(issues) def _find_issues( diff --git a/deptry/json_writer.py b/deptry/json_writer.py new file mode 100644 index 00000000..f2858c20 --- /dev/null +++ b/deptry/json_writer.py @@ -0,0 +1,18 @@ +import json +from typing import Dict + + +class JsonWriter: + """ + Class to write issues to a json file + + Args: + json_output: file path to store output, e.g. `output.json` + """ + + def __init__(self, json_output: str) -> None: + self.json_output = json_output + + def write(self, issues: Dict): + with open(self.json_output, "w", encoding="utf-8") as f: + json.dump(issues, f, ensure_ascii=False, indent=4) diff --git a/docs/docs/usage.md b/docs/docs/usage.md index 959e5ea4..9b56ff79 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -109,4 +109,25 @@ deptry . \ --requirements-txt-dev req/dev.txt,req/test.txt ``` -Here, the `requirements-txt` takes only a single file as argument, but multiple files can be passed to `requirements-txt-dev` by providing them as a comma-separated list. \ No newline at end of file +Here, the `requirements-txt` takes only a single file as argument, but multiple files can be passed to `requirements-txt-dev` by providing them as a comma-separated list. + +## Output as a json file + +_deptry_ can be configured to write the detected issues to a json file by specifying the `--json-output` (`-o`) flag. For example: + +``` +deptry . -o deptry.json +``` + +An example of the contents of the resulting `deptry.json` file is as follows: + +``` +{ + "obsolete": [ + "foo" + ], + "missing": [], + "transitive": [], + "misplaced_dev": [] +} +``` \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py index 7afc883c..fb47f1f8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,5 @@ +import json +import pathlib import shlex import shutil import subprocess @@ -71,5 +73,22 @@ def test_cli_extend_exclude(dir_with_venv_installed): assert "The project contains obsolete dependencies:\n\n\tisort\n\trequests\n\ttoml\n\n" in result.stderr +def test_cli_with_json_output(dir_with_venv_installed): + with run_within_dir(str(dir_with_venv_installed)): + + # assert that there is no json output + subprocess.run(shlex.split("poetry run deptry ."), capture_output=True, text=True) + assert len(list(pathlib.Path(".").glob("*.json"))) == 0 + + # assert that there is json output + subprocess.run(shlex.split("poetry run deptry . -o deptry.json"), capture_output=True, text=True) + with open("deptry.json", "r") as f: + data = json.load(f) + assert set(data["obsolete"]) == set(["isort", "requests"]) + assert set(data["missing"]) == set(["white"]) + assert set(data["misplaced_dev"]) == set(["black"]) + assert set(data["transitive"]) == set([]) + + def test_cli_help(): subprocess.check_call(shlex.split("deptry --help")) == 0 diff --git a/tests/test_json_writer.py b/tests/test_json_writer.py new file mode 100644 index 00000000..6f759f5c --- /dev/null +++ b/tests/test_json_writer.py @@ -0,0 +1,16 @@ +import json + +from deptry.json_writer import JsonWriter +from deptry.utils import run_within_dir + + +def test_simple(tmp_path): + + with run_within_dir(tmp_path): + JsonWriter(json_output="output.json").write(issues={"one": "two", "three": "four"}) + + with open("output.json", "r") as f: + data = json.load(f) + + assert data["one"] == "two" + assert data["three"] == "four"