Skip to content

Commit 4578eaa

Browse files
dconathanpre-commit-ci[bot]gaborbernat
authored
update how extras are extracted to handle cases with more than 2 groups (#2812)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bernát Gábor <[email protected]> Resolves #2791 fixes #2791
1 parent 27c52ec commit 4578eaa

File tree

3 files changed

+50
-23
lines changed

3 files changed

+50
-23
lines changed

docs/changelog/2791.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix extracting extras from markers with more than 2 extras in an or chain - by :user:`dconathan`.
Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
from __future__ import annotations
22

33
from copy import deepcopy
4+
from typing import Optional, Set, cast
45

5-
from packaging.markers import Variable # type: ignore[attr-defined]
6+
from packaging.markers import Marker, Op, Value, Variable # type: ignore[attr-defined]
67
from packaging.requirements import Requirement
78

89

@@ -29,25 +30,42 @@ def dependencies_with_extras(deps: list[Requirement], extras: set[str], package_
2930

3031

3132
def extract_extra_markers(deps: list[Requirement]) -> list[tuple[Requirement, set[str | None]]]:
32-
# extras might show up as markers, move them into extras property
33-
result: list[tuple[Requirement, set[str | None]]] = []
34-
for req in deps:
35-
req = deepcopy(req)
36-
markers: list[str | tuple[Variable, Variable, Variable]] = getattr(req.marker, "_markers", []) or []
37-
_at: int | None = None
38-
extra_markers = set()
39-
for _at, (marker_key, op, marker_value) in (
40-
(_at_marker, marker)
41-
for _at_marker, marker in enumerate(markers)
42-
if isinstance(marker, tuple) and len(marker) == 3
43-
):
44-
if marker_key.value == "extra" and op.value == "==": # pragma: no branch
45-
extra_markers.add(marker_value.value)
46-
del markers[_at]
47-
_at -= 1
48-
if _at >= 0 and (isinstance(markers[_at], str) and markers[_at] in ("and", "or")):
49-
del markers[_at]
50-
if len(markers) == 0:
51-
req.marker = None
52-
result.append((req, extra_markers or {None}))
33+
"""
34+
Extract extra markers from dependencies.
35+
36+
:param deps: the dependencies
37+
:return: a list of requirement, extras set
38+
"""
39+
result = [_extract_extra_markers(d) for d in deps]
5340
return result
41+
42+
43+
def _extract_extra_markers(req: Requirement) -> tuple[Requirement, set[str | None]]:
44+
req = deepcopy(req)
45+
markers: list[str | tuple[Variable, Op, Variable]] = getattr(req.marker, "_markers", []) or []
46+
new_markers: list[str | tuple[Variable, Op, Variable]] = []
47+
extra_markers: set[str] = set() # markers that have a key of extra
48+
marker = markers.pop(0) if markers else None
49+
while marker:
50+
extra = _get_extra(marker)
51+
if extra is not None:
52+
extra_markers.add(extra)
53+
if new_markers and new_markers[-1] in ("and", "or"):
54+
del new_markers[-1]
55+
marker = markers.pop(0) if markers else None
56+
if marker in ("and", "or"):
57+
marker = markers.pop(0) if markers else None
58+
else:
59+
new_markers.append(marker)
60+
marker = markers.pop(0) if markers else None
61+
if new_markers:
62+
cast(Marker, req.marker)._markers = new_markers
63+
else:
64+
req.marker = None
65+
return req, cast(Set[Optional[str]], extra_markers) or {None}
66+
67+
68+
def _get_extra(_marker: str | tuple[Variable, Op, Value]) -> str | None:
69+
if isinstance(_marker, tuple) and len(_marker) == 3 and _marker[0].value == "extra" and _marker[1].value == "==":
70+
return cast(str, _marker[2].value)
71+
return None

tests/tox_env/python/virtual_env/package/test_python_package_util.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,13 @@ def test_loads_deps_recursive_extras() -> None:
6767

6868
def test_load_dependency_requirement_or_extras() -> None:
6969
requires = [Requirement('filelock<4.0.0,>=3.9.0; extra == "extras1" or extra == "extras2"')]
70-
result = dependencies_with_extras(requires, {"extras1"}, "")
70+
for extras in ["extras1", "extras2"]:
71+
result = dependencies_with_extras(requires, {extras}, "")
72+
assert [str(r) for r in result] == ["filelock<4.0.0,>=3.9.0"]
73+
74+
75+
@pytest.mark.parametrize("extra", ["extras1", "extras2", "extras3"])
76+
def test_load_dependency_requirement_many_or_extras(extra: str) -> None:
77+
requires = [Requirement('filelock<4.0.0,>=3.9.0; extra == "extras1" or extra == "extras2" or extra == "extras3"')]
78+
result = dependencies_with_extras(requires, {extra}, "")
7179
assert [str(r) for r in result] == ["filelock<4.0.0,>=3.9.0"]

0 commit comments

Comments
 (0)