| |
| |
| |
|
|
| from __future__ import annotations |
|
|
| import operator |
| import os |
| import platform |
| import sys |
| from typing import AbstractSet, Any, Callable, Literal, TypedDict, Union, cast |
|
|
| from ._parser import MarkerAtom, MarkerList, Op, Value, Variable |
| from ._parser import parse_marker as _parse_marker |
| from ._tokenizer import ParserSyntaxError |
| from .specifiers import InvalidSpecifier, Specifier |
| from .utils import canonicalize_name |
|
|
| __all__ = [ |
| "EvaluateContext", |
| "InvalidMarker", |
| "Marker", |
| "UndefinedComparison", |
| "UndefinedEnvironmentName", |
| "default_environment", |
| ] |
|
|
| Operator = Callable[[str, Union[str, AbstractSet[str]]], bool] |
| EvaluateContext = Literal["metadata", "lock_file", "requirement"] |
| MARKERS_ALLOWING_SET = {"extras", "dependency_groups"} |
|
|
|
|
| class InvalidMarker(ValueError): |
| """ |
| An invalid marker was found, users should refer to PEP 508. |
| """ |
|
|
|
|
| class UndefinedComparison(ValueError): |
| """ |
| An invalid operation was attempted on a value that doesn't support it. |
| """ |
|
|
|
|
| class UndefinedEnvironmentName(ValueError): |
| """ |
| A name was attempted to be used that does not exist inside of the |
| environment. |
| """ |
|
|
|
|
| class Environment(TypedDict): |
| implementation_name: str |
| """The implementation's identifier, e.g. ``'cpython'``.""" |
|
|
| implementation_version: str |
| """ |
| The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or |
| ``'7.3.13'`` for PyPy3.10 v7.3.13. |
| """ |
|
|
| os_name: str |
| """ |
| The value of :py:data:`os.name`. The name of the operating system dependent module |
| imported, e.g. ``'posix'``. |
| """ |
|
|
| platform_machine: str |
| """ |
| Returns the machine type, e.g. ``'i386'``. |
| |
| An empty string if the value cannot be determined. |
| """ |
|
|
| platform_release: str |
| """ |
| The system's release, e.g. ``'2.2.0'`` or ``'NT'``. |
| |
| An empty string if the value cannot be determined. |
| """ |
|
|
| platform_system: str |
| """ |
| The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``. |
| |
| An empty string if the value cannot be determined. |
| """ |
|
|
| platform_version: str |
| """ |
| The system's release version, e.g. ``'#3 on degas'``. |
| |
| An empty string if the value cannot be determined. |
| """ |
|
|
| python_full_version: str |
| """ |
| The Python version as string ``'major.minor.patchlevel'``. |
| |
| Note that unlike the Python :py:data:`sys.version`, this value will always include |
| the patchlevel (it defaults to 0). |
| """ |
|
|
| platform_python_implementation: str |
| """ |
| A string identifying the Python implementation, e.g. ``'CPython'``. |
| """ |
|
|
| python_version: str |
| """The Python version as string ``'major.minor'``.""" |
|
|
| sys_platform: str |
| """ |
| This string contains a platform identifier that can be used to append |
| platform-specific components to :py:data:`sys.path`, for instance. |
| |
| For Unix systems, except on Linux and AIX, this is the lowercased OS name as |
| returned by ``uname -s`` with the first part of the version as returned by |
| ``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python |
| was built. |
| """ |
|
|
|
|
| def _normalize_extra_values(results: Any) -> Any: |
| """ |
| Normalize extra values. |
| """ |
| if isinstance(results[0], tuple): |
| lhs, op, rhs = results[0] |
| if isinstance(lhs, Variable) and lhs.value == "extra": |
| normalized_extra = canonicalize_name(rhs.value) |
| rhs = Value(normalized_extra) |
| elif isinstance(rhs, Variable) and rhs.value == "extra": |
| normalized_extra = canonicalize_name(lhs.value) |
| lhs = Value(normalized_extra) |
| results[0] = lhs, op, rhs |
| return results |
|
|
|
|
| def _format_marker( |
| marker: list[str] | MarkerAtom | str, first: bool | None = True |
| ) -> str: |
| assert isinstance(marker, (list, tuple, str)) |
|
|
| |
| |
| |
| |
| if ( |
| isinstance(marker, list) |
| and len(marker) == 1 |
| and isinstance(marker[0], (list, tuple)) |
| ): |
| return _format_marker(marker[0]) |
|
|
| if isinstance(marker, list): |
| inner = (_format_marker(m, first=False) for m in marker) |
| if first: |
| return " ".join(inner) |
| else: |
| return "(" + " ".join(inner) + ")" |
| elif isinstance(marker, tuple): |
| return " ".join([m.serialize() for m in marker]) |
| else: |
| return marker |
|
|
|
|
| _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, |
| } |
|
|
|
|
| def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str]) -> bool: |
| if isinstance(rhs, str): |
| try: |
| spec = Specifier("".join([op.serialize(), rhs])) |
| except InvalidSpecifier: |
| pass |
| else: |
| return spec.contains(lhs, prereleases=True) |
|
|
| oper: Operator | None = _operators.get(op.serialize()) |
| if oper is None: |
| raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") |
|
|
| return oper(lhs, rhs) |
|
|
|
|
| def _normalize( |
| lhs: str, rhs: str | AbstractSet[str], key: str |
| ) -> tuple[str, str | AbstractSet[str]]: |
| |
| |
| |
| |
| if key == "extra": |
| assert isinstance(rhs, str), "extra value must be a string" |
| return (canonicalize_name(lhs), canonicalize_name(rhs)) |
| if key in MARKERS_ALLOWING_SET: |
| if isinstance(rhs, str): |
| return (canonicalize_name(lhs), canonicalize_name(rhs)) |
| else: |
| return (canonicalize_name(lhs), {canonicalize_name(v) for v in rhs}) |
|
|
| |
| return lhs, rhs |
|
|
|
|
| def _evaluate_markers( |
| markers: MarkerList, environment: dict[str, str | AbstractSet[str]] |
| ) -> bool: |
| groups: list[list[bool]] = [[]] |
|
|
| for marker in markers: |
| assert isinstance(marker, (list, tuple, str)) |
|
|
| if isinstance(marker, list): |
| groups[-1].append(_evaluate_markers(marker, environment)) |
| elif isinstance(marker, tuple): |
| lhs, op, rhs = marker |
|
|
| if isinstance(lhs, Variable): |
| environment_key = lhs.value |
| lhs_value = environment[environment_key] |
| rhs_value = rhs.value |
| else: |
| lhs_value = lhs.value |
| environment_key = rhs.value |
| rhs_value = environment[environment_key] |
| assert isinstance(lhs_value, str), "lhs must be a string" |
| lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) |
| groups[-1].append(_eval_op(lhs_value, op, rhs_value)) |
| else: |
| assert marker in ["and", "or"] |
| if marker == "or": |
| groups.append([]) |
|
|
| return any(all(item) for item in groups) |
|
|
|
|
| def format_full_version(info: sys._version_info) -> str: |
| version = f"{info.major}.{info.minor}.{info.micro}" |
| kind = info.releaselevel |
| if kind != "final": |
| version += kind[0] + str(info.serial) |
| return version |
|
|
|
|
| def default_environment() -> Environment: |
| iver = format_full_version(sys.implementation.version) |
| implementation_name = sys.implementation.name |
| return { |
| "implementation_name": implementation_name, |
| "implementation_version": iver, |
| "os_name": os.name, |
| "platform_machine": platform.machine(), |
| "platform_release": platform.release(), |
| "platform_system": platform.system(), |
| "platform_version": platform.version(), |
| "python_full_version": platform.python_version(), |
| "platform_python_implementation": platform.python_implementation(), |
| "python_version": ".".join(platform.python_version_tuple()[:2]), |
| "sys_platform": sys.platform, |
| } |
|
|
|
|
| class Marker: |
| def __init__(self, marker: str) -> None: |
| |
| |
| |
| try: |
| self._markers = _normalize_extra_values(_parse_marker(marker)) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| except ParserSyntaxError as e: |
| raise InvalidMarker(str(e)) from e |
|
|
| def __str__(self) -> str: |
| return _format_marker(self._markers) |
|
|
| def __repr__(self) -> str: |
| return f"<Marker('{self}')>" |
|
|
| def __hash__(self) -> int: |
| return hash((self.__class__.__name__, str(self))) |
|
|
| def __eq__(self, other: Any) -> bool: |
| if not isinstance(other, Marker): |
| return NotImplemented |
|
|
| return str(self) == str(other) |
|
|
| def evaluate( |
| self, |
| environment: dict[str, str] | None = None, |
| context: EvaluateContext = "metadata", |
| ) -> bool: |
| """Evaluate a marker. |
| |
| Return the boolean from evaluating the given marker against the |
| environment. environment is an optional argument to override all or |
| part of the determined environment. The *context* parameter specifies what |
| context the markers are being evaluated for, which influences what markers |
| are considered valid. Acceptable values are "metadata" (for core metadata; |
| default), "lock_file", and "requirement" (i.e. all other situations). |
| |
| The environment is determined from the current Python process. |
| """ |
| current_environment = cast( |
| "dict[str, str | AbstractSet[str]]", default_environment() |
| ) |
| if context == "lock_file": |
| current_environment.update( |
| extras=frozenset(), dependency_groups=frozenset() |
| ) |
| elif context == "metadata": |
| current_environment["extra"] = "" |
| if environment is not None: |
| current_environment.update(environment) |
| |
| |
| if "extra" in current_environment and current_environment["extra"] is None: |
| current_environment["extra"] = "" |
|
|
| return _evaluate_markers( |
| self._markers, _repair_python_full_version(current_environment) |
| ) |
|
|
|
|
| def _repair_python_full_version( |
| env: dict[str, str | AbstractSet[str]], |
| ) -> dict[str, str | AbstractSet[str]]: |
| """ |
| Work around platform.python_version() returning something that is not PEP 440 |
| compliant for non-tagged Python builds. |
| """ |
| python_full_version = cast(str, env["python_full_version"]) |
| if python_full_version.endswith("+"): |
| env["python_full_version"] = f"{python_full_version}local" |
| return env |
|
|