Skip to content

Commit 1210c0d

Browse files
authored
feat: support PEP 735 dependency groups (#892)
* feat(dependency_getter): support PEP 735 dependency groups * test(dependency_getter): add unit tests for `dependency-groups` * test(dependency_getter): add functional tests for `dependency-groups` * docs(usage): mention `[dependency-groups]` support
1 parent b105cd7 commit 1210c0d

File tree

16 files changed

+228
-57
lines changed

16 files changed

+228
-57
lines changed

docs/usage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ To determine the project's dependencies, _deptry_ will scan the directory it is
4242
- development dependencies from `[tool.uv.dev-dependencies]` section and from the groups under `[project.optional-dependencies]` passed via the [`--pep621-dev-dependency-groups`](#pep-621-dev-dependency-groups) argument.
4343
1. If a `pyproject.toml` file with a `[project]` section is found, _deptry_ will assume it uses [PEP 621](https://peps.python.org/pep-0621/) for dependency specification and extract:
4444
- dependencies from `[project.dependencies]` and `[project.optional-dependencies]`.
45-
- development dependencies from the groups under `[project.optional-dependencies]` passed via the [`--pep621-dev-dependency-groups`](#pep-621-dev-dependency-groups) argument.
45+
- development dependencies from the groups under `[dependency-groups]`, and the ones under `[project.optional-dependencies]` passed via the [`--pep621-dev-dependency-groups`](#pep-621-dev-dependency-groups) argument.
4646
1. If a `requirements.in` or `requirements.txt` file is found, _deptry_ will:
4747
- extract dependencies from that file.
4848
- extract development dependencies from `dev-dependencies.txt` and `dependencies-dev.txt`, if any exist

python/deptry/dependency_getter/pep621/base.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import itertools
34
import logging
45
import re
56
from dataclasses import dataclass
@@ -40,30 +41,52 @@ class PEP621DependencyGetter(DependencyGetter):
4041
def get(self) -> DependenciesExtract:
4142
dependencies = self._get_dependencies()
4243
optional_dependencies = self._get_optional_dependencies()
44+
dependency_groups_dependencies = self._get_dependency_groups_dependencies()
4345

4446
dev_dependencies_from_optional, remaining_optional_dependencies = (
4547
self._split_development_dependencies_from_optional_dependencies(optional_dependencies)
4648
)
4749
return DependenciesExtract(
4850
[*dependencies, *remaining_optional_dependencies],
49-
self._get_dev_dependencies(dev_dependencies_from_optional),
51+
self._get_dev_dependencies(dependency_groups_dependencies, dev_dependencies_from_optional),
5052
)
5153

5254
def _get_dependencies(self) -> list[Dependency]:
55+
"""Extract dependencies from `[project.dependencies]` (https://packaging.python.org/en/latest/specifications/pyproject-toml/#dependencies-optional-dependencies)."""
5356
pyproject_data = load_pyproject_toml(self.config)
5457
dependency_strings: list[str] = pyproject_data["project"].get("dependencies", [])
5558
return self._extract_pep_508_dependencies(dependency_strings)
5659

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

6064
return {
6165
group: self._extract_pep_508_dependencies(dependencies)
6266
for group, dependencies in pyproject_data["project"].get("optional-dependencies", {}).items()
6367
}
6468

65-
def _get_dev_dependencies(self, dev_dependencies_from_optional: list[Dependency]) -> list[Dependency]:
66-
return dev_dependencies_from_optional
69+
def _get_dependency_groups_dependencies(self) -> dict[str, list[Dependency]]:
70+
"""Extract dependencies from `[dependency-groups]` (https://peps.python.org/pep-0735/)."""
71+
pyproject_data = load_pyproject_toml(self.config)
72+
73+
return {
74+
# PEP 735 supports maps in dependency groups, to for instance extend existing
75+
# groups (https://peps.python.org/pep-0735/#dependency-group-include). Since we do not need to treat group
76+
# extension right now, and there are no other existing key, we want to filter out non-string items.
77+
group: self._extract_pep_508_dependencies(list(filter(lambda x: isinstance(x, str), dependencies)))
78+
for group, dependencies in pyproject_data.get("dependency-groups", {}).items()
79+
}
80+
81+
def _get_dev_dependencies(
82+
self,
83+
dependency_groups_dependencies: dict[str, list[Dependency]],
84+
dev_dependencies_from_optional: list[Dependency],
85+
) -> list[Dependency]:
86+
return [
87+
*itertools.chain(*dependency_groups_dependencies.values()),
88+
*dev_dependencies_from_optional,
89+
]
6790

6891
def _check_for_invalid_group_names(self, optional_dependencies: dict[str, list[Dependency]]) -> None:
6992
missing_groups = set(self.pep621_dev_dependency_groups) - set(optional_dependencies.keys())

python/deptry/dependency_getter/pep621/pdm.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ class PDMDependencyGetter(PEP621DependencyGetter):
1818
uses PDM for its dependency management.
1919
"""
2020

21-
def _get_dev_dependencies(self, dev_dependencies_from_optional: list[Dependency]) -> list[Dependency]:
21+
def _get_dev_dependencies(
22+
self,
23+
dependency_groups_dependencies: dict[str, list[Dependency]],
24+
dev_dependencies_from_optional: list[Dependency],
25+
) -> list[Dependency]:
2226
"""
2327
Retrieve dev dependencies from pyproject.toml, which in PDM are specified as:
2428
@@ -32,7 +36,7 @@ def _get_dev_dependencies(self, dev_dependencies_from_optional: list[Dependency]
3236
"tox-pdm>=0.5",
3337
]
3438
"""
35-
dev_dependencies = super()._get_dev_dependencies(dev_dependencies_from_optional)
39+
dev_dependencies = super()._get_dev_dependencies(dependency_groups_dependencies, dev_dependencies_from_optional)
3640

3741
pyproject_data = load_pyproject_toml(self.config)
3842

python/deptry/dependency_getter/pep621/uv.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ class UvDependencyGetter(PEP621DependencyGetter):
1818
uses uv for its dependency management.
1919
"""
2020

21-
def _get_dev_dependencies(self, dev_dependencies_from_optional: list[Dependency]) -> list[Dependency]:
21+
def _get_dev_dependencies(
22+
self,
23+
dependency_groups_dependencies: dict[str, list[Dependency]],
24+
dev_dependencies_from_optional: list[Dependency],
25+
) -> list[Dependency]:
2226
"""
2327
Retrieve dev dependencies from pyproject.toml, which in uv are specified as:
2428
@@ -31,7 +35,7 @@ def _get_dev_dependencies(self, dev_dependencies_from_optional: list[Dependency]
3135
3236
Dev dependencies marked as such from optional dependencies are also added to the list of dev dependencies found.
3337
"""
34-
dev_dependencies = super()._get_dev_dependencies(dev_dependencies_from_optional)
38+
dev_dependencies = super()._get_dev_dependencies(dependency_groups_dependencies, dev_dependencies_from_optional)
3539

3640
pyproject_data = load_pyproject_toml(self.config)
3741

tests/data/pep_621_project/pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ dev = [
2222
test = ["pytest==7.2.0"]
2323
plot = ["matplotlib"]
2424

25+
[dependency-groups]
26+
doc = [
27+
"mkdocs==1.6.1",
28+
"mkdocs-material==9.5.40",
29+
]
30+
all = [{include-group = "doc"}, "packaging==24.1"]
31+
2532
[build-system]
2633
requires = ["setuptools>=61.0.0"]
2734
build-backend = "setuptools.build_meta"

tests/data/pep_621_project/src/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from os import chdir, walk
22
from pathlib import Path
33

4+
import asyncio
45
import black
56
import click
7+
import mkdocs
8+
import mkdocs_material
9+
import packaging
610
import white as w
711
from urllib3 import contrib
8-
import asyncio

tests/data/project_with_pdm/pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ bar = ["requests>=2.28.1"]
1818
[tool.pdm]
1919
version = {source = "scm"}
2020

21+
[dependency-groups]
22+
doc = [
23+
"mkdocs==1.6.1",
24+
"mkdocs-material==9.5.40",
25+
]
26+
all = [{include-group = "doc"}, "packaging==24.1"]
27+
2128
[tool.pdm.dev-dependencies]
2229
lint = [
2330
"black>=22.6.0",

tests/data/project_with_pdm/src/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
import black
55
import click
6+
import mkdocs
7+
import mkdocs_material
68
import mypy
9+
import packaging
710
import pytest
811
import pytest_cov
912
import white as w

tests/data/project_with_uv/pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ foo = [
1717
]
1818
bar = ["requests==2.32.3"]
1919

20+
[dependency-groups]
21+
doc = [
22+
"mkdocs==1.6.1",
23+
"mkdocs-material==9.5.40",
24+
]
25+
all = [{include-group = "doc"}, "packaging==24.1"]
26+
2027
[tool.uv]
2128
dev-dependencies = [
2229
"black==24.8.0",

tests/data/project_with_uv/src/main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
import black
55
import click
6+
import mkdocs
7+
import mkdocs_material
68
import mypy
9+
import packaging
710
import pytest
811
import pytest_cov
912
import white as w

0 commit comments

Comments
 (0)