Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update how extras are extracted to handle cases with more than 2 groups #2812

Merged
merged 4 commits into from Jan 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog/2791.bugfix.rst
@@ -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
@@ -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
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"')]
gaborbernat marked this conversation as resolved.
Show resolved Hide resolved
result = dependencies_with_extras(requires, {extra}, "")
assert [str(r) for r in result] == ["filelock<4.0.0,>=3.9.0"]