Skip to content

Commit

Permalink
update how extras are extracted to handle cases with more than 2 grou…
Browse files Browse the repository at this point in the history
…ps (#2812)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bernát Gábor <bgabor8@bloomberg.net>
Resolves #2791
fixes #2791
  • Loading branch information
dconathan authored Jan 4, 2023
1 parent 27c52ec commit 4578eaa
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 23 deletions.
1 change: 1 addition & 0 deletions docs/changelog/2791.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix extracting extras from markers with more than 2 extras in an or chain - by :user:`dconathan`.
62 changes: 40 additions & 22 deletions src/tox/tox_env/python/virtual_env/package/util.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

from copy import deepcopy
from typing import Optional, Set, cast

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


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


def extract_extra_markers(deps: list[Requirement]) -> list[tuple[Requirement, set[str | None]]]:
# extras might show up as markers, move them into extras property
result: list[tuple[Requirement, set[str | None]]] = []
for req in deps:
req = deepcopy(req)
markers: list[str | tuple[Variable, Variable, Variable]] = getattr(req.marker, "_markers", []) or []
_at: int | None = None
extra_markers = set()
for _at, (marker_key, op, marker_value) in (
(_at_marker, marker)
for _at_marker, marker in enumerate(markers)
if isinstance(marker, tuple) and len(marker) == 3
):
if marker_key.value == "extra" and op.value == "==": # pragma: no branch
extra_markers.add(marker_value.value)
del markers[_at]
_at -= 1
if _at >= 0 and (isinstance(markers[_at], str) and markers[_at] in ("and", "or")):
del markers[_at]
if len(markers) == 0:
req.marker = None
result.append((req, extra_markers or {None}))
"""
Extract extra markers from dependencies.
:param deps: the dependencies
:return: a list of requirement, extras set
"""
result = [_extract_extra_markers(d) for d in deps]
return result


def _extract_extra_markers(req: Requirement) -> tuple[Requirement, set[str | None]]:
req = deepcopy(req)
markers: list[str | tuple[Variable, Op, Variable]] = getattr(req.marker, "_markers", []) or []
new_markers: list[str | tuple[Variable, Op, Variable]] = []
extra_markers: set[str] = set() # markers that have a key of extra
marker = markers.pop(0) if markers else None
while marker:
extra = _get_extra(marker)
if extra is not None:
extra_markers.add(extra)
if new_markers and new_markers[-1] in ("and", "or"):
del new_markers[-1]
marker = markers.pop(0) if markers else None
if marker in ("and", "or"):
marker = markers.pop(0) if markers else None
else:
new_markers.append(marker)
marker = markers.pop(0) if markers else None
if new_markers:
cast(Marker, req.marker)._markers = new_markers
else:
req.marker = None
return req, cast(Set[Optional[str]], extra_markers) or {None}


def _get_extra(_marker: str | tuple[Variable, Op, Value]) -> str | None:
if isinstance(_marker, tuple) and len(_marker) == 3 and _marker[0].value == "extra" and _marker[1].value == "==":
return cast(str, _marker[2].value)
return None
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,13 @@ def test_loads_deps_recursive_extras() -> None:

def test_load_dependency_requirement_or_extras() -> None:
requires = [Requirement('filelock<4.0.0,>=3.9.0; extra == "extras1" or extra == "extras2"')]
result = dependencies_with_extras(requires, {"extras1"}, "")
for extras in ["extras1", "extras2"]:
result = dependencies_with_extras(requires, {extras}, "")
assert [str(r) for r in result] == ["filelock<4.0.0,>=3.9.0"]


@pytest.mark.parametrize("extra", ["extras1", "extras2", "extras3"])
def test_load_dependency_requirement_many_or_extras(extra: str) -> None:
requires = [Requirement('filelock<4.0.0,>=3.9.0; extra == "extras1" or extra == "extras2" or extra == "extras3"')]
result = dependencies_with_extras(requires, {extra}, "")
assert [str(r) for r in result] == ["filelock<4.0.0,>=3.9.0"]

0 comments on commit 4578eaa

Please sign in to comment.