Skip to content

Commit

Permalink
Added comma-separated CLI arguments, added unit tests for configurati…
Browse files Browse the repository at this point in the history
…on (#79)
  • Loading branch information
Florian Maas authored Sep 10, 2022
1 parent 352c5a6 commit f3b6ef0
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 177 deletions.
70 changes: 41 additions & 29 deletions deptry/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import click

from deptry.cli_defaults import DEFAULTS
from deptry.config import Config
from deptry.core import Core
from deptry.utils import import_importlib_metadata, run_within_dir
Expand Down Expand Up @@ -41,47 +42,58 @@
@click.option(
"--ignore-obsolete",
"-io",
multiple=True,
type=click.STRING,
help="""
Dependencies listed in pyproject.toml that should never be marked as obsolete, even if they are not imported in any of the files scanned.
Can be used multiple times. For example; `deptry . -io foo -io bar`.
Comma-separated list of dependencies listed in pyproject.toml that should never be marked as obsolete, even if they are not imported in any of the files scanned.
For example; `deptry . --ignore-obsolete foo,bar`.
""",
default=DEFAULTS["ignore_obsolete"],
)
@click.option(
"--ignore-missing",
"-im",
multiple=True,
help="""Modules that should never be marked as missing dependencies, even if the matching package for the import statement cannot be found.
Can be used multiple times. For example; `deptry . -io foo -io bar`.""",
type=click.STRING,
help="""Comma-separated list of modules that should never be marked as missing dependencies, even if the matching package for the import statement cannot be found.
For example; `deptry . --ignore-missing foo,bar`.
""",
default=DEFAULTS["ignore_missing"],
)
@click.option(
"--ignore-transitive",
"-it",
multiple=True,
help="""Dependencies that should never be marked as an issue due to it being a transitive dependency, even though deptry determines them to be transitive.
Can be used multiple times. For example; `deptry . -it foo -io bar`.""",
type=click.STRING,
help="""Comma-separated list of dependencies that should never be marked as an issue due to it being a transitive dependency, even though deptry determines them to be transitive.
For example; `deptry . --ignore-transitive foo,bar`.
""",
default=DEFAULTS["ignore_transitive"],
)
@click.option(
"--ignore-misplaced-dev",
"-id",
multiple=True,
help="""Modules that should never be marked as a misplaced development dependency, even though it seems to not be used solely for development purposes.
Can be used multiple times. For example; `deptry . -id foo -id bar`.""",
type=click.STRING,
help="""Comma-separated list of modules that should never be marked as a misplaced development dependency, even though it seems to not be used solely for development purposes.
For example; `deptry . --ignore-misplaced-dev foo,bar`.
""",
default=DEFAULTS["ignore_misplaced_dev"],
)
@click.option(
"--exclude",
"-e",
multiple=True,
help="""Directories or files in which .py files should not be scanned for imports to determine if there are dependency issues.
Defaults to ['venv','tests']. Specify multiple directories or files by using this flag multiple times, e.g. `-e .venv -e tests
-e other_dir`.""",
type=click.STRING,
help="""Comma-separated list of directories or files in which .py files should not be scanned for imports to determine if there are dependency issues.
For example: `deptry . --exclude .venv,tests,foo,bar.py`
""",
default=DEFAULTS["exclude"],
show_default=True,
)
@click.option(
"--extend-exclude",
"-ee",
multiple=True,
type=click.STRING,
help="""Like --exclude, but adds additional files and directories on top of the excluded ones instead of overwriting the defaults.
(Useful if you simply want to add to the default) `deptry . -ee examples -ee not_this_file.py`""",
(Useful if you simply want to add to the default) `deptry . --extend-exclude foo,bar.py`""",
default=DEFAULTS["extend_exclude"],
show_default=True,
)
@click.option(
"--ignore-notebooks",
Expand Down Expand Up @@ -128,17 +140,17 @@ def deptry(
# This way, we can distinguish if a argument was actually passed by the user
# (e.g. ignore_notebooks is 'False' by default).
config = Config(
ignore_obsolete=list(ignore_obsolete) if ignore_obsolete else None,
ignore_missing=list(ignore_missing) if ignore_missing else None,
ignore_transitive=list(ignore_transitive) if ignore_transitive else None,
ignore_misplaced_dev=list(ignore_misplaced_dev) if ignore_misplaced_dev else None,
exclude=list(exclude) if exclude else None,
extend_exclude=list(extend_exclude) if extend_exclude else None,
ignore_notebooks=ignore_notebooks if ignore_notebooks else None,
skip_obsolete=skip_obsolete if skip_obsolete else None,
skip_missing=skip_missing if skip_missing else None,
skip_transitive=skip_transitive if skip_transitive else None,
skip_misplaced_dev=skip_misplaced_dev if skip_misplaced_dev else None,
ignore_obsolete=ignore_obsolete,
ignore_missing=ignore_missing,
ignore_transitive=ignore_transitive,
ignore_misplaced_dev=ignore_misplaced_dev,
exclude=exclude,
extend_exclude=extend_exclude,
ignore_notebooks=ignore_notebooks,
skip_obsolete=skip_obsolete,
skip_missing=skip_missing,
skip_transitive=skip_transitive,
skip_misplaced_dev=skip_misplaced_dev,
)

result = Core(
Expand Down
13 changes: 13 additions & 0 deletions deptry/cli_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
DEFAULTS = {
"ignore_obsolete": "",
"ignore_missing": "",
"ignore_transitive": "",
"ignore_misplaced_dev": "",
"exclude": ".venv,tests",
"extend_exclude": "",
"ignore_notebooks": False,
"skip_obsolete": False,
"skip_missing": False,
"skip_transitive": False,
"skip_misplaced_dev": False,
}
208 changes: 73 additions & 135 deletions deptry/config.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,9 @@
import logging
from typing import Any, Dict, List, Optional
from typing import Any, Dict

from deptry.cli_defaults import DEFAULTS
from deptry.utils import load_pyproject_toml

DEFAULTS = {
"ignore_obsolete": [],
"ignore_missing": [],
"ignore_transitive": [],
"ignore_misplaced_dev": [],
"exclude": [".venv", "tests"],
"extend_exclude": [],
"ignore_notebooks": False,
"skip_obsolete": False,
"skip_missing": False,
"skip_transitive": False,
"skip_misplaced_dev": False,
}


class Config:
"""
Expand All @@ -27,137 +14,81 @@ class Config:

def __init__(
self,
ignore_obsolete: Optional[List[str]],
ignore_missing: Optional[List[str]],
ignore_transitive: Optional[List[str]],
ignore_misplaced_dev: Optional[List[str]],
skip_obsolete: Optional[bool],
skip_missing: Optional[bool],
skip_transitive: Optional[bool],
skip_misplaced_dev: Optional[bool],
exclude: Optional[List[str]],
extend_exclude: Optional[List[str]],
ignore_notebooks: Optional[bool],
ignore_obsolete: str = None,
ignore_missing: str = None,
ignore_transitive: str = None,
ignore_misplaced_dev: str = None,
skip_obsolete: bool = None,
skip_missing: bool = None,
skip_transitive: bool = None,
skip_misplaced_dev: bool = None,
exclude: str = None,
extend_exclude: str = None,
ignore_notebooks: bool = None,
) -> None:
self._set_defaults()
self._override_config_with_pyproject_toml()
self._override_config_with_cli_arguments(
ignore_obsolete=ignore_obsolete,
ignore_missing=ignore_missing,
ignore_transitive=ignore_transitive,
ignore_misplaced_dev=ignore_misplaced_dev,
exclude=exclude,
extend_exclude=extend_exclude,
ignore_notebooks=ignore_notebooks,
skip_obsolete=skip_obsolete,
skip_missing=skip_missing,
skip_transitive=skip_transitive,
skip_misplaced_dev=skip_misplaced_dev,
)

def _set_defaults(self) -> None:
self.ignore_obsolete = DEFAULTS["ignore_obsolete"]
self.ignore_missing = DEFAULTS["ignore_missing"]
self.ignore_transitive = DEFAULTS["ignore_transitive"]
self.ignore_misplaced_dev = DEFAULTS["ignore_misplaced_dev"]
self.exclude = DEFAULTS["exclude"]
self.extend_exclude = DEFAULTS["extend_exclude"]
self.ignore_notebooks = DEFAULTS["ignore_notebooks"]
self.skip_obsolete = DEFAULTS["skip_obsolete"]
self.skip_missing = DEFAULTS["skip_missing"]
self.skip_transitive = DEFAULTS["skip_transitive"]
self.skip_misplaced_dev = DEFAULTS["skip_misplaced_dev"]

def _override_config_with_pyproject_toml(self) -> None:
pyproject_toml_config = self._read_configuration_from_pyproject_toml()
if pyproject_toml_config:
self._override_with_toml_argument("ignore_obsolete", pyproject_toml_config)
self._override_with_toml_argument("ignore_missing", pyproject_toml_config)
self._override_with_toml_argument("ignore_transitive", pyproject_toml_config)
self._override_with_toml_argument("ignore_misplaced_dev", pyproject_toml_config)
self._override_with_toml_argument("skip_missing", pyproject_toml_config)
self._override_with_toml_argument("skip_obsolete", pyproject_toml_config)
self._override_with_toml_argument("skip_transitive", pyproject_toml_config)
self._override_with_toml_argument("skip_misplaced_dev", pyproject_toml_config)
self._override_with_toml_argument("exclude", pyproject_toml_config)
self._override_with_toml_argument("extend_exclude", pyproject_toml_config)
self._override_with_toml_argument("ignore_notebooks", pyproject_toml_config)

def _read_configuration_from_pyproject_toml(self) -> Optional[Dict]:
pyproject_data = load_pyproject_toml()
try:
return pyproject_data["tool"]["deptry"]
except KeyError: # noqa
logging.debug("No configuration for deptry was found in pyproject.toml.")
return None

def _override_with_toml_argument(self, argument: str, pyproject_toml_config: Dict) -> None:
self.pyproject_data = self._read_configuration_from_pyproject_toml()
self._set_string_to_list_config("ignore_obsolete", ignore_obsolete)
self._set_string_to_list_config("ignore_missing", ignore_missing)
self._set_string_to_list_config("ignore_transitive", ignore_transitive)
self._set_string_to_list_config("ignore_misplaced_dev", ignore_misplaced_dev)
self._set_string_to_list_config("exclude", exclude)
self._set_string_to_list_config("extend_exclude", extend_exclude)
self._set_bool_config("skip_obsolete", skip_obsolete)
self._set_bool_config("skip_missing", skip_missing)
self._set_bool_config("skip_transitive", skip_transitive)
self._set_bool_config("skip_misplaced_dev", skip_misplaced_dev)
self._set_bool_config("ignore_notebooks", ignore_notebooks)

def _set_string_to_list_config(self, attribute: str, cli_value: str):
"""
If argument is found in pyproject.toml, override the default argument with the found value.
Set configuration for arguments that are supplied as strings in the CLI, but should be converted to a list.
"""
if argument in pyproject_toml_config:
value = pyproject_toml_config[argument]
setattr(self, argument, value)
self._log_changed_by_pyproject_toml(argument, value)

def _override_config_with_cli_arguments( # noqa
self,
ignore_obsolete: Optional[List[str]],
ignore_missing: Optional[List[str]],
ignore_transitive: Optional[List[str]],
ignore_misplaced_dev: Optional[List[str]],
exclude: Optional[List[str]],
extend_exclude: Optional[List[str]],
ignore_notebooks: Optional[bool],
skip_obsolete: Optional[bool],
skip_missing: Optional[bool],
skip_transitive: Optional[bool],
skip_misplaced_dev: Optional[bool],
) -> None:

if ignore_obsolete:
self.ignore_obsolete = ignore_obsolete
self._log_changed_by_command_line_argument("ignore_obsolete", ignore_obsolete)

if ignore_missing:
self.ignore_missing = ignore_missing
self._log_changed_by_command_line_argument("ignore_missing", ignore_missing)

if ignore_transitive:
self.ignore_transitive = ignore_transitive
self._log_changed_by_command_line_argument("ignore_transitive", ignore_transitive)
self._set_default_string_to_list(attribute)
self._override_with_toml_argument(attribute)
self._override_with_cli_argument_string_to_list(attribute, cli_value)

if ignore_misplaced_dev:
self.ignore_misplaced_dev = ignore_misplaced_dev
self._log_changed_by_command_line_argument("ignore_misplaced_dev", ignore_misplaced_dev)

if skip_obsolete:
self.skip_obsolete = skip_obsolete
self._log_changed_by_command_line_argument("skip_obsolete", skip_obsolete)
def _set_bool_config(self, attribute: str, cli_value: str):
"""
Set configuration for boolean arguments.
"""
self._set_default_boolean(attribute)
self._override_with_toml_argument(attribute)
self._override_with_cli_argument_boolean(attribute, cli_value)

if skip_missing:
self.skip_missing = skip_missing
self._log_changed_by_command_line_argument("skip_missing", skip_missing)
def _set_default_string_to_list(self, attribute: str):
setattr(self, attribute, self._comma_separated_string_to_list(DEFAULTS[attribute]))

if skip_transitive:
self.skip_transitive = skip_transitive
self._log_changed_by_command_line_argument("skip_transitive", skip_transitive)
def _set_default_boolean(self, attribute: str):
setattr(self, attribute, DEFAULTS[attribute])

if skip_misplaced_dev:
self.skip_misplaced_dev = skip_misplaced_dev
self._log_changed_by_command_line_argument("skip_misplaced_dev", skip_misplaced_dev)
def _override_with_cli_argument_string_to_list(self, attribute, value):
if value and not value == DEFAULTS[attribute]:
value_as_list = self._comma_separated_string_to_list(value)
self._log_changed_by_command_line_argument(attribute, value_as_list)
setattr(self, attribute, value_as_list)

if exclude:
self.exclude = exclude
self._log_changed_by_command_line_argument("exclude", exclude)
def _override_with_cli_argument_boolean(self, attribute, value):
if value and not value == DEFAULTS[attribute]:
self._log_changed_by_command_line_argument(attribute, value)
setattr(self, attribute, value)

if extend_exclude:
self.extend_exclude = extend_exclude
self._log_changed_by_command_line_argument("extend_exclude", extend_exclude)
def _override_with_toml_argument(self, argument: str) -> None:
"""
If argument is found in pyproject.toml, override the default argument with the found value.
"""
if self.pyproject_data and argument in self.pyproject_data:
value = self.pyproject_data[argument]
setattr(self, argument, value)
self._log_changed_by_pyproject_toml(argument, value)

if ignore_notebooks:
self.ignore_notebooks = ignore_notebooks
self._log_changed_by_command_line_argument("ignore_notebooks", ignore_notebooks)
def _read_configuration_from_pyproject_toml(self) -> Dict:
pyproject_data = load_pyproject_toml()
try:
return pyproject_data["tool"]["deptry"]
except KeyError: # noqa
logging.debug("No configuration for deptry was found in pyproject.toml.")
return None

@staticmethod
def _log_changed_by_pyproject_toml(argument: str, value: Any) -> None:
Expand All @@ -166,3 +97,10 @@ def _log_changed_by_pyproject_toml(argument: str, value: Any) -> None:
@staticmethod
def _log_changed_by_command_line_argument(argument: str, value: Any) -> None:
logging.debug(f"Argument {argument} set to {str(value)} by command line argument")

@staticmethod
def _comma_separated_string_to_list(string: str):
if len(string) > 0:
return string.split(",")
else:
return []
4 changes: 3 additions & 1 deletion docs/docs/pyproject-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ _deptry_ can be configured by adding a `[tool.deptry]` section to _pyproject.tom
- `skip_transitive`: `bool`
- `skip_misplaced_dev`: `bool`

An example of such a section is given below.
Note that the command line arguments that should be passed as a string with comma-separated values should simply be passed as a list in _pyproject.toml_.

An example of a configuration section for _deptry_ is given below.

```
[tool.deptry]
Expand Down
Loading

0 comments on commit f3b6ef0

Please sign in to comment.