diff --git a/python/deptry/dependency_getter/builder.py b/python/deptry/dependency_getter/builder.py index 4e343532..1c95648a 100644 --- a/python/deptry/dependency_getter/builder.py +++ b/python/deptry/dependency_getter/builder.py @@ -7,8 +7,8 @@ from deptry.dependency_getter.pep621.base import PEP621DependencyGetter from deptry.dependency_getter.pep621.pdm import PDMDependencyGetter +from deptry.dependency_getter.pep621.poetry import PoetryDependencyGetter from deptry.dependency_getter.pep621.uv import UvDependencyGetter -from deptry.dependency_getter.poetry import PoetryDependencyGetter from deptry.dependency_getter.requirements_files import RequirementsTxtDependencyGetter from deptry.exceptions import DependencySpecificationNotFoundError from deptry.utils import load_pyproject_toml @@ -45,7 +45,9 @@ def build(self) -> DependencyGetter: pyproject_toml = load_pyproject_toml(self.config) if self._project_uses_poetry(pyproject_toml): - return PoetryDependencyGetter(self.config, self.package_module_name_map) + return PoetryDependencyGetter( + self.config, self.package_module_name_map, self.pep621_dev_dependency_groups + ) if self._project_uses_uv(pyproject_toml): return UvDependencyGetter(self.config, self.package_module_name_map, self.pep621_dev_dependency_groups) diff --git a/python/deptry/dependency_getter/pep621/poetry.py b/python/deptry/dependency_getter/pep621/poetry.py new file mode 100644 index 00000000..5bbf2ef0 --- /dev/null +++ b/python/deptry/dependency_getter/pep621/poetry.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +import contextlib +from dataclasses import dataclass +from typing import Any + +from deptry.dependency import Dependency +from deptry.dependency_getter.pep621.base import PEP621DependencyGetter +from deptry.utils import load_pyproject_toml + + +@dataclass +class PoetryDependencyGetter(PEP621DependencyGetter): + """ + Class that retrieves dependencies from a project that uses Poetry, either through PEP 621 syntax, Poetry specific + syntax, or a mix of both. + """ + + def _get_dependencies(self) -> list[Dependency]: + """ + Retrieve dependencies from either: + - `[project.dependencies]` defined by PEP 621 + - `[tool.poetry.dependencies]` which is specific to Poetry + + If dependencies are set in `[project.dependencies]`, then assume that the project uses PEP 621 format to define + dependencies. Even if `[tool.poetry.dependencies]` is populated, having entries in `[project.dependencies]` + means that `[tool.poetry.dependencies]` is only used to enrich existing dependencies, and cannot be used to + define additional ones. + + If no dependencies are found in `[project.dependencies]`, then extract dependencies present in + `[tool.poetry.dependencies]`. + """ + if dependencies := super()._get_dependencies(): + return dependencies + + pyproject_data = load_pyproject_toml(self.config) + return self._extract_poetry_dependencies(pyproject_data["tool"]["poetry"].get("dependencies", {})) + + def _get_dev_dependencies( + self, + dependency_groups_dependencies: dict[str, list[Dependency]], + dev_dependencies_from_optional: list[Dependency], + ) -> list[Dependency]: + """ + Poetry's development dependencies can be specified under either, or both: + - [tool.poetry.dev-dependencies] + - [tool.poetry.group..dependencies] + """ + dev_dependencies = super()._get_dev_dependencies(dependency_groups_dependencies, dev_dependencies_from_optional) + + pyproject_data = load_pyproject_toml(self.config) + poetry_dev_dependencies: dict[str, str] = {} + + with contextlib.suppress(KeyError): + poetry_dev_dependencies = { + **poetry_dev_dependencies, + **pyproject_data["tool"]["poetry"]["dev-dependencies"], + } + + try: + dependency_groups = pyproject_data["tool"]["poetry"]["group"] + except KeyError: + dependency_groups = {} + + for group_values in dependency_groups.values(): + with contextlib.suppress(KeyError): + poetry_dev_dependencies = {**poetry_dev_dependencies, **group_values["dependencies"]} + + return [*dev_dependencies, *self._extract_poetry_dependencies(poetry_dev_dependencies)] + + def _extract_poetry_dependencies(self, poetry_dependencies: dict[str, Any]) -> list[Dependency]: + return [ + Dependency(dep, self.config, module_names=self.package_module_name_map.get(dep)) + for dep in poetry_dependencies + if dep != "python" + ] diff --git a/python/deptry/dependency_getter/poetry.py b/python/deptry/dependency_getter/poetry.py deleted file mode 100644 index 6e35eca3..00000000 --- a/python/deptry/dependency_getter/poetry.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import contextlib -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -from deptry.dependency import Dependency -from deptry.dependency_getter.base import DependenciesExtract, DependencyGetter -from deptry.utils import load_pyproject_toml - -if TYPE_CHECKING: - from collections.abc import Mapping, Sequence - - -@dataclass -class PoetryDependencyGetter(DependencyGetter): - """Extract Poetry dependencies from pyproject.toml.""" - - def get(self) -> DependenciesExtract: - return DependenciesExtract(self._get_poetry_dependencies(), self._get_poetry_dev_dependencies()) - - def _get_poetry_dependencies(self) -> list[Dependency]: - pyproject_data = load_pyproject_toml(self.config) - dependencies: dict[str, Any] = pyproject_data["tool"]["poetry"].get("dependencies", {}) - return self._get_dependencies(dependencies, self.package_module_name_map) - - def _get_poetry_dev_dependencies(self) -> list[Dependency]: - """ - Poetry's development dependencies can be specified under either of the following: - - [tool.poetry.dev-dependencies] - [tool.poetry.group.dev.dependencies] - - or both. - """ - dependencies: dict[str, str] = {} - pyproject_data = load_pyproject_toml(self.config) - - with contextlib.suppress(KeyError): - dependencies = {**dependencies, **pyproject_data["tool"]["poetry"]["dev-dependencies"]} - - try: - dependency_groups = pyproject_data["tool"]["poetry"]["group"] - except KeyError: - dependency_groups = {} - - for group_values in dependency_groups.values(): - with contextlib.suppress(KeyError): - dependencies = {**dependencies, **group_values["dependencies"]} - - return self._get_dependencies(dependencies, self.package_module_name_map) - - def _get_dependencies( - self, poetry_dependencies: dict[str, Any], package_module_name_map: Mapping[str, Sequence[str]] - ) -> list[Dependency]: - return [ - Dependency(dep, self.config, module_names=package_module_name_map.get(dep)) - for dep in poetry_dependencies - if dep != "python" - ] diff --git a/tests/unit/dependency_getter/test_builder.py b/tests/unit/dependency_getter/test_builder.py index 6b58a305..3189a485 100644 --- a/tests/unit/dependency_getter/test_builder.py +++ b/tests/unit/dependency_getter/test_builder.py @@ -10,8 +10,8 @@ from deptry.dependency_getter.builder import DependencyGetterBuilder from deptry.dependency_getter.pep621.base import PEP621DependencyGetter from deptry.dependency_getter.pep621.pdm import PDMDependencyGetter +from deptry.dependency_getter.pep621.poetry import PoetryDependencyGetter from deptry.dependency_getter.pep621.uv import UvDependencyGetter -from deptry.dependency_getter.poetry import PoetryDependencyGetter from deptry.dependency_getter.requirements_files import RequirementsTxtDependencyGetter from deptry.exceptions import DependencySpecificationNotFoundError from tests.utils import run_within_dir diff --git a/tests/unit/dependency_getter/test_poetry.py b/tests/unit/dependency_getter/test_poetry.py index 4403836d..3d40127c 100644 --- a/tests/unit/dependency_getter/test_poetry.py +++ b/tests/unit/dependency_getter/test_poetry.py @@ -2,7 +2,7 @@ from pathlib import Path -from deptry.dependency_getter.poetry import PoetryDependencyGetter +from deptry.dependency_getter.pep621.poetry import PoetryDependencyGetter from tests.utils import run_within_dir