Skip to content

Commit e7acc02

Browse files
authored
Merge pull request #1623 from sirosen/dependency-groups
Add an initial specification doc for Dependency Groups
2 parents 5f8ec55 + b767e4b commit e7acc02

File tree

2 files changed

+251
-0
lines changed

2 files changed

+251
-0
lines changed
+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
.. _dependency-groups:
2+
3+
=================
4+
Dependency Groups
5+
=================
6+
7+
This specification defines Dependency Groups, a mechanism for storing package
8+
requirements in ``pyproject.toml`` files such that they are not included in
9+
project metadata when it is built.
10+
11+
Dependency Groups are suitable for internal development use-cases like linting
12+
and testing, as well as for projects which are not built for distribution, like
13+
collections of related scripts.
14+
15+
Fundamentally, Dependency Groups should be thought of as being a standardized
16+
subset of the capabilities of ``requirements.txt`` files (which are
17+
``pip``-specific).
18+
19+
Specification
20+
=============
21+
22+
Examples
23+
--------
24+
25+
This is a simple table which shows a ``test`` group::
26+
27+
[dependency-groups]
28+
test = ["pytest>7", "coverage"]
29+
30+
and a similar table which defines ``test`` and ``coverage`` groups::
31+
32+
[dependency-groups]
33+
coverage = ["coverage[toml]"]
34+
test = ["pytest>7", {include-group = "coverage"}]
35+
36+
The ``[dependency-groups]`` Table
37+
---------------------------------
38+
39+
Dependency Groups are defined as a table in ``pyproject.toml`` named
40+
``dependency-groups``. The ``dependency-groups`` table contains an arbitrary
41+
number of user-defined keys, each of which has, as its value, a list of
42+
requirements.
43+
44+
``[dependency-groups]`` keys, sometimes also called "group names", must be
45+
:ref:`valid non-normalized names <name-format>`. Tools which handle Dependency
46+
Groups MUST :ref:`normalize <name-normalization>` these names before
47+
comparisons.
48+
49+
Tools SHOULD prefer to present the original, non-normalized name to users, and
50+
if duplicate names are detected after normalization, tools SHOULD emit an
51+
error.
52+
53+
Requirement lists, the values in ``[dependency-groups]``, may contain strings,
54+
tables (``dict`` in Python), or a mix of strings and tables. Strings must be
55+
valid :ref:`dependency specifiers <dependency-specifiers>`, and tables must be
56+
valid Dependency Group Includes.
57+
58+
Dependency Group Include
59+
------------------------
60+
61+
A Dependency Group Include includes another Dependency Group in the current
62+
group.
63+
64+
An include is a table with exactly one key, ``"include-group"``, whose value is
65+
a string, the name of another Dependency Group.
66+
67+
Includes are defined to be exactly equivalent to the contents of the named
68+
Dependency Group, inserted into the current group at the location of the include.
69+
For example, if ``foo = ["a", "b"]`` is one group, and
70+
``bar = ["c", {include-group = "foo"}, "d"]`` is another, then ``bar`` should
71+
evaluate to ``["c", "a", "b", "d"]`` when Dependency Group Includes are expanded.
72+
73+
Dependency Group Includes may specify the same package multiple times.
74+
Tools SHOULD NOT deduplicate or otherwise alter the list contents produced by the
75+
include. For example, given the following table:
76+
77+
.. code-block:: toml
78+
79+
[dependency-groups]
80+
group-a = ["foo"]
81+
group-b = ["foo>1.0"]
82+
group-c = ["foo<1.0"]
83+
all = [
84+
"foo",
85+
{include-group = "group-a"},
86+
{include-group = "group-b"},
87+
{include-group = "group-c"},
88+
]
89+
90+
The resolved value of ``all`` SHOULD be ``["foo", "foo", "foo>1.0", "foo<1.0"]``.
91+
Tools should handle such a list exactly as they would handle any other case in
92+
which they are asked to process the same requirement multiple times with
93+
different version constraints.
94+
95+
Dependency Group Includes may include groups containing Dependency Group Includes,
96+
in which case those includes should be expanded as well. Dependency Group Includes
97+
MUST NOT include cycles, and tools SHOULD report an error if they detect a cycle.
98+
99+
Package Building
100+
----------------
101+
102+
Build backends MUST NOT include Dependency Group data in built distributions as
103+
package metadata. This means that sdist ``PKG-INFO`` and wheel ``METADATA``
104+
files should not include referenceable fields containing Dependency Groups.
105+
106+
It is, however, valid to use Dependency Groups in the evaluation of dynamic
107+
metadata, and ``pyproject.toml`` files included in sdists will still contain
108+
``[dependency-groups]``. However, the table's contents are not part of a built
109+
package's interfaces.
110+
111+
Installing Dependency Groups & Extras
112+
-------------------------------------
113+
114+
There is no syntax or specification-defined interface for installing or
115+
referring to Dependency Groups. Tools are expected to provide dedicated
116+
interfaces for this purpose.
117+
118+
Tools MAY choose to provide the same or similar interfaces for interacting
119+
with Dependency Groups as they do for managing extras. Tools authors are
120+
advised that the specification does not forbid having an extra whose name
121+
matches a Dependency Group. Separately, users are advised to avoid creating
122+
Dependency Groups whose names match extras, and tools MAY treat such matching
123+
as an error.
124+
125+
Validation and Compatibility
126+
----------------------------
127+
128+
Tools supporting Dependency Groups may want to validate data before using it.
129+
When implementing such validation, authors should be aware of the possibility
130+
of future extensions to the specification, so that they do not unnecessarily
131+
emit errors or warnings.
132+
133+
Tools SHOULD error when evaluating or processing unrecognized data in
134+
Dependency Groups.
135+
136+
Tools SHOULD NOT eagerly validate the contents of *all* Dependency Groups
137+
unless they have a need to do so.
138+
139+
This means that in the presence of the following data, most tools should allow
140+
the ``foo`` group to be used and only error if the ``bar`` group is used:
141+
142+
.. code-block:: toml
143+
144+
[dependency-groups]
145+
foo = ["pyparsing"]
146+
bar = [{set-phasers-to = "stun"}]
147+
148+
.. note::
149+
150+
There are several known cases of tools which have good cause to be
151+
stricter. Linters and validators are an example, as their purpose is to
152+
validate the contents of all Dependency Groups.
153+
154+
Reference Implementation
155+
========================
156+
157+
The following Reference Implementation prints the contents of a Dependency
158+
Group to stdout, newline delimited.
159+
The output is therefore valid ``requirements.txt`` data.
160+
161+
.. code-block:: python
162+
163+
import re
164+
import sys
165+
import tomllib
166+
from collections import defaultdict
167+
168+
from packaging.requirements import Requirement
169+
170+
171+
def _normalize_name(name: str) -> str:
172+
return re.sub(r"[-_.]+", "-", name).lower()
173+
174+
175+
def _normalize_group_names(dependency_groups: dict) -> dict:
176+
original_names = defaultdict(list)
177+
normalized_groups = {}
178+
179+
for group_name, value in dependency_groups.items():
180+
normed_group_name = _normalize_name(group_name)
181+
original_names[normed_group_name].append(group_name)
182+
normalized_groups[normed_group_name] = value
183+
184+
errors = []
185+
for normed_name, names in original_names.items():
186+
if len(names) > 1:
187+
errors.append(f"{normed_name} ({', '.join(names)})")
188+
if errors:
189+
raise ValueError(f"Duplicate dependency group names: {', '.join(errors)}")
190+
191+
return normalized_groups
192+
193+
194+
def _resolve_dependency_group(
195+
dependency_groups: dict, group: str, past_groups: tuple[str, ...] = ()
196+
) -> list[str]:
197+
if group in past_groups:
198+
raise ValueError(f"Cyclic dependency group include: {group} -> {past_groups}")
199+
200+
if group not in dependency_groups:
201+
raise LookupError(f"Dependency group '{group}' not found")
202+
203+
raw_group = dependency_groups[group]
204+
if not isinstance(raw_group, list):
205+
raise ValueError(f"Dependency group '{group}' is not a list")
206+
207+
realized_group = []
208+
for item in raw_group:
209+
if isinstance(item, str):
210+
# packaging.requirements.Requirement parsing ensures that this is a valid
211+
# PEP 508 Dependency Specifier
212+
# raises InvalidRequirement on failure
213+
Requirement(item)
214+
realized_group.append(item)
215+
elif isinstance(item, dict):
216+
if tuple(item.keys()) != ("include-group",):
217+
raise ValueError(f"Invalid dependency group item: {item}")
218+
219+
include_group = _normalize_name(next(iter(item.values())))
220+
realized_group.extend(
221+
_resolve_dependency_group(
222+
dependency_groups, include_group, past_groups + (group,)
223+
)
224+
)
225+
else:
226+
raise ValueError(f"Invalid dependency group item: {item}")
227+
228+
return realized_group
229+
230+
231+
def resolve(dependency_groups: dict, group: str) -> list[str]:
232+
if not isinstance(dependency_groups, dict):
233+
raise TypeError("Dependency Groups table is not a dict")
234+
if not isinstance(group, str):
235+
raise TypeError("Dependency group name is not a str")
236+
return _resolve_dependency_group(dependency_groups, group)
237+
238+
239+
if __name__ == "__main__":
240+
with open("pyproject.toml", "rb") as fp:
241+
pyproject = tomllib.load(fp)
242+
243+
dependency_groups_raw = pyproject["dependency-groups"]
244+
dependency_groups = _normalize_group_names(dependency_groups_raw)
245+
print("\n".join(resolve(pyproject["dependency-groups"], sys.argv[1])))
246+
247+
History
248+
=======
249+
250+
- October 2024: This specification was approved through :pep:`735`.

source/specifications/section-distribution-metadata.rst

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Package Distribution Metadata
1010
version-specifiers
1111
dependency-specifiers
1212
pyproject-toml
13+
dependency-groups
1314
inline-script-metadata
1415
platform-compatibility-tags
1516
well-known-project-urls

0 commit comments

Comments
 (0)