Skip to content

Commit 60c0658

Browse files
authored
feat(poetry): support PEP 621 in Poetry 2.0+ (#1003)
* test: add Poetry PEP 621 unit and functional tests * fix(pep621): soft access `project` * feat(poetry): extend PEP 621 dependency getter * docs: document Poetry PEP 621 support
1 parent 26511d8 commit 60c0658

File tree

13 files changed

+444
-117
lines changed

13 files changed

+444
-117
lines changed

docs/supported-dependency-managers.md

Lines changed: 85 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ standard [PEP 621 format](https://packaging.python.org/en/latest/specifications/
55
dependencies in `pyproject.toml`, not all of them do. Even those that do often provide additional ways to define
66
dependencies that are not standardized.
77

8-
_deptry_ can extract dependencies from most of the package managers that support PEP
9-
621 (e.g. [uv](https://docs.astral.sh/uv/), [PDM](https://pdm-project.org/en/latest/)), including tool-specific
10-
extensions, but also from package managers that do not (or used to not) support PEP
11-
621 (e.g. [Poetry](https://python-poetry.org/), [pip](https://pip.pypa.io/en/stable/reference/requirements-file-format/)).
8+
_deptry_ can extract dependencies for any dependency manager that supports standard PEP 621, while also extracting them
9+
from locations that are specific to some dependency managers that support this standard, but provide additional ways of
10+
defining dependencies (e.g., [uv](https://docs.astral.sh/uv/), [Poetry](https://python-poetry.org/)).
11+
12+
_deptry_ can also extract dependencies from dependency managers that do not support PEP 621 at
13+
all (e.g., [pip](https://pip.pypa.io/en/stable/reference/requirements-file-format/)).
1214

1315
## PEP 621
1416

@@ -22,7 +24,7 @@ By default, _deptry_ extracts, from `pyproject.toml`:
2224
- groups under `[project.optional-dependencies]` section
2325
- development dependencies from groups under `[dependency-groups]` section
2426

25-
For instance, with this `pyproject.toml`:
27+
For instance, given this `pyproject.toml`:
2628

2729
```toml title="pyproject.toml"
2830
[project]
@@ -57,7 +59,8 @@ the following dependencies will be extracted:
5759

5860
### uv
5961

60-
Additionally to PEP 621 dependencies, _deptry_ will
62+
If a `[tool.uv.dev-dependencies]` section is found, _deptry_ will assume that uv is used as a dependency manager, and
63+
will, additionally to PEP 621 dependencies,
6164
extract [uv development dependencies](https://docs.astral.sh/uv/concepts/dependencies/#development-dependencies) from
6265
`dev-dependencies` entry under `[tool.uv]` section, for instance:
6366

@@ -70,9 +73,84 @@ dev-dependencies = [
7073
]
7174
```
7275

76+
### Poetry
77+
78+
Until [version 2.0](https://python-poetry.org/blog/announcing-poetry-2.0.0/), Poetry did not support PEP 621 syntax to
79+
define project dependencies, instead relying on a specific syntax.
80+
81+
Because Poetry now supports PEP 621, it is now treated as an extension of PEP 621 manager, allowing _deptry_ to retrieve
82+
dependencies defined under `[project.dependencies]` and `[project.optional-dependencies]`, while still allowing
83+
retrieving:
84+
85+
- regular dependencies from `[tool.poetry.dependencies]` (which is still supported in Poetry 2.0)
86+
- development dependencies from `[tool.poetry.group.<group>.dependencies]` and `[tool.poetry.dev-dependencies]`
87+
88+
#### Regular dependencies
89+
90+
Which regular dependencies are extracted depend on how you define your dependencies with Poetry, as _deptry_ will
91+
closely
92+
match [Poetry's behavior](https://python-poetry.org/docs/dependency-specification/#projectdependencies-and-toolpoetrydependencies).
93+
94+
If `[project.dependencies]` is not set, or is empty, regular dependencies will be extracted from
95+
`[tool.poetry.dependencies]`. For instance, in this case:
96+
97+
```toml title="pyproject.toml"
98+
[project]
99+
name = "foo"
100+
101+
[tool.poetry.dependencies]
102+
httpx = "0.28.1"
103+
```
104+
105+
`httpx` will be extracted as a regular dependency.
106+
107+
If `[project.dependencies]` contains at least one dependency, then dependencies will **NOT** be extracted from
108+
`[tool.poetry.dependencies]`, as in that case, Poetry will only consider that data in this section enriches dependencies
109+
already defined in `[project.dependencies]` (for instance, to set a specific source), and not defining new dependencies.
110+
111+
For instance, in this case:
112+
113+
```toml title="pyproject.toml"
114+
[project]
115+
name = "foo"
116+
dependencies = ["httpx"]
117+
118+
[tool.poetry.dependencies]
119+
httpx = { git = "https://github.com/encode/httpx", tag = "0.28.1" }
120+
urllib3 = "2.3.0"
121+
```
122+
123+
although `[tool.poetry.dependencies]` contains both `httpx` and `urllib3`, only `httpx` will be extracted as a regular
124+
dependency, as `[project.dependencies]` contains at least one dependency, so Poetry itself will not consider `urllib3`
125+
to be a dependency of the project.
126+
127+
#### Development dependencies
128+
129+
In Poetry, [development dependencies](https://python-poetry.org/docs/managing-dependencies/#dependency-groups) can be
130+
defined under either (or both):
131+
132+
- `[tool.poetry.group.<group>.dependencies]` sections
133+
- `[tool.poetry.dev-dependencies]` section (which is considered legacy)
134+
135+
_deptry_ will extract dependencies from all those sections, for instance:
136+
137+
```toml title="pyproject.toml"
138+
[tool.poetry.dev-dependencies]
139+
mypy = "1.14.1"
140+
ruff = "0.8.6"
141+
142+
[tool.poetry.group.docs.dependencies]
143+
mkdocs = "1.6.1"
144+
145+
[tool.poetry.group.test.dependencies]
146+
pytest = "8.3.3"
147+
pytest-cov = "5.0.0"
148+
```
149+
73150
### PDM
74151

75-
Additionally to PEP 621 dependencies, _deptry_ will
152+
If a `[tool.pdm.dev-dependencies]` section is found, _deptry_ will assume that PDM is used as a dependency manager, and
153+
will, additionally to PEP 621 dependencies,
76154
extract [PDM development dependencies](https://pdm-project.org/en/latest/usage/dependency/#add-development-only-dependencies)
77155
from `[tool.pdm.dev-dependencies]` section, for instance:
78156

@@ -115,43 +193,6 @@ In this example, regular dependencies will be extracted from both `requirements.
115193
using [`--pep621-dev-dependency-groups`](usage.md#pep-621-dev-dependency-groups) argument (or its
116194
`pep_621_dev_dependency_groups` equivalent in `pyproject.toml`).
117195

118-
## Poetry
119-
120-
_deptry_ supports
121-
extracting [dependencies defined using Poetry](https://python-poetry.org/docs/pyproject/#dependencies-and-dependency-groups),
122-
and uses the presence of a `[tool.poetry.dependencies]` section in `pyproject.toml` to determine that the project uses
123-
Poetry.
124-
125-
In a `pyproject.toml` file where Poetry is used, _deptry_ will extract:
126-
127-
- regular dependencies from entries under `[tool.poetry.dependencies]` section
128-
- development dependencies from entries under each `[tool.poetry.group.<group>.dependencies]` section (or the
129-
legacy `[tool.poetry.dev-dependencies]` section)
130-
131-
For instance, given the following `pyproject.toml` file:
132-
133-
```toml title="pyproject.toml"
134-
[tool.poetry.dependencies]
135-
python = "^3.10"
136-
orjson = "^3.0.0"
137-
click = { version = "^8.0.0", optional = true }
138-
139-
[tool.poetry.extras]
140-
cli = ["click"]
141-
142-
[tool.poetry.group.docs.dependencies]
143-
mkdocs = "1.6.1"
144-
145-
[tool.poetry.group.test.dependencies]
146-
pytest = "8.3.3"
147-
pytest-cov = "5.0.0"
148-
```
149-
150-
the following dependencies will be extracted:
151-
152-
- regular dependencies: `orjson`, `click`
153-
- development dependencies: `mkdocs`, `pytest`, `pytest-cov`
154-
155196
## `requirements.txt` (pip, pip-tools)
156197

157198
_deptry_ supports extracting [dependencies using

python/deptry/dependency_getter/builder.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
from deptry.dependency_getter.pep621.base import PEP621DependencyGetter
99
from deptry.dependency_getter.pep621.pdm import PDMDependencyGetter
10+
from deptry.dependency_getter.pep621.poetry import PoetryDependencyGetter
1011
from deptry.dependency_getter.pep621.uv import UvDependencyGetter
11-
from deptry.dependency_getter.poetry import PoetryDependencyGetter
1212
from deptry.dependency_getter.requirements_files import RequirementsTxtDependencyGetter
1313
from deptry.exceptions import DependencySpecificationNotFoundError
1414
from deptry.utils import load_pyproject_toml
@@ -45,7 +45,9 @@ def build(self) -> DependencyGetter:
4545
pyproject_toml = load_pyproject_toml(self.config)
4646

4747
if self._project_uses_poetry(pyproject_toml):
48-
return PoetryDependencyGetter(self.config, self.package_module_name_map)
48+
return PoetryDependencyGetter(
49+
self.config, self.package_module_name_map, self.pep621_dev_dependency_groups
50+
)
4951

5052
if self._project_uses_uv(pyproject_toml):
5153
return UvDependencyGetter(self.config, self.package_module_name_map, self.pep621_dev_dependency_groups)

python/deptry/dependency_getter/pep621/base.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def _get_dependencies(self) -> list[Dependency]:
6161
"""Extract dependencies from `[project.dependencies]` (https://packaging.python.org/en/latest/specifications/pyproject-toml/#dependencies-optional-dependencies)."""
6262
pyproject_data = load_pyproject_toml(self.config)
6363

64-
if self._project_uses_setuptools(pyproject_data) and "dependencies" in pyproject_data["project"].get(
64+
if self._project_uses_setuptools(pyproject_data) and "dependencies" in pyproject_data.get("project", {}).get(
6565
"dynamic", {}
6666
):
6767
dependencies_files = pyproject_data["tool"]["setuptools"]["dynamic"]["dependencies"]["file"]
@@ -70,16 +70,16 @@ def _get_dependencies(self) -> list[Dependency]:
7070

7171
return get_dependencies_from_requirements_files(dependencies_files, self.package_module_name_map)
7272

73-
dependency_strings: list[str] = pyproject_data["project"].get("dependencies", [])
73+
dependency_strings: list[str] = pyproject_data.get("project", {}).get("dependencies", [])
7474
return self._extract_pep_508_dependencies(dependency_strings)
7575

7676
def _get_optional_dependencies(self) -> dict[str, list[Dependency]]:
7777
"""Extract dependencies from `[project.optional-dependencies]` (https://packaging.python.org/en/latest/specifications/pyproject-toml/#dependencies-optional-dependencies)."""
7878
pyproject_data = load_pyproject_toml(self.config)
7979

80-
if self._project_uses_setuptools(pyproject_data) and "optional-dependencies" in pyproject_data["project"].get(
81-
"dynamic", {}
82-
):
80+
if self._project_uses_setuptools(pyproject_data) and "optional-dependencies" in pyproject_data.get(
81+
"project", {}
82+
).get("dynamic", {}):
8383
return {
8484
group: get_dependencies_from_requirements_files(
8585
[specification["file"]] if isinstance(specification["file"], str) else specification["file"],
@@ -92,7 +92,7 @@ def _get_optional_dependencies(self) -> dict[str, list[Dependency]]:
9292

9393
return {
9494
group: self._extract_pep_508_dependencies(dependencies)
95-
for group, dependencies in pyproject_data["project"].get("optional-dependencies", {}).items()
95+
for group, dependencies in pyproject_data.get("project", {}).get("optional-dependencies", {}).items()
9696
}
9797

9898
def _get_dependency_groups_dependencies(self) -> dict[str, list[Dependency]]:
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from __future__ import annotations
2+
3+
import contextlib
4+
from dataclasses import dataclass
5+
from typing import Any
6+
7+
from deptry.dependency import Dependency
8+
from deptry.dependency_getter.pep621.base import PEP621DependencyGetter
9+
from deptry.utils import load_pyproject_toml
10+
11+
12+
@dataclass
13+
class PoetryDependencyGetter(PEP621DependencyGetter):
14+
"""
15+
Class that retrieves dependencies from a project that uses Poetry, either through PEP 621 syntax, Poetry specific
16+
syntax, or a mix of both.
17+
"""
18+
19+
def _get_dependencies(self) -> list[Dependency]:
20+
"""
21+
Retrieve dependencies from either:
22+
- `[project.dependencies]` defined by PEP 621
23+
- `[tool.poetry.dependencies]` which is specific to Poetry
24+
25+
If dependencies are set in `[project.dependencies]`, then assume that the project uses PEP 621 format to define
26+
dependencies. Even if `[tool.poetry.dependencies]` is populated, having entries in `[project.dependencies]`
27+
means that `[tool.poetry.dependencies]` is only used to enrich existing dependencies, and cannot be used to
28+
define additional ones.
29+
30+
If no dependencies are found in `[project.dependencies]`, then extract dependencies present in
31+
`[tool.poetry.dependencies]`.
32+
"""
33+
if dependencies := super()._get_dependencies():
34+
return dependencies
35+
36+
pyproject_data = load_pyproject_toml(self.config)
37+
return self._extract_poetry_dependencies(pyproject_data["tool"]["poetry"].get("dependencies", {}))
38+
39+
def _get_dev_dependencies(
40+
self,
41+
dependency_groups_dependencies: dict[str, list[Dependency]],
42+
dev_dependencies_from_optional: list[Dependency],
43+
) -> list[Dependency]:
44+
"""
45+
Poetry's development dependencies can be specified under either, or both:
46+
- [tool.poetry.dev-dependencies]
47+
- [tool.poetry.group.<group>.dependencies]
48+
"""
49+
dev_dependencies = super()._get_dev_dependencies(dependency_groups_dependencies, dev_dependencies_from_optional)
50+
51+
pyproject_data = load_pyproject_toml(self.config)
52+
poetry_dev_dependencies: dict[str, str] = {}
53+
54+
with contextlib.suppress(KeyError):
55+
poetry_dev_dependencies = {
56+
**poetry_dev_dependencies,
57+
**pyproject_data["tool"]["poetry"]["dev-dependencies"],
58+
}
59+
60+
try:
61+
dependency_groups = pyproject_data["tool"]["poetry"]["group"]
62+
except KeyError:
63+
dependency_groups = {}
64+
65+
for group_values in dependency_groups.values():
66+
with contextlib.suppress(KeyError):
67+
poetry_dev_dependencies = {**poetry_dev_dependencies, **group_values["dependencies"]}
68+
69+
return [*dev_dependencies, *self._extract_poetry_dependencies(poetry_dev_dependencies)]
70+
71+
def _extract_poetry_dependencies(self, poetry_dependencies: dict[str, Any]) -> list[Dependency]:
72+
return [
73+
Dependency(dep, self.config, module_names=self.package_module_name_map.get(dep))
74+
for dep in poetry_dependencies
75+
if dep != "python"
76+
]

python/deptry/dependency_getter/poetry.py

Lines changed: 0 additions & 60 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[virtualenvs]
2+
in-project = true

0 commit comments

Comments
 (0)