Skip to content

Commit 99af225

Browse files
committed
Add doc for dependency-groups, import from PEP
This content is copied from the PEP, with the following characteristics: - the initial overview and examples are new - the "History" footer is new - all of the other sections are copied from the PEP verbatim
1 parent 13308d5 commit 99af225

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
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+
This PEP defines a new section (table) in ``pyproject.toml`` files 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 (defined below). These keys must be
43+
`valid non-normalized names <https://packaging.python.org/en/latest/specifications/name-normalization/#valid-non-normalized-names>`__,
44+
and must be
45+
`normalized <https://packaging.python.org/en/latest/specifications/name-normalization/#normalization>`__
46+
before comparisons.
47+
48+
Tools SHOULD prefer to present the original, non-normalized name to users by
49+
default. If duplicate names, after normalization, are encountered, tools SHOULD
50+
emit an error.
51+
52+
Requirement lists under ``dependency-groups`` may contain strings, tables
53+
("dicts" in Python), or a mix of strings and tables.
54+
55+
Strings in requirement lists must be valid
56+
`Dependency Specifiers <https://packaging.python.org/en/latest/specifications/dependency-specifiers/>`__,
57+
as defined in :pep:`508`.
58+
59+
Tables in requirement lists must be valid Dependency Object Specifiers.
60+
61+
Dependency Object Specifiers
62+
----------------------------
63+
64+
Dependency Object Specifiers are tables which define zero or more dependencies.
65+
66+
This PEP standardizes only one type of Dependency Object Specifier, a
67+
"Dependency Group Include". Other types may be added in future standards.
68+
69+
Dependency Group Include
70+
''''''''''''''''''''''''
71+
72+
A Dependency Group Include includes the dependencies of another Dependency
73+
Group in the current Dependency Group.
74+
75+
An include is defined as a table with exactly one key, ``"include-group"``,
76+
whose value is a string, the name of another Dependency Group.
77+
78+
For example, ``{include-group = "test"}`` is an include which expands to the
79+
contents of the ``test`` Dependency Group.
80+
81+
Includes are defined to be exactly equivalent to the contents of the named
82+
Dependency Group, inserted into the current group at the location of the include.
83+
For example, if ``foo = ["a", "b"]`` is one group, and
84+
``bar = ["c", {include-group = "foo"}, "d"]`` is another, then ``bar`` should
85+
evaluate to ``["c", "a", "b", "d"]`` when Dependency Group Includes are expanded.
86+
87+
Dependency Group Includes may specify the same package multiple times. Tools
88+
SHOULD NOT deduplicate or otherwise alter the list contents produced by the
89+
include. For example, given the following table:
90+
91+
.. code:: toml
92+
93+
[dependency-groups]
94+
group-a = ["foo"]
95+
group-b = ["foo>1.0"]
96+
group-c = ["foo<1.0"]
97+
all = ["foo", {include-group = "group-a"}, {include-group = "group-b"}, {include-group = "group-c"}]
98+
99+
The resolved value of ``all`` SHOULD be ``["foo", "foo", "foo>1.0", "foo<1.0"]``.
100+
Tools should handle such a list exactly as they would handle any other case in
101+
which they are asked to process the same requirement multiple times with
102+
different version constraints.
103+
104+
Dependency Group Includes may include lists containing Dependency Group
105+
Includes, in which case those includes should be expanded as well. Dependency
106+
Group Includes MUST NOT include cycles, and tools SHOULD report an error if
107+
they detect a cycle.
108+
109+
Package Building
110+
----------------
111+
112+
Build backends MUST NOT include Dependency Group data in built distributions as
113+
package metadata. This means that PKG-INFO in sdists and METADATA in wheels
114+
do not include any referencable fields containing Dependency Groups.
115+
116+
It is valid to use Dependency Groups in the evaluation of dynamic metadata, and
117+
``pyproject.toml`` files included in sdists will naturally still contain the
118+
``[dependency-groups]`` table. However, the table contents are not part of a
119+
published package's interfaces.
120+
121+
Installing Dependency Groups
122+
----------------------------
123+
124+
Tools which support Dependency Groups are expected to provide new options and
125+
interfaces to allow users to install from Dependency Groups.
126+
127+
No syntax is defined for expressing the Dependency Group of a package, for two
128+
reasons:
129+
130+
* it would not be valid to refer to the Dependency Groups of a third-party
131+
package from PyPI (because the data is defined to be unpublished)
132+
133+
* there is not guaranteed to be a current package for Dependency Groups -- part
134+
of their purpose is to support non-package projects
135+
136+
For example, a possible pip interface for installing Dependency Groups
137+
would be:
138+
139+
.. code:: shell
140+
141+
pip install --dependency-groups=test,typing
142+
143+
Note that this is only an example. This PEP does not declare any requirements
144+
for how tools support the installation of Dependency Groups.
145+
146+
Overlapping Install UX with Extras
147+
''''''''''''''''''''''''''''''''''
148+
149+
Tools MAY choose to provide the same interfaces for installing Dependency
150+
Groups as they do for installing extras.
151+
152+
Note that this specification does not forbid having an extra whose name matches
153+
a Dependency Group.
154+
155+
Users are advised to avoid creating Dependency Groups whose names match extras.
156+
Tools MAY treat such matching as an error.
157+
158+
Validation and Compatibility
159+
----------------------------
160+
161+
Tools supporting Dependency Groups may want to validate data before using it.
162+
However, tools implementing such validation behavior should be careful to allow
163+
for future expansions to this spec, so that they do not unnecessarily emit
164+
errors or warnings in the presence of new syntax.
165+
166+
Tools SHOULD error when evaluating or processing unrecognized data in
167+
Dependency Groups.
168+
169+
Tools SHOULD NOT eagerly validate the list contents of **all** Dependency
170+
Groups.
171+
172+
This means that in the presence of the following data, most tools will allow
173+
the ``foo`` group to be used, and will only error when the ``bar`` group is
174+
used:
175+
176+
Reference Implementation
177+
========================
178+
179+
The following Reference Implementation prints the contents of a Dependency
180+
Group to stdout, newline delimited.
181+
The output is therefore valid ``requirements.txt`` data.
182+
183+
.. code-block:: python
184+
185+
import re
186+
import sys
187+
import tomllib
188+
from collections import defaultdict
189+
190+
from packaging.requirements import Requirement
191+
192+
193+
def _normalize_name(name: str) -> str:
194+
return re.sub(r"[-_.]+", "-", name).lower()
195+
196+
197+
def _normalize_group_names(dependency_groups: dict) -> dict:
198+
original_names = defaultdict(list)
199+
normalized_groups = {}
200+
201+
for group_name, value in dependency_groups.items():
202+
normed_group_name = _normalize_name(group_name)
203+
original_names[normed_group_name].append(group_name)
204+
normalized_groups[normed_group_name] = value
205+
206+
errors = []
207+
for normed_name, names in original_names.items():
208+
if len(names) > 1:
209+
errors.append(f"{normed_name} ({', '.join(names)})")
210+
if errors:
211+
raise ValueError(f"Duplicate dependency group names: {', '.join(errors)}")
212+
213+
return normalized_groups
214+
215+
216+
def _resolve_dependency_group(
217+
dependency_groups: dict, group: str, past_groups: tuple[str, ...] = ()
218+
) -> list[str]:
219+
if group in past_groups:
220+
raise ValueError(f"Cyclic dependency group include: {group} -> {past_groups}")
221+
222+
if group not in dependency_groups:
223+
raise LookupError(f"Dependency group '{group}' not found")
224+
225+
raw_group = dependency_groups[group]
226+
if not isinstance(raw_group, list):
227+
raise ValueError(f"Dependency group '{group}' is not a list")
228+
229+
realized_group = []
230+
for item in raw_group:
231+
if isinstance(item, str):
232+
# packaging.requirements.Requirement parsing ensures that this is a valid
233+
# PEP 508 Dependency Specifier
234+
# raises InvalidRequirement on failure
235+
Requirement(item)
236+
realized_group.append(item)
237+
elif isinstance(item, dict):
238+
if tuple(item.keys()) != ("include-group",):
239+
raise ValueError(f"Invalid dependency group item: {item}")
240+
241+
include_group = _normalize_name(next(iter(item.values())))
242+
realized_group.extend(
243+
_resolve_dependency_group(
244+
dependency_groups, include_group, past_groups + (group,)
245+
)
246+
)
247+
else:
248+
raise ValueError(f"Invalid dependency group item: {item}")
249+
250+
return realized_group
251+
252+
253+
def resolve(dependency_groups: dict, group: str) -> list[str]:
254+
if not isinstance(dependency_groups, dict):
255+
raise TypeError("Dependency Groups table is not a dict")
256+
if not isinstance(group, str):
257+
raise TypeError("Dependency group name is not a str")
258+
return _resolve_dependency_group(dependency_groups, group)
259+
260+
261+
if __name__ == "__main__":
262+
with open("pyproject.toml", "rb") as fp:
263+
pyproject = tomllib.load(fp)
264+
265+
dependency_groups_raw = pyproject["dependency-groups"]
266+
dependency_groups = _normalize_group_names(dependency_groups_raw)
267+
print("\n".join(resolve(pyproject["dependency-groups"], sys.argv[1])))
268+
269+
History
270+
=======
271+
272+
- October 2024: This specification was approved through :pep:`735`.

source/specifications/section-distribution-metadata.rst

Lines changed: 1 addition & 0 deletions
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)