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

Add DEP005 to detect dependencies that are in the standard library #761

Merged
merged 15 commits into from
Jul 20, 2024
34 changes: 34 additions & 0 deletions docs/rules-violations.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ _deptry_ checks your project against the following rules related to dependencies
| DEP002 | Project should not contain unused dependencies | [link](#unused-dependencies-dep002) |
| DEP003 | Project should not use transitive dependencies | [link](#transitive-dependencies-dep003) |
| DEP004 | Project should not use development dependencies in non-development code | [link](#misplaced-development-dependencies-dep004) |
| DEP005 | Project should not contain dependencies that are in the standard library | [link](#standard-library-dependencies-dep005) |

Any of the checks can be disabled with the [`ignore`](usage.md#ignore) flag. Specific dependencies or modules can be
ignored with the [`per-rule-ignores`](usage.md#per-rule-ignores) flag.
Expand Down Expand Up @@ -170,3 +171,36 @@ dependencies = [
[tool.pdm.dev-dependencies]
test = ["pytest==7.2.0"]
```

## Standard library dependencies (DEP005)

Dependencies that are part of the Python standard library should not be defined as dependencies in your project.

### Example

On a project with the following dependencies:

```toml
[project]
dependencies = [
"asyncio",
]
```

and the following `main.py` in the project:

```python
import asyncio

def async_example():
return asyncio.run(some_coroutine())
```

_deptry_ will report `asyncio` as a standard library dependency because it is part of the standard library, yet it is defined as a dependency in the project.

To fix the issue, `asyncio` should be removed from `[project.dependencies]`:

```toml
[project]
dependencies = []
```
17 changes: 8 additions & 9 deletions python/deptry/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,28 @@

python_files = self._find_python_files()
local_modules = self._get_local_modules()
stdlib_modules = self._get_stdlib_modules()
standard_library_modules = self._get_standard_library_modules()

Check warning on line 62 in python/deptry/core.py

View check run for this annotation

Codecov / codecov/patch

python/deptry/core.py#L62

Added line #L62 was not covered by tests

imported_modules_with_locations = [
ModuleLocations(
ModuleBuilder(
module,
local_modules,
stdlib_modules,
standard_library_modules,
dependencies_extract.dependencies,
dependencies_extract.dev_dependencies,
).build(),
locations,
)
for module, locations in get_imported_modules_from_list_of_files(python_files).items()
]
imported_modules_with_locations = [
module_with_locations
for module_with_locations in imported_modules_with_locations
if not module_with_locations.module.standard_library
]

violations = find_violations(
imported_modules_with_locations, dependencies_extract.dependencies, self.ignore, self.per_rule_ignores
imported_modules_with_locations,
dependencies_extract.dependencies,
self.ignore,
self.per_rule_ignores,
standard_library_modules,
)
TextReporter(violations, use_ansi=not self.no_ansi).report()

Expand Down Expand Up @@ -126,7 +125,7 @@
)

@staticmethod
def _get_stdlib_modules() -> frozenset[str]:
def _get_standard_library_modules() -> frozenset[str]:
if sys.version_info[:2] >= (3, 10):
return sys.stdlib_module_names

Expand Down
8 changes: 4 additions & 4 deletions python/deptry/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def __init__(
self,
name: str,
local_modules: set[str],
stdlib_modules: frozenset[str],
standard_library_modules: frozenset[str],
dependencies: list[Dependency] | None = None,
dev_dependencies: list[Dependency] | None = None,
) -> None:
Expand All @@ -74,13 +74,13 @@ def __init__(
Args:
name: The name of the imported module
local_modules: The list of local modules
stdlib_modules: The list of Python stdlib modules
standard_library_modules: The list of Python stdlib modules
dependencies: A list of the project's dependencies
dev_dependencies: A list of the project's development dependencies
"""
self.name = name
self.local_modules = local_modules
self.stdlib_modules = stdlib_modules
self.standard_library_modules = standard_library_modules
self.dependencies = dependencies or []
self.dev_dependencies = dev_dependencies or []

Expand Down Expand Up @@ -137,7 +137,7 @@ def _get_corresponding_top_levels_from(self, dependencies: list[Dependency]) ->
]

def _in_standard_library(self) -> bool:
return self.name in self.stdlib_modules
return self.name in self.standard_library_modules

def _is_local_module(self) -> bool:
"""
Expand Down
4 changes: 4 additions & 0 deletions python/deptry/violations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,20 @@
from deptry.violations.dep003_transitive.violation import DEP003TransitiveDependencyViolation
from deptry.violations.dep004_misplaced_dev.finder import DEP004MisplacedDevDependenciesFinder
from deptry.violations.dep004_misplaced_dev.violation import DEP004MisplacedDevDependencyViolation
from deptry.violations.dep005_standard_library.finder import DEP005StandardLibraryDependencyFinder
from deptry.violations.dep005_standard_library.violation import DEP005StandardLibraryDependencyViolation

__all__ = (
"DEP001MissingDependencyViolation",
"DEP002UnusedDependencyViolation",
"DEP003TransitiveDependencyViolation",
"DEP004MisplacedDevDependencyViolation",
"DEP005StandardLibraryDependencyViolation",
"DEP001MissingDependenciesFinder",
"DEP002UnusedDependenciesFinder",
"DEP003TransitiveDependenciesFinder",
"DEP004MisplacedDevDependenciesFinder",
"DEP005StandardLibraryDependencyFinder",
"Violation",
"ViolationsFinder",
)
15 changes: 10 additions & 5 deletions python/deptry/violations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from deptry.module import Module, ModuleLocations


@dataclass
class ViolationsFinder(ABC):
"""Base class for all issues finders.

Expand All @@ -23,13 +22,19 @@ class ViolationsFinder(ABC):
dependencies: A list of Dependency objects representing the project's dependencies.
ignored_modules: A tuple of module names to ignore when scanning for issues. Defaults to an
empty tuple.

"""

violation: ClassVar[type[Violation]]
imported_modules_with_locations: list[ModuleLocations]
dependencies: list[Dependency]
ignored_modules: tuple[str, ...] = ()

def __init__(
fpgmaas marked this conversation as resolved.
Show resolved Hide resolved
self,
imported_modules_with_locations: list[ModuleLocations],
dependencies: list[Dependency],
ignored_modules: tuple[str, ...] = (),
):
self.imported_modules_with_locations = imported_modules_with_locations
self.dependencies = dependencies
self.ignored_modules = ignored_modules

@abstractmethod
def find(self) -> list[Violation]:
Expand Down
5 changes: 3 additions & 2 deletions python/deptry/violations/dep001_missing/finder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

from deptry.violations.base import ViolationsFinder
Expand All @@ -12,7 +11,6 @@
from deptry.violations.base import Violation


@dataclass
class DEP001MissingDependenciesFinder(ViolationsFinder):
"""
Given a list of imported modules and a list of project dependencies, determine which ones are missing.
Expand All @@ -27,6 +25,9 @@ def find(self) -> list[Violation]:
for module_with_locations in self.imported_modules_with_locations:
module = module_with_locations.module

if module.standard_library:
continue

logging.debug("Scanning module %s...", module.name)

if self._is_missing(module):
Expand Down
2 changes: 0 additions & 2 deletions python/deptry/violations/dep002_unused/finder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

from deptry.imports.location import Location
Expand All @@ -13,7 +12,6 @@
from deptry.violations import Violation


@dataclass
class DEP002UnusedDependenciesFinder(ViolationsFinder):
"""
Finds unused dependencies by comparing a list of imported modules to a list of project dependencies.
Expand Down
5 changes: 3 additions & 2 deletions python/deptry/violations/dep003_transitive/finder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

from deptry.violations.base import ViolationsFinder
Expand All @@ -12,7 +11,6 @@
from deptry.violations import Violation


@dataclass
class DEP003TransitiveDependenciesFinder(ViolationsFinder):
"""
Given a list of imported modules and a list of project dependencies, determine which ones are transitive.
Expand All @@ -34,6 +32,9 @@ def find(self) -> list[Violation]:
for module_with_locations in self.imported_modules_with_locations:
module = module_with_locations.module

if module.standard_library:
mkniewallner marked this conversation as resolved.
Show resolved Hide resolved
continue

logging.debug("Scanning module %s...", module.name)

if self._is_transitive(module):
Expand Down
5 changes: 3 additions & 2 deletions python/deptry/violations/dep004_misplaced_dev/finder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations

import logging
from dataclasses import dataclass
from typing import TYPE_CHECKING

from deptry.violations.base import ViolationsFinder
Expand All @@ -12,7 +11,6 @@
from deptry.violations import Violation


@dataclass
class DEP004MisplacedDevDependenciesFinder(ViolationsFinder):
"""
Given a list of imported modules and a list of project dependencies, determine which development dependencies
Expand All @@ -35,6 +33,9 @@
for module_with_locations in self.imported_modules_with_locations:
module = module_with_locations.module

if module.standard_library:
continue

Check warning on line 37 in python/deptry/violations/dep004_misplaced_dev/finder.py

View check run for this annotation

Codecov / codecov/patch

python/deptry/violations/dep004_misplaced_dev/finder.py#L37

Added line #L37 was not covered by tests

logging.debug("Scanning module %s...", module.name)
corresponding_package_name = self._get_package_name(module)

Expand Down
Empty file.
53 changes: 53 additions & 0 deletions python/deptry/violations/dep005_standard_library/finder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from deptry.imports.location import Location
from deptry.violations.base import ViolationsFinder
from deptry.violations.dep005_standard_library.violation import DEP005StandardLibraryDependencyViolation

if TYPE_CHECKING:
from deptry.dependency import Dependency
from deptry.module import ModuleLocations
from deptry.violations import Violation


class DEP005StandardLibraryDependencyFinder(ViolationsFinder):
fpgmaas marked this conversation as resolved.
Show resolved Hide resolved
"""
Finds dependencies that are part of the standard library but are defined as dependencies.
"""

violation = DEP005StandardLibraryDependencyViolation

def __init__(
self,
imported_modules_with_locations: list[ModuleLocations],
dependencies: list[Dependency],
standard_library_modules: frozenset[str],
ignored_modules: tuple[str, ...] = (),
):
super().__init__(imported_modules_with_locations, dependencies, ignored_modules)
self.standard_library_modules = standard_library_modules

def find(self) -> list[Violation]:
logging.debug("\nScanning for dependencies that are part of the standard library...")
stdlib_violations: list[Violation] = []

for dependency in self.dependencies:
logging.debug("Scanning module %s...", dependency.name)

if dependency.name in self.standard_library_modules:
if dependency.name in self.ignored_modules:
logging.debug(
"Dependency '%s' found to be a dependency that is part of the standard library, but ignoring.",
dependency.name,
)
continue

logging.debug(
"Dependency '%s' marked as a dependency that is part of the standard library.", dependency.name
)
stdlib_violations.append(self.violation(dependency, Location(dependency.definition_file)))

return stdlib_violations
21 changes: 21 additions & 0 deletions python/deptry/violations/dep005_standard_library/violation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, ClassVar

from deptry.violations.base import Violation

if TYPE_CHECKING:
from deptry.dependency import Dependency


@dataclass
class DEP005StandardLibraryDependencyViolation(Violation):
error_code: ClassVar[str] = "DEP005"
error_template: ClassVar[str] = (
"'{name}' is defined as a dependency but it is included in the Python standard library."
)
issue: Dependency

def get_error_message(self) -> str:
return self.error_template.format(name=self.issue.name)

Check warning on line 21 in python/deptry/violations/dep005_standard_library/violation.py

View check run for this annotation

Codecov / codecov/patch

python/deptry/violations/dep005_standard_library/violation.py#L21

Added line #L21 was not covered by tests
20 changes: 17 additions & 3 deletions python/deptry/violations/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
DEP002UnusedDependenciesFinder,
DEP003TransitiveDependenciesFinder,
DEP004MisplacedDevDependenciesFinder,
DEP005StandardLibraryDependencyFinder,
)

if TYPE_CHECKING:
Expand All @@ -31,19 +32,32 @@
dependencies: list[Dependency],
ignore: tuple[str, ...],
per_rule_ignores: Mapping[str, tuple[str, ...]],
standard_library_modules: frozenset[str],
) -> list[Violation]:
violations = []

for violation_finder in _VIOLATIONS_FINDERS:
if violation_finder.violation.error_code not in ignore:
violations.extend(
violation_finder(
imported_modules_with_locations,
dependencies,
per_rule_ignores.get(violation_finder.violation.error_code, ()),
imported_modules_with_locations=imported_modules_with_locations,
dependencies=dependencies,
ignored_modules=per_rule_ignores.get(violation_finder.violation.error_code, ()),
).find()
)

# Since DEP005StandardLibraryDependencyFinder has a different constructor than the other 4 classes,
# we handle it separately.
if DEP005StandardLibraryDependencyFinder.violation.error_code not in ignore:
fpgmaas marked this conversation as resolved.
Show resolved Hide resolved
violations.extend(

Check warning on line 52 in python/deptry/violations/finder.py

View check run for this annotation

Codecov / codecov/patch

python/deptry/violations/finder.py#L52

Added line #L52 was not covered by tests
DEP005StandardLibraryDependencyFinder(
imported_modules_with_locations=imported_modules_with_locations,
dependencies=dependencies,
ignored_modules=per_rule_ignores.get(violation_finder.violation.error_code, ()),
fpgmaas marked this conversation as resolved.
Show resolved Hide resolved
standard_library_modules=standard_library_modules,
).find()
)

return _get_sorted_violations(violations)


Expand Down
Loading
Loading