from __future__ import annotations from copy import deepcopy from typing import Optional, Set, cast from packaging.markers import Marker, Op, Variable # type: ignore[attr-defined] from packaging.requirements import Requirement def dependencies_with_extras(deps: list[Requirement], extras: set[str], package_name: str) -> list[Requirement]: return dependencies_with_extras_from_markers(extract_extra_markers(deps), extras, package_name) def dependencies_with_extras_from_markers( deps_with_markers: list[tuple[Requirement, set[str | None]]], extras: set[str], package_name: str, ) -> list[Requirement]: result: list[Requirement] = [] found: set[str] = set() todo: set[str | None] = extras | {None} visited: set[str | None] = set() while todo: new_extras: set[str | None] = set() for req, extra_markers in deps_with_markers: if todo & extra_markers: if req.name == package_name: # support for recursive extras new_extras.update(req.extras or set()) else: req_str = str(req) if req_str not in found: found.add(req_str) result.append(req) visited.update(todo) todo = new_extras - visited return result def extract_extra_markers(deps: list[Requirement]) -> list[tuple[Requirement, set[str | 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, Variable]) -> str | None: if isinstance(_marker, tuple) and len(_marker) == 3 and _marker[0].value == "extra" and _marker[1].value == "==": return _marker[2].value return None