From 61410a2abb242cddacdeda8e294666f0e27932ec Mon Sep 17 00:00:00 2001 From: Mathieu Kniewallner Date: Thu, 7 Mar 2024 00:35:43 +0100 Subject: [PATCH] feat(config): detect invalid `pyproject.toml` options --- deptry/config.py | 14 +++++++++++- deptry/exceptions.py | 9 ++++++++ tests/unit/test_config.py | 46 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/deptry/config.py b/deptry/config.py index 021d9f0f..66a389bb 100644 --- a/deptry/config.py +++ b/deptry/config.py @@ -3,6 +3,7 @@ import logging from typing import TYPE_CHECKING, Any +from deptry.exceptions import InvalidPyprojectTOMLOptionsError from deptry.utils import load_pyproject_toml if TYPE_CHECKING: @@ -11,6 +12,13 @@ import click +def _get_invalid_pyproject_toml_keys(ctx: click.Context, deptry_toml_config_keys: set[str]) -> list[str]: + """Returns the list of options set in `pyproject.toml` that do not exist as CLI parameters.""" + existing_cli_params = {param.name for param in ctx.command.params} + + return sorted(deptry_toml_config_keys.difference(existing_cli_params)) + + 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 @@ -28,11 +36,15 @@ def read_configuration_from_pyproject_toml(ctx: click.Context, _param: click.Par return value try: - deptry_toml_config = pyproject_data["tool"]["deptry"] + deptry_toml_config: dict[str, Any] = pyproject_data["tool"]["deptry"] except KeyError: logging.debug("No configuration for deptry was found in pyproject.toml.") return value + invalid_pyproject_toml_keys = _get_invalid_pyproject_toml_keys(ctx, set(deptry_toml_config)) + if invalid_pyproject_toml_keys: + raise InvalidPyprojectTOMLOptionsError(invalid_pyproject_toml_keys) + click_default_map: dict[str, Any] = {} if ctx.default_map: diff --git a/deptry/exceptions.py b/deptry/exceptions.py index 7685d054..78e545ed 100644 --- a/deptry/exceptions.py +++ b/deptry/exceptions.py @@ -2,6 +2,8 @@ from typing import TYPE_CHECKING +from click import UsageError + if TYPE_CHECKING: from pathlib import Path @@ -29,3 +31,10 @@ def __init__(self, version: tuple[int, int]) -> None: super().__init__( f"Python version {version[0]}.{version[1]} is not supported. Only versions >= 3.8 are supported." ) + + +class InvalidPyprojectTOMLOptionsError(UsageError): + def __init__(self, invalid_options: list[str]) -> None: + super().__init__( + f"'[tool.deptry]' section in 'pyproject.toml' contains invalid configuration options: {invalid_options}." + ) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index a22f058d..2e42387d 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -5,17 +5,34 @@ from typing import TYPE_CHECKING import click +import pytest +from click import Argument from deptry.config import read_configuration_from_pyproject_toml +from deptry.exceptions import InvalidPyprojectTOMLOptionsError from tests.utils import run_within_dir if TYPE_CHECKING: from _pytest.logging import LogCaptureFixture +click_command = click.Command( + "", + params=[ + Argument(param_decls=["exclude"]), + Argument(param_decls=["extend_exclude"]), + Argument(param_decls=["per_rule_ignores"]), + Argument(param_decls=["ignore"]), + Argument(param_decls=["ignore_notebooks"]), + Argument(param_decls=["requirements_txt"]), + Argument(param_decls=["requirements_txt_dev"]), + ], +) + + def test_read_configuration_from_pyproject_toml_exists(tmp_path: Path) -> None: click_context = click.Context( - click.Command(""), + click_command, default_map={ "exclude": ["bar"], "extend_exclude": ["foo"], @@ -77,7 +94,7 @@ def test_read_configuration_from_pyproject_toml_file_not_found(caplog: LogCaptur with caplog.at_level(logging.DEBUG): assert ( read_configuration_from_pyproject_toml( - click.Context(click.Command("")), click.UNPROCESSED(None), pyproject_toml_path + click.Context(click_command), click.UNPROCESSED(None), pyproject_toml_path ) == pyproject_toml_path ) @@ -101,7 +118,30 @@ def test_read_configuration_from_pyproject_toml_file_without_deptry_section( with caplog.at_level(logging.DEBUG): assert read_configuration_from_pyproject_toml( - click.Context(click.Command("")), click.UNPROCESSED(None), pyproject_toml_path + click.Context(click_command), click.UNPROCESSED(None), pyproject_toml_path ) == Path("pyproject.toml") assert "No configuration for deptry was found in pyproject.toml." in caplog.text + + +def test_read_configuration_from_pyproject_toml_file_with_invalid_options( + caplog: LogCaptureFixture, tmp_path: Path +) -> None: + pyproject_toml_content = """ + [tool.deptry] + exclude = ["foo", "bar"] + invalid_option = "nope" + another_invalid_option = "still nope" + extend_exclude = ["bar", "foo"] + """ + + with run_within_dir(tmp_path): + pyproject_toml_path = Path("pyproject.toml") + + with pyproject_toml_path.open("w") as f: + f.write(pyproject_toml_content) + + with pytest.raises(InvalidPyprojectTOMLOptionsError): + assert read_configuration_from_pyproject_toml( + click.Context(click_command), click.UNPROCESSED(None), pyproject_toml_path + ) == Path("pyproject.toml")