Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-dep-logic for openSUSE:Factory checked in at 2025-07-14 10:51:42 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-dep-logic (Old) and /work/SRC/openSUSE:Factory/.python-dep-logic.new.7373 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-dep-logic" Mon Jul 14 10:51:42 2025 rev:6 rq:1292441 version:0.5.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-dep-logic/python-dep-logic.changes 2025-01-23 18:06:54.363835133 +0100 +++ /work/SRC/openSUSE:Factory/.python-dep-logic.new.7373/python-dep-logic.changes 2025-07-14 10:57:01.481870034 +0200 @@ -1,0 +2,13 @@ +Sat Jul 12 17:31:35 UTC 2025 - Dirk Müller <dmuel...@suse.com> + +- update to 0.5.1: + * Improve docstrings and format +- update to 0.5.0: + * Support 'extras' and 'dependency_groups' markers + * Improve logic for detecting msys2-based Python (works for + 3.11 and 3. +- update to 0.4.11: + * Handle irregular python tags + * Basic loongarch64 support + +------------------------------------------------------------------- Old: ---- dep_logic-0.4.10.tar.gz New: ---- dep_logic-0.5.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-dep-logic.spec ++++++ --- /var/tmp/diff_new_pack.2HD6dI/_old 2025-07-14 10:57:02.085895074 +0200 +++ /var/tmp/diff_new_pack.2HD6dI/_new 2025-07-14 10:57:02.089895240 +0200 @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-dep-logic -Version: 0.4.10 +Version: 0.5.1 Release: 0 Summary: Python dependency specifications supporting logical operations License: Apache-2.0 ++++++ dep_logic-0.4.10.tar.gz -> dep_logic-0.5.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/PKG-INFO new/dep_logic-0.5.1/PKG-INFO --- old/dep_logic-0.4.10/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 +++ new/dep_logic-0.5.1/PKG-INFO 1970-01-01 01:00:00.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: dep-logic -Version: 0.4.10 +Version: 0.5.1 Summary: Python dependency specifications supporting logical operations Keywords: dependency,specification,logic,packaging Author-Email: Frost Ming <m...@frostming.com> @@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 Classifier: License :: OSI Approved :: Apache Software License Requires-Python: >=3.8 Requires-Dist: packaging>=22 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/pyproject.toml new/dep_logic-0.5.1/pyproject.toml --- old/dep_logic-0.4.10/pyproject.toml 2024-12-13 12:04:19.215864000 +0100 +++ new/dep_logic-0.5.1/pyproject.toml 2025-05-19 11:34:51.963228700 +0200 @@ -24,9 +24,10 @@ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "License :: OSI Approved :: Apache Software License", ] -version = "0.4.10" +version = "0.5.1" [project.license] text = "Apache-2.0" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/markers/__init__.py new/dep_logic-0.5.1/src/dep_logic/markers/__init__.py --- old/dep_logic-0.4.10/src/dep_logic/markers/__init__.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/markers/__init__.py 2025-05-19 11:34:42.943260200 +0200 @@ -31,15 +31,15 @@ __all__ = [ - "parse_marker", - "from_pkg_marker", - "InvalidMarker", - "BaseMarker", "AnyMarker", + "BaseMarker", "EmptyMarker", + "InvalidMarker", "MarkerExpression", "MarkerUnion", "MultiMarker", + "from_pkg_marker", + "parse_marker", ] @@ -99,3 +99,34 @@ else: or_groups[-1] &= _build_markers(item) return MarkerUnion.of(*or_groups) + + +def _patch_marker_parser() -> None: + import re + + try: + from packaging._tokenizer import DEFAULT_RULES + except (ModuleNotFoundError, AttributeError): + return + + DEFAULT_RULES["VARIABLE"] = re.compile( + r""" + \b( + python_version + |python_full_version + |os[._]name + |sys[._]platform + |platform_(release|system) + |platform[._](version|machine|python_implementation) + |python_implementation + |implementation_(name|version) + |extras? + |dependency_groups + )\b + """, + re.VERBOSE, + ) + + +_patch_marker_parser() +del _patch_marker_parser diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/markers/any.py new/dep_logic-0.5.1/src/dep_logic/markers/any.py --- old/dep_logic-0.4.10/src/dep_logic/markers/any.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/markers/any.py 2025-05-19 11:34:42.943260200 +0200 @@ -1,6 +1,6 @@ from __future__ import annotations -from dep_logic.markers.base import BaseMarker +from dep_logic.markers.base import BaseMarker, EvaluationContext class AnyMarker(BaseMarker): @@ -17,7 +17,11 @@ def is_any(self) -> bool: return True - def evaluate(self, environment: dict[str, str] | None = None) -> bool: + def evaluate( + self, + environment: dict[str, str | set[str]] | None = None, + context: EvaluationContext = "metadata", + ): return True def without_extras(self) -> BaseMarker: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/markers/base.py new/dep_logic-0.5.1/src/dep_logic/markers/base.py --- old/dep_logic-0.4.10/src/dep_logic/markers/base.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/markers/base.py 2025-05-19 11:34:42.943260200 +0200 @@ -1,7 +1,9 @@ from __future__ import annotations from abc import ABCMeta, abstractmethod -from typing import Any +from typing import Any, Literal + +EvaluationContext = Literal["lock_file", "metadata", "requirement"] class BaseMarker(metaclass=ABCMeta): @@ -22,25 +24,41 @@ raise NotImplementedError def is_any(self) -> bool: + """Returns True if the marker allows any environment.""" return False def is_empty(self) -> bool: + """Returns True if the marker disallows any environment.""" return False @abstractmethod - def evaluate(self, environment: dict[str, str] | None = None) -> bool: + def evaluate( + self, + environment: dict[str, str | set[str]] | None = None, + context: EvaluationContext = "metadata", + ) -> bool: + """Evaluates the marker against the given environment. + + Args: + environment: The environment to evaluate against. + context: The context in which the evaluation is performed, + can be "lock_file", "metadata", or "requirement". + """ raise NotImplementedError @abstractmethod def without_extras(self) -> BaseMarker: + """Generate a new marker from the current marker but without "extra" markers.""" raise NotImplementedError @abstractmethod def exclude(self, marker_name: str) -> BaseMarker: + """Generate a new marker from the current marker but without the given marker.""" raise NotImplementedError @abstractmethod def only(self, *marker_names: str) -> BaseMarker: + """Generate a new marker from the current marker but only with the given markers.""" raise NotImplementedError def __repr__(self) -> str: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/markers/empty.py new/dep_logic-0.5.1/src/dep_logic/markers/empty.py --- old/dep_logic-0.4.10/src/dep_logic/markers/empty.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/markers/empty.py 2025-05-19 11:34:42.943260200 +0200 @@ -1,6 +1,6 @@ from __future__ import annotations -from dep_logic.markers.base import BaseMarker +from dep_logic.markers.base import BaseMarker, EvaluationContext class EmptyMarker(BaseMarker): @@ -17,7 +17,11 @@ def is_empty(self) -> bool: return True - def evaluate(self, environment: dict[str, str] | None = None) -> bool: + def evaluate( + self, + environment: dict[str, str | set[str]] | None = None, + context: EvaluationContext = "metadata", + ) -> bool: return False def without_extras(self) -> BaseMarker: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/markers/multi.py new/dep_logic-0.5.1/src/dep_logic/markers/multi.py --- old/dep_logic-0.4.10/src/dep_logic/markers/multi.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/markers/multi.py 2025-05-19 11:34:42.943260200 +0200 @@ -4,7 +4,7 @@ from typing import Iterator from dep_logic.markers.any import AnyMarker -from dep_logic.markers.base import BaseMarker +from dep_logic.markers.base import BaseMarker, EvaluationContext from dep_logic.markers.empty import EmptyMarker from dep_logic.markers.single import MarkerExpression, SingleMarker from dep_logic.utils import DATACLASS_ARGS, flatten_items, intersection, union @@ -135,8 +135,12 @@ return None - def evaluate(self, environment: dict[str, str] | None = None) -> bool: - return all(m.evaluate(environment) for m in self.markers) + def evaluate( + self, + environment: dict[str, str | set[str]] | None = None, + context: EvaluationContext = "metadata", + ) -> bool: + return all(m.evaluate(environment, context) for m in self.markers) def without_extras(self) -> BaseMarker: return self.exclude("extra") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/markers/single.py new/dep_logic-0.5.1/src/dep_logic/markers/single.py --- old/dep_logic-0.4.10/src/dep_logic/markers/single.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/markers/single.py 2025-05-19 11:34:42.943260200 +0200 @@ -1,24 +1,44 @@ from __future__ import annotations import functools +import operator import typing as t +from abc import abstractmethod from dataclasses import dataclass, field, replace -from packaging.markers import Marker as _Marker +from packaging.markers import default_environment +from packaging.specifiers import InvalidSpecifier, Specifier +from packaging.version import InvalidVersion from dep_logic.markers.any import AnyMarker -from dep_logic.markers.base import BaseMarker +from dep_logic.markers.base import BaseMarker, EvaluationContext from dep_logic.markers.empty import EmptyMarker from dep_logic.specifiers import BaseSpecifier from dep_logic.specifiers.base import VersionSpecifier from dep_logic.specifiers.generic import GenericSpecifier -from dep_logic.utils import DATACLASS_ARGS, OrderedSet, get_reflect_op +from dep_logic.utils import DATACLASS_ARGS, OrderedSet, get_reflect_op, normalize_name if t.TYPE_CHECKING: from dep_logic.markers.multi import MultiMarker from dep_logic.markers.union import MarkerUnion PYTHON_VERSION_MARKERS = {"python_version", "python_full_version"} +MARKERS_ALLOWING_SET = {"extras", "dependency_groups"} +Operator = t.Callable[[str, t.Union[str, t.Set[str]]], bool] +_operators: dict[str, Operator] = { + "in": lambda lhs, rhs: lhs in rhs, + "not in": lambda lhs, rhs: lhs not in rhs, + "<": operator.lt, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, + ">=": operator.ge, + ">": operator.gt, +} + + +class UndefinedComparison(ValueError): + pass class SingleMarker(BaseMarker): @@ -44,16 +64,25 @@ return self - def evaluate(self, environment: dict[str, str] | None = None) -> bool: - pkg_marker = _Marker(str(self)) - if self.name != "extra" or not environment or "extra" not in environment: - return pkg_marker.evaluate(environment) - extras = [extra] if isinstance(extra := environment["extra"], str) else extra - assert isinstance(self, MarkerExpression) - is_negated = self.op in ("not in", "!=") - if is_negated: - return all(pkg_marker.evaluate({"extra": extra}) for extra in extras) - return any(pkg_marker.evaluate({"extra": extra}) for extra in extras) + def evaluate( + self, + environment: dict[str, str | set[str]] | None = None, + context: EvaluationContext = "metadata", + ) -> bool: + current_environment = t.cast("dict[str, str|set[str]]", default_environment()) + if context == "metadata": + current_environment["extra"] = "" + elif context == "lock_file": + current_environment.update(extras=set(), dependency_groups=set()) + if environment: + current_environment.update(environment) + if "extra" in current_environment and current_environment["extra"] is None: + current_environment["extra"] = "" + return self._evaluate(current_environment) + + @abstractmethod + def _evaluate(self, environment: dict[str, str | set[str]]) -> bool: + raise NotImplementedError @dataclass(unsafe_hash=True, **DATACLASS_ARGS) @@ -141,6 +170,46 @@ return MarkerUnion(self, other) + def _evaluate(self, environment: dict[str, str | set[str]]) -> bool: + if self.name == "extra": + # Support batch comparison for "extra" markers + extra = environment["extra"] + if isinstance(extra, str): + extra = {extra} + assert self.op in ("==", "!=") + value = normalize_name(self.value) + extra = {normalize_name(v) for v in extra} + return value in extra if self.op == "==" else value not in extra + + target = environment[self.name] + if self.reversed: + lhs, rhs = self.value, target + oper = _operators.get(get_reflect_op(self.op)) + else: + lhs, rhs = target, self.value + assert isinstance(lhs, str) + oper = _operators.get(self.op) + if self.name in MARKERS_ALLOWING_SET: + lhs = normalize_name(lhs) + if isinstance(rhs, set): + rhs = {normalize_name(v) for v in rhs} + else: + rhs = normalize_name(rhs) + if isinstance(rhs, str): + try: + spec = Specifier(f"{self.op}{rhs}") + except InvalidSpecifier: + pass + else: + try: + return spec.contains(lhs) + except InvalidVersion: + pass + + if oper is None: + raise UndefinedComparison(f"Undefined comparison {self}") + return oper(lhs, rhs) + @dataclass(frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class EqualityMarkerUnion(SingleMarker): @@ -210,6 +279,9 @@ __rand__ = __and__ __ror__ = __or__ + def _evaluate(self, environment: dict[str, str | set[str]]) -> bool: + return environment[self.name] in self.values + @dataclass(frozen=True, unsafe_hash=True, **DATACLASS_ARGS) class InequalityMultiMarker(SingleMarker): @@ -283,6 +355,9 @@ __rand__ = __and__ __ror__ = __or__ + def _evaluate(self, environment: dict[str, str | set[str]]) -> bool: + return environment[self.name] not in self.values + @functools.lru_cache(maxsize=None) def _merge_single_markers( @@ -375,5 +450,5 @@ splitted[-1] = str(int(splitted[-1]) + 1) op = "<" - spec = parse_version_specifier(f'{op}{".".join(splitted)}') + spec = parse_version_specifier(f"{op}{'.'.join(splitted)}") return spec diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/markers/union.py new/dep_logic-0.5.1/src/dep_logic/markers/union.py --- old/dep_logic-0.4.10/src/dep_logic/markers/union.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/markers/union.py 2025-05-19 11:34:42.943260200 +0200 @@ -4,7 +4,7 @@ from typing import Iterator from dep_logic.markers.any import AnyMarker -from dep_logic.markers.base import BaseMarker +from dep_logic.markers.base import BaseMarker, EvaluationContext from dep_logic.markers.empty import EmptyMarker from dep_logic.markers.multi import MultiMarker from dep_logic.markers.single import SingleMarker @@ -135,8 +135,12 @@ return None - def evaluate(self, environment: dict[str, str] | None = None) -> bool: - return any(m.evaluate(environment) for m in self.markers) + def evaluate( + self, + environment: dict[str, str | set[str]] | None = None, + context: EvaluationContext = "metadata", + ) -> bool: + return any(m.evaluate(environment, context) for m in self.markers) def without_extras(self) -> BaseMarker: return self.exclude("extra") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/specifiers/range.py new/dep_logic-0.5.1/src/dep_logic/specifiers/range.py --- old/dep_logic-0.4.10/src/dep_logic/specifiers/range.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/specifiers/range.py 2025-05-19 11:34:42.944260100 +0200 @@ -81,7 +81,7 @@ simplified = self._simplified_form if simplified is not None: return simplified - return f'{">=" if self.include_min else ">"}{self.min},{"<=" if self.include_max else "<"}{self.max}' + return f"{'>=' if self.include_min else '>'}{self.min},{'<=' if self.include_max else '<'}{self.max}" def contains( self, version: UnparsedVersion, prereleases: bool | None = None @@ -115,11 +115,8 @@ if self.min is None: return True - return ( - self.min < other.min - or self.min == other.min - and self.include_min - and not other.include_min + return self.min < other.min or ( + self.min == other.min and self.include_min and not other.include_min ) def allows_higher(self, other: RangeSpecifier) -> bool: @@ -128,11 +125,8 @@ if self.max is None: return True - return ( - self.max > other.max - or self.max == other.max - and self.include_max - and not other.include_max + return self.max > other.max or ( + self.max == other.max and self.include_max and not other.include_max ) def is_strictly_lower(self, other: RangeSpecifier) -> bool: @@ -142,10 +136,8 @@ if self.max is None or other.min is None: return False - return ( - self.max < other.min - or self.max == other.min - and False in (self.include_max, other.include_min) + return self.max < other.min or ( + self.max == other.min and False in (self.include_max, other.include_min) ) def is_adjacent_to(self, other: RangeSpecifier) -> bool: @@ -162,22 +154,24 @@ return self.allows_lower(other) def is_superset(self, other: RangeSpecifier) -> bool: - min_lower = ( - self.min is None - or other.min is not None + min_lower = self.min is None or ( + other.min is not None and ( self.min < other.min - or self.min == other.min - and not (not self.include_min and other.include_min) + or ( + self.min == other.min + and not (not self.include_min and other.include_min) + ) ) ) - max_higher = ( - self.max is None - or other.max is not None + max_higher = self.max is None or ( + other.max is not None and ( self.max > other.max - or self.max == other.max - and not (not self.include_max and other.include_max) + or ( + self.max == other.max + and not (not self.include_max and other.include_max) + ) ) ) return min_lower and max_higher diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/specifiers/special.py new/dep_logic-0.5.1/src/dep_logic/specifiers/special.py --- old/dep_logic-0.4.10/src/dep_logic/specifiers/special.py 2024-12-13 12:04:09.695863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/specifiers/special.py 2025-05-19 11:34:42.944260100 +0200 @@ -36,7 +36,7 @@ return True def __contains__(self, value: str) -> bool: - return True + return False class AnySpecifier(BaseSpecifier): @@ -72,4 +72,4 @@ return True def __contains__(self, value: str) -> bool: - return False + return True diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/tags/platform.py new/dep_logic-0.5.1/src/dep_logic/tags/platform.py --- old/dep_logic-0.4.10/src/dep_logic/tags/platform.py 2024-12-13 12:04:09.696863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/tags/platform.py 2025-05-19 11:34:42.944260100 +0200 @@ -118,9 +118,11 @@ # Ex: macosx-11.2-arm64 version, architecture = version_arch.rsplit("-", 1) else: - # Ex: linux-x86_64 + # Ex: linux-x86_64 or x86_64_msvcrt_gnu version = None architecture = version_arch + if version_arch.startswith("x86_64"): + architecture = "x86_64" if operating_system == "linux": from packaging._manylinux import _get_glibc_version @@ -337,6 +339,7 @@ X86_64 = "x86_64" S390X = "s390x" RISCV64 = "riscv64" + LoongArch64 = "loongarch64" def __str__(self) -> str: return self.value diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/tags/tags.py new/dep_logic-0.5.1/src/dep_logic/tags/tags.py --- old/dep_logic-0.4.10/src/dep_logic/tags/tags.py 2024-12-13 12:04:09.696863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/tags/tags.py 2025-05-19 11:34:42.944260100 +0200 @@ -155,7 +155,7 @@ self, python_tag: str, abi_tag: str ) -> tuple[int, int, int] | None: """Return a tuple of (major, minor, abi) if the wheel is compatible with the environment, or None otherwise.""" - impl, major, minor = python_tag[:2], python_tag[2], python_tag[3:] + impl, major, minor = python_tag[:2], python_tag[2:3], python_tag[3:] if self.implementation is not None and impl not in [ self.implementation.short, "py", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/src/dep_logic/utils.py new/dep_logic-0.5.1/src/dep_logic/utils.py --- old/dep_logic-0.4.10/src/dep_logic/utils.py 2024-12-13 12:04:09.696863000 +0100 +++ new/dep_logic-0.5.1/src/dep_logic/utils.py 2025-05-19 11:34:42.944260100 +0200 @@ -191,3 +191,7 @@ def peek(self) -> T: return self._data[0] + + +def normalize_name(name: str) -> str: + return re.sub(r"[-_.]+", "-", name).lower() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dep_logic-0.4.10/tests/marker/test_evaluation.py new/dep_logic-0.5.1/tests/marker/test_evaluation.py --- old/dep_logic-0.4.10/tests/marker/test_evaluation.py 2024-12-13 12:04:09.696863000 +0100 +++ new/dep_logic-0.5.1/tests/marker/test_evaluation.py 2025-05-19 11:34:42.945260000 +0200 @@ -21,17 +21,17 @@ True, ), ( - "python_version ~= '2.7.0' and (os_name == 'foo' or " "os_name == 'bar')", + "python_version ~= '2.7.0' and (os_name == 'foo' or os_name == 'bar')", {"os_name": "foo", "python_version": "2.7.4"}, True, ), ( - "python_version ~= '2.7.0' and (os_name == 'foo' or " "os_name == 'bar')", + "python_version ~= '2.7.0' and (os_name == 'foo' or os_name == 'bar')", {"os_name": "bar", "python_version": "2.7.4"}, True, ), ( - "python_version ~= '2.7.0' and (os_name == 'foo' or " "os_name == 'bar')", + "python_version ~= '2.7.0' and (os_name == 'foo' or os_name == 'bar')", {"os_name": "other", "python_version": "2.7.4"}, False, ), @@ -48,10 +48,9 @@ ], ) def test_evaluates( - marker_string: str, environment: dict[str, str], expected: bool + marker_string: str, environment: dict[str, str | set[str]], expected: bool ) -> None: - args = [] if environment is None else [environment] - assert parse_marker(marker_string).evaluate(*args) == expected + assert parse_marker(marker_string).evaluate(environment) == expected @pytest.mark.parametrize( @@ -103,6 +102,7 @@ {"platform_machine": "x86_64"}, False, ), + ("platform_release >= '6'", {"platform_release": "6.1-foobar"}, True), # extras # single extra ("extra != 'security'", {"extra": "quux"}, True), @@ -144,7 +144,7 @@ ], ) def test_evaluate_extra( - marker_string: str, environment: dict[str, str] | None, expected: bool + marker_string: str, environment: dict[str, str | set[str]] | None, expected: bool ) -> None: m = parse_marker(marker_string) @@ -160,7 +160,43 @@ ) ], ) -def test_parse_version_like_markers(marker: str, env: dict[str, str]) -> None: +def test_parse_version_like_markers( + marker: str, env: dict[str, str | set[str]] +) -> None: m = parse_marker(marker) assert m.evaluate(env) + + +@pytest.mark.parametrize("variable", ["extras", "dependency_groups"]) +@pytest.mark.parametrize( + "expression,result", + [ + pytest.param('"foo" in {0}', True, id="value-in-foo"), + pytest.param('"bar" in {0}', True, id="value-in-bar"), + pytest.param('"baz" in {0}', False, id="value-not-in"), + pytest.param('"baz" not in {0}', True, id="value-not-in-negated"), + pytest.param('"foo" in {0} and "bar" in {0}', True, id="and-in"), + pytest.param('"foo" in {0} or "bar" in {0}', True, id="or-in"), + pytest.param('"baz" in {0} and "foo" in {0}', False, id="short-circuit-and"), + pytest.param('"foo" in {0} or "baz" in {0}', True, id="short-circuit-or"), + pytest.param('"Foo" in {0}', True, id="case-sensitive"), + ], +) +def test_extras_and_dependency_groups( + variable: str, expression: str, result: bool +) -> None: + environment: dict[str, str | set[str]] = {variable: {"foo", "bar"}} + assert parse_marker(expression.format(variable)).evaluate(environment) == result + + +@pytest.mark.parametrize("variable", ["extras", "dependency_groups"]) +def test_extras_and_dependency_groups_disallowed(variable: str) -> None: + marker = parse_marker(f'"foo" in {variable}') + assert not marker.evaluate(context="lock_file") + + with pytest.raises(KeyError): + marker.evaluate() + + with pytest.raises(KeyError): + marker.evaluate(context="requirement")