Skip to content

Commit 2b70f5d

Browse files
author
Florian Maas
authored
Add check for misplaced development dependencies (#51)
* Added a check for development dependencies * Added fixture to speed up the CLI unit tests * changed --ignore-directories to --exclude * Updated the documentation
1 parent 6d16d0e commit 2b70f5d

33 files changed

+405
-287
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88

99
---
1010

11-
_deptry_ is a command line tool to check for issues with dependencies in a poetry managed Python project. It checks for three types of issues:
11+
_deptry_ is a command line tool to check for issues with dependencies in a poetry managed Python project. It checks for four types of issues:
1212

1313
- Obsolete dependencies: Dependencies which are added to your project's dependencies, but which are not used within the codebase.
14-
- Transitive dependencies: Packages from which code is imported, but the package (A) itself is not in your projects dependencies. Instead, another package (B) is in your list of dependencies, which depends on (A). Package (A) should be added to your project's list of dependencies.
1514
- Missing dependencies: Modules that are imported within your project, but no corresponding package is found in the environment.
15+
- Transitive dependencies: Packages from which code is imported, but the package (A) itself is not in your projects dependencies. Instead, another package (B) is in your list of dependencies, which depends on (A). Package (A) should be added to your project's list of dependencies.
16+
- Misplaced dependencies: Development dependencies that should be included as regular dependencies.
1617

1718
_deptry_ detects these issue by scanning the imported modules within all Python files in
1819
a directory and it's subdirectories, and comparing those to the dependencies listed in _pyproject.toml_.

deptry/cli.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
is_flag=True,
3434
help="Boolean flag to specify if deptry should skip scanning the project for transitive dependencies.",
3535
)
36+
@click.option(
37+
"--skip-misplaced-dev",
38+
is_flag=True,
39+
help="Boolean flag to specify if deptry should skip scanning the project for development dependencies that should be regular dependencies.",
40+
)
3641
@click.option(
3742
"--ignore-obsolete",
3843
"-io",
@@ -46,22 +51,29 @@
4651
"--ignore-missing",
4752
"-im",
4853
multiple=True,
49-
help="""Modules that should never be marked as having missing dependencies, even if the matching package for the import statement cannot be found.
54+
help="""Modules that should never be marked as missing dependencies, even if the matching package for the import statement cannot be found.
5055
Can be used multiple times. For example; `deptry . -io foo -io bar`.""",
5156
)
5257
@click.option(
5358
"--ignore-transitive",
5459
"-it",
5560
multiple=True,
56-
help="""Modules that should never be marked as 'missing due to transitive' even though deptry determines them to be transitive.
61+
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.
5762
Can be used multiple times. For example; `deptry . -it foo -io bar`.""",
5863
)
5964
@click.option(
60-
"--ignore-directories",
65+
"--ignore-misplaced-dev",
6166
"-id",
6267
multiple=True,
63-
help="""Directories in which .py files should not be scanned for imports to determine if dependencies are obsolete, missing or transitive.
64-
Defaults to ['venv','tests']. Specify multiple directories by using this flag twice, e.g. `-id .venv -id tests -id other_dir`.""",
68+
help="""Modules that should never be marked as a misplaced development dependency, even though it seems to not be used solely for development purposes.
69+
Can be used multiple times. For example; `deptry . -id foo -id bar`.""",
70+
)
71+
@click.option(
72+
"--exclude",
73+
multiple=True,
74+
help="""Directories or files in which .py files should not be scanned for imports to determine if there are dependency issues.
75+
Defaults to ['venv','tests']. Specify multiple directories by using this flag multiple times, e.g. `--exclude .venv --exclude tests
76+
--exclude other_dir`.""",
6577
)
6678
@click.option(
6779
"--ignore-notebooks",
@@ -80,10 +92,12 @@ def deptry(
8092
ignore_obsolete: List[str],
8193
ignore_missing: List[str],
8294
ignore_transitive: List[str],
95+
ignore_misplaced_dev: List[str],
8396
skip_obsolete: bool,
8497
skip_missing: bool,
8598
skip_transitive: bool,
86-
ignore_directories: List[str],
99+
skip_misplaced_dev: bool,
100+
exclude: List[str],
87101
ignore_notebooks: bool,
88102
version: bool,
89103
) -> None:
@@ -108,22 +122,26 @@ def deptry(
108122
ignore_obsolete=ignore_obsolete if ignore_obsolete else None,
109123
ignore_missing=ignore_missing if ignore_missing else None,
110124
ignore_transitive=ignore_transitive if ignore_transitive else None,
111-
ignore_directories=ignore_directories if ignore_directories else None,
125+
ignore_misplaced_dev=ignore_misplaced_dev if ignore_misplaced_dev else None,
126+
exclude=exclude if exclude else None,
112127
ignore_notebooks=ignore_notebooks if ignore_notebooks else None,
113128
skip_obsolete=skip_obsolete if skip_obsolete else None,
114129
skip_missing=skip_missing if skip_missing else None,
115130
skip_transitive=skip_transitive if skip_transitive else None,
131+
skip_misplaced_dev=skip_misplaced_dev if skip_misplaced_dev else None,
116132
)
117133

118134
result = Core(
119135
ignore_obsolete=config.ignore_obsolete,
120136
ignore_missing=config.ignore_missing,
121137
ignore_transitive=config.ignore_transitive,
122-
ignore_directories=config.ignore_directories,
138+
ignore_misplaced_dev=config.ignore_misplaced_dev,
139+
exclude=config.exclude,
123140
ignore_notebooks=config.ignore_notebooks,
124141
skip_obsolete=config.skip_obsolete,
125142
skip_missing=config.skip_missing,
126143
skip_transitive=config.skip_transitive,
144+
skip_misplaced_dev=config.skip_misplaced_dev,
127145
).run()
128146
issue_found = False
129147
if not skip_obsolete and "obsolete" in result and result["obsolete"]:
@@ -135,6 +153,9 @@ def deptry(
135153
if not skip_transitive and "transitive" in result and result["transitive"]:
136154
log_transitive_dependencies(result["transitive"])
137155
issue_found = True
156+
if not skip_misplaced_dev and "misplaced_dev" in result and result["misplaced_dev"]:
157+
log_misplaced_develop_dependencies(result["misplaced_dev"])
158+
issue_found = True
138159

139160
if issue_found:
140161
log_additional_info()
@@ -165,17 +186,24 @@ def log_transitive_dependencies(dependencies: List[str], sep="\n\t") -> None:
165186
logging.info(
166187
f"There are transitive dependencies that should be explicitly defined as dependencies in pyproject.toml:\n{sep}{sep.join(dependencies)}\n"
167188
)
189+
logging.info("""They are currently imported but not specified directly as your project's dependencies.""")
190+
191+
192+
def log_misplaced_develop_dependencies(dependencies: List[str], sep="\n\t") -> None:
193+
logging.info("\n-----------------------------------------------------\n")
194+
logging.info(f"There are imported modules from development dependencies detected:\n{sep}{sep.join(dependencies)}\n")
168195
logging.info(
169-
"""They are currently imported but not specified directly as your project's dependencies. This issue also be caused
170-
by a development dependency that is found to be used within the scanned Python files."""
196+
"""Consider moving them to `[tool.poetry.dependencies]` in pyproject.toml. If this is not correct and the
197+
dependencies listed above are indeed development dependencies, it's likely that files were scanned that are only used
198+
for development purposes. Run `deptry -v .` to see a list of scanned files."""
171199
)
172200

173201

174202
def log_additional_info():
175203
logging.info("\n-----------------------------------------------------\n")
176204
logging.info(
177205
"""Dependencies and directories can be ignored by passing additional command-line arguments. See `deptry --help` for more details.
178-
Alternatively, deptry can be configured through `pyproject.toml`:
206+
Alternatively, deptry can be configured through `pyproject.toml`. An example:
179207
180208
```
181209
[tool.deptry]
@@ -188,7 +216,7 @@ def log_additional_info():
188216
ignore_transitive = [
189217
'your-dependency'
190218
]
191-
ignore_directories = [
219+
exclude = [
192220
'.venv', 'tests', 'docs'
193221
]
194222
```

deptry/config.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
"ignore_obsolete": [],
99
"ignore_missing": [],
1010
"ignore_transitive": [],
11-
"ignore_directories": [".venv", "tests"],
11+
"ignore_misplaced_dev": [],
12+
"exclude": [".venv", "tests"],
1213
"ignore_notebooks": False,
1314
"skip_obsolete": False,
1415
"skip_missing": False,
1516
"skip_transitive": False,
17+
"skip_misplaced_dev": False,
1618
}
1719

1820

@@ -28,45 +30,53 @@ def __init__(
2830
ignore_obsolete: Optional[List[str]],
2931
ignore_missing: Optional[List[str]],
3032
ignore_transitive: Optional[List[str]],
33+
ignore_misplaced_dev: Optional[List[str]],
3134
skip_obsolete: Optional[bool],
3235
skip_missing: Optional[bool],
3336
skip_transitive: Optional[bool],
34-
ignore_directories: Optional[List[str]],
37+
skip_misplaced_dev: Optional[bool],
38+
exclude: Optional[List[str]],
3539
ignore_notebooks: Optional[bool],
3640
) -> None:
3741
self._set_defaults()
3842
self._override_config_with_pyproject_toml()
3943
self._override_config_with_cli_arguments(
40-
ignore_obsolete,
41-
ignore_missing,
42-
ignore_transitive,
43-
ignore_directories,
44-
ignore_notebooks,
45-
skip_obsolete,
46-
skip_missing,
47-
skip_transitive,
44+
ignore_obsolete=ignore_obsolete,
45+
ignore_missing=ignore_missing,
46+
ignore_transitive=ignore_transitive,
47+
ignore_misplaced_dev=ignore_misplaced_dev,
48+
exclude=exclude,
49+
ignore_notebooks=ignore_notebooks,
50+
skip_obsolete=skip_obsolete,
51+
skip_missing=skip_missing,
52+
skip_transitive=skip_transitive,
53+
skip_misplaced_dev=skip_misplaced_dev,
4854
)
4955

5056
def _set_defaults(self) -> None:
5157
self.ignore_obsolete = DEFAULTS["ignore_obsolete"]
5258
self.ignore_missing = DEFAULTS["ignore_missing"]
5359
self.ignore_transitive = DEFAULTS["ignore_transitive"]
54-
self.ignore_directories = DEFAULTS["ignore_directories"]
60+
self.ignore_misplaced_dev = DEFAULTS["ignore_misplaced_dev"]
61+
self.exclude = DEFAULTS["exclude"]
5562
self.ignore_notebooks = DEFAULTS["ignore_notebooks"]
5663
self.skip_obsolete = DEFAULTS["skip_obsolete"]
5764
self.skip_missing = DEFAULTS["skip_missing"]
5865
self.skip_transitive = DEFAULTS["skip_transitive"]
66+
self.skip_misplaced_dev = DEFAULTS["skip_misplaced_dev"]
5967

6068
def _override_config_with_pyproject_toml(self) -> None:
6169
pyproject_toml_config = self._read_configuration_from_pyproject_toml()
6270
if pyproject_toml_config:
6371
self._override_with_toml_argument("ignore_obsolete", List[str], pyproject_toml_config)
6472
self._override_with_toml_argument("ignore_missing", List[str], pyproject_toml_config)
6573
self._override_with_toml_argument("ignore_transitive", List[str], pyproject_toml_config)
74+
self._override_with_toml_argument("ignore_misplaced_dev", List[str], pyproject_toml_config)
6675
self._override_with_toml_argument("skip_missing", List[str], pyproject_toml_config)
6776
self._override_with_toml_argument("skip_obsolete", List[str], pyproject_toml_config)
6877
self._override_with_toml_argument("skip_transitive", List[str], pyproject_toml_config)
69-
self._override_with_toml_argument("ignore_directories", List[str], pyproject_toml_config)
78+
self._override_with_toml_argument("skip_misplaced_dev", List[str], pyproject_toml_config)
79+
self._override_with_toml_argument("exclude", List[str], pyproject_toml_config)
7080
self._override_with_toml_argument("ignore_notebooks", List[str], pyproject_toml_config)
7181

7282
def _read_configuration_from_pyproject_toml(self) -> Optional[Dict]:
@@ -87,16 +97,18 @@ def _override_with_toml_argument(self, argument: str, expected_type: Any, pyproj
8797
setattr(self, argument, value)
8898
self._log_changed_by_pyproject_toml(argument, value)
8999

90-
def _override_config_with_cli_arguments(
100+
def _override_config_with_cli_arguments( # noqa
91101
self,
92102
ignore_obsolete: Optional[List[str]],
93103
ignore_missing: Optional[List[str]],
94104
ignore_transitive: Optional[List[str]],
95-
ignore_directories: Optional[List[str]],
105+
ignore_misplaced_dev: Optional[List[str]],
106+
exclude: Optional[List[str]],
96107
ignore_notebooks: Optional[bool],
97108
skip_obsolete: Optional[bool],
98109
skip_missing: Optional[bool],
99110
skip_transitive: Optional[bool],
111+
skip_misplaced_dev: Optional[bool],
100112
) -> None:
101113

102114
if ignore_obsolete:
@@ -111,6 +123,10 @@ def _override_config_with_cli_arguments(
111123
self.ignore_transitive = ignore_transitive
112124
self._log_changed_by_command_line_argument("ignore_transitive", ignore_transitive)
113125

126+
if ignore_misplaced_dev:
127+
self.ignore_misplaced_dev = ignore_misplaced_dev
128+
self._log_changed_by_command_line_argument("ignore_misplaced_dev", ignore_misplaced_dev)
129+
114130
if skip_obsolete:
115131
self.skip_obsolete = skip_obsolete
116132
self._log_changed_by_command_line_argument("skip_obsolete", skip_obsolete)
@@ -123,9 +139,13 @@ def _override_config_with_cli_arguments(
123139
self.skip_transitive = skip_transitive
124140
self._log_changed_by_command_line_argument("skip_transitive", skip_transitive)
125141

126-
if ignore_directories:
127-
self.ignore_directories = ignore_directories
128-
self._log_changed_by_command_line_argument("ignore_directories", ignore_directories)
142+
if skip_misplaced_dev:
143+
self.skip_misplaced_dev = skip_misplaced_dev
144+
self._log_changed_by_command_line_argument("skip_misplaced_dev", skip_misplaced_dev)
145+
146+
if exclude:
147+
self.exclude = exclude
148+
self._log_changed_by_command_line_argument("exclude", exclude)
129149

130150
if ignore_notebooks:
131151
self.ignore_notebooks = ignore_notebooks

deptry/core.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44

55
from deptry.dependency_getter import DependencyGetter
66
from deptry.import_parser import ImportParser
7+
from deptry.issue_finders.misplaced_dev import MisplacedDevDependenciesFinder
78
from deptry.issue_finders.missing import MissingDependenciesFinder
89
from deptry.issue_finders.obsolete import ObsoleteDependenciesFinder
910
from deptry.issue_finders.transitive import TransitiveDependenciesFinder
10-
from deptry.module import Module
11+
from deptry.module import ModuleBuilder
1112
from deptry.python_file_finder import PythonFileFinder
1213

1314

@@ -17,53 +18,67 @@ def __init__(
1718
ignore_obsolete: List[str],
1819
ignore_missing: List[str],
1920
ignore_transitive: List[str],
21+
ignore_misplaced_dev: List[str],
2022
skip_obsolete: bool,
2123
skip_missing: bool,
2224
skip_transitive: bool,
23-
ignore_directories: List[str],
25+
skip_misplaced_dev: bool,
26+
exclude: List[str],
2427
ignore_notebooks: bool,
2528
) -> None:
2629
self.ignore_obsolete = ignore_obsolete
2730
self.ignore_missing = ignore_missing
2831
self.ignore_transitive = ignore_transitive
29-
self.ignore_directories = ignore_directories
32+
self.ignore_misplaced_dev = ignore_misplaced_dev
33+
self.exclude = exclude
3034
self.ignore_notebooks = ignore_notebooks
3135
self.skip_obsolete = skip_obsolete
3236
self.skip_missing = skip_missing
3337
self.skip_transitive = skip_transitive
38+
self.skip_misplaced_dev = skip_misplaced_dev
3439
logging.debug("Running with the following configuration:")
3540
logging.debug(f"ignore_obsolete: {ignore_obsolete}")
3641
logging.debug(f"ignore_missing: {ignore_missing}")
3742
logging.debug(f"ignore_transitive: {ignore_transitive}")
43+
logging.debug(f"ignore_misplaced_dev: {ignore_misplaced_dev}")
3844
logging.debug(f"skip_obsolete: {skip_obsolete}")
3945
logging.debug(f"skip_missing: {skip_missing}")
4046
logging.debug(f"skip_transitive: {skip_transitive}")
41-
logging.debug(f"ignore_directories: {ignore_directories}")
47+
logging.debug(f"skip_misplaced_dev {skip_misplaced_dev}")
48+
logging.debug(f"exclude: {exclude}")
4249
logging.debug(f"ignore_notebooks: {ignore_notebooks}\n")
4350

4451
def run(self) -> Dict:
4552

4653
dependencies = DependencyGetter().get()
54+
dev_dependencies = DependencyGetter(dev=True).get()
55+
4756
all_python_files = PythonFileFinder(
48-
ignore_directories=self.ignore_directories, ignore_notebooks=self.ignore_notebooks
57+
exclude=self.exclude, ignore_notebooks=self.ignore_notebooks
4958
).get_all_python_files_in(Path("."))
5059

5160
imported_modules = ImportParser().get_imported_modules_for_list_of_files(all_python_files)
52-
imported_modules = [Module(mod, dependencies) for mod in imported_modules]
53-
imported_modules = [mod for mod in imported_modules if not mod.is_standard_library()]
61+
imported_modules = [ModuleBuilder(mod, dependencies, dev_dependencies).build() for mod in imported_modules]
62+
imported_modules = [mod for mod in imported_modules if not mod.standard_library]
5463

5564
result = {}
5665
if not self.skip_obsolete:
5766
result["obsolete"] = ObsoleteDependenciesFinder(
58-
imported_modules=imported_modules, dependencies=dependencies, list_to_ignore=self.ignore_obsolete
67+
imported_modules=imported_modules, dependencies=dependencies, ignore_obsolete=self.ignore_obsolete
5968
).find()
6069
if not self.skip_missing:
6170
result["missing"] = MissingDependenciesFinder(
62-
imported_modules=imported_modules, dependencies=dependencies, list_to_ignore=self.ignore_missing
71+
imported_modules=imported_modules, dependencies=dependencies, ignore_missing=self.ignore_missing
6372
).find()
6473
if not self.skip_transitive:
6574
result["transitive"] = TransitiveDependenciesFinder(
66-
imported_modules=imported_modules, dependencies=dependencies, list_to_ignore=self.ignore_transitive
75+
imported_modules=imported_modules, dependencies=dependencies, ignore_transitive=self.ignore_transitive
76+
).find()
77+
if not self.skip_misplaced_dev:
78+
result["misplaced_dev"] = MisplacedDevDependenciesFinder(
79+
imported_modules=imported_modules,
80+
dependencies=dependencies,
81+
ignore_misplaced_dev=self.ignore_misplaced_dev,
6782
).find()
6883

6984
return result

0 commit comments

Comments
 (0)