Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
markers.py493 linesDownload Raw Back to packaging
1# This file is dual licensed under the terms of the Apache License, Version2# 2.0, and the BSD License. See the LICENSE file in the root of this repository3# for complete details.4 5from __future__ import annotations6 7import operator8import os9import platform10import sys11from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast12 13from ._parser import MarkerAtom, MarkerList, Op, Value, Variable14from ._parser import parse_marker as _parse_marker15from ._tokenizer import ParserSyntaxError16from .specifiers import InvalidSpecifier, Specifier17from .utils import canonicalize_name18 19__all__ = [20    "Environment",21    "EvaluateContext",22    "InvalidMarker",23    "Marker",24    "UndefinedComparison",25    "UndefinedEnvironmentName",26    "default_environment",27]28 29 30def __dir__() -> list[str]:31    return __all__32 33 34Operator = Callable[[str, Union[str, AbstractSet[str]]], bool]35EvaluateContext = Literal["metadata", "lock_file", "requirement"]36"""A ``typing.Literal`` enumerating valid marker evaluation contexts.37 38Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:39 40* ``"metadata"`` (for core metadata; default)41* ``"lock_file"`` (for lock files)42* ``"requirement"`` (i.e. all other situations)43"""44 45MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}46MARKERS_REQUIRING_VERSION = {47    "implementation_version",48    "platform_release",49    "python_full_version",50    "python_version",51}52 53 54class InvalidMarker(ValueError):55    """Raised when attempting to create a :class:`Marker` from invalid input.56 57    This error indicates that the given marker string does not conform to the58    :ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.59    """60 61 62class UndefinedComparison(ValueError):63    """Raised when evaluating an unsupported marker comparison.64 65    This can happen when marker values are compared as versions but do not66    conform to the :ref:`specification of version specifiers67    <pypug:version-specifiers>`.68    """69 70 71class UndefinedEnvironmentName(ValueError):72    """Raised when evaluating a marker that references a missing environment key."""73 74 75class Environment(TypedDict):76    """77    A dictionary that represents a Python environment as captured by78    :func:`default_environment`. All fields are required.79    """80 81    implementation_name: str82    """The implementation's identifier, e.g. ``'cpython'``."""83 84    implementation_version: str85    """86    The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or87    ``'7.3.13'`` for PyPy3.10 v7.3.13.88    """89 90    os_name: str91    """92    The value of :py:data:`os.name`. The name of the operating system dependent module93    imported, e.g. ``'posix'``.94    """95 96    platform_machine: str97    """98    Returns the machine type, e.g. ``'i386'``.99 100    An empty string if the value cannot be determined.101    """102 103    platform_release: str104    """105    The system's release, e.g. ``'2.2.0'`` or ``'NT'``.106 107    An empty string if the value cannot be determined.108    """109 110    platform_system: str111    """112    The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``.113 114    An empty string if the value cannot be determined.115    """116 117    platform_version: str118    """119    The system's release version, e.g. ``'#3 on degas'``.120 121    An empty string if the value cannot be determined.122    """123 124    python_full_version: str125    """126    The Python version as string ``'major.minor.patchlevel'``.127 128    Note that unlike the Python :py:data:`sys.version`, this value will always include129    the patchlevel (it defaults to 0).130    """131 132    platform_python_implementation: str133    """134    A string identifying the Python implementation, e.g. ``'CPython'``.135    """136 137    python_version: str138    """The Python version as string ``'major.minor'``."""139 140    sys_platform: str141    """142    This string contains a platform identifier that can be used to append143    platform-specific components to :py:data:`sys.path`, for instance.144 145    For Unix systems, except on Linux and AIX, this is the lowercased OS name as146    returned by ``uname -s`` with the first part of the version as returned by147    ``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python148    was built.149    """150 151 152def _normalize_extras(153    result: MarkerList | MarkerAtom | str,154) -> MarkerList | MarkerAtom | str:155    if not isinstance(result, tuple):156        return result157 158    lhs, op, rhs = result159    if isinstance(lhs, Variable) and lhs.value == "extra":160        normalized_extra = canonicalize_name(rhs.value)161        rhs = Value(normalized_extra)162    elif isinstance(rhs, Variable) and rhs.value == "extra":163        normalized_extra = canonicalize_name(lhs.value)164        lhs = Value(normalized_extra)165    return lhs, op, rhs166 167 168def _normalize_extra_values(results: MarkerList) -> MarkerList:169    """170    Normalize extra values.171    """172 173    return [_normalize_extras(r) for r in results]174 175 176def _format_marker(177    marker: list[str] | MarkerAtom | str, first: bool | None = True178) -> str:179    assert isinstance(marker, (list, tuple, str))180 181    # Sometimes we have a structure like [[...]] which is a single item list182    # where the single item is itself it's own list. In that case we want skip183    # the rest of this function so that we don't get extraneous () on the184    # outside.185    if (186        isinstance(marker, list)187        and len(marker) == 1188        and isinstance(marker[0], (list, tuple))189    ):190        return _format_marker(marker[0])191 192    if isinstance(marker, list):193        inner = (_format_marker(m, first=False) for m in marker)194        if first:195            return " ".join(inner)196        else:197            return "(" + " ".join(inner) + ")"198    elif isinstance(marker, tuple):199        return " ".join([m.serialize() for m in marker])200    else:201        return marker202 203 204_operators: dict[str, Operator] = {205    "in": lambda lhs, rhs: lhs in rhs,206    "not in": lambda lhs, rhs: lhs not in rhs,207    "<": lambda _lhs, _rhs: False,208    "<=": operator.eq,209    "==": operator.eq,210    "!=": operator.ne,211    ">=": operator.eq,212    ">": lambda _lhs, _rhs: False,213}214 215 216def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool:217    op_str = op.serialize()218    if key in MARKERS_REQUIRING_VERSION:219        try:220            spec = Specifier(f"{op_str}{rhs}")221        except InvalidSpecifier:222            pass223        else:224            return spec.contains(lhs, prereleases=True)225 226    oper: Operator | None = _operators.get(op_str)227    if oper is None:228        raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")229 230    return oper(lhs, rhs)231 232 233def _normalize(234    lhs: str, rhs: str | AbstractSet[str], key: str235) -> tuple[str, str | AbstractSet[str]]:236    # PEP 685 - Comparison of extra names for optional distribution dependencies237    # https://peps.python.org/pep-0685/238    # > When comparing extra names, tools MUST normalize the names being239    # > compared using the semantics outlined in PEP 503 for names240    if key == "extra":241        assert isinstance(rhs, str), "extra value must be a string"242        # Both sides are normalized at this point already243        return (lhs, rhs)244    if key in MARKERS_ALLOWING_SET:245        if isinstance(rhs, str):  # pragma: no cover246            return (canonicalize_name(lhs), canonicalize_name(rhs))247        else:248            return (canonicalize_name(lhs), {canonicalize_name(v) for v in rhs})249 250    # other environment markers don't have such standards251    return lhs, rhs252 253 254def _evaluate_markers(255    markers: MarkerList, environment: dict[str, str | AbstractSet[str]]256) -> bool:257    groups: list[list[bool]] = [[]]258 259    for marker in markers:260        if isinstance(marker, list):261            groups[-1].append(_evaluate_markers(marker, environment))262        elif isinstance(marker, tuple):263            lhs, op, rhs = marker264 265            if isinstance(lhs, Variable):266                environment_key = lhs.value267                lhs_value = environment[environment_key]268                rhs_value = rhs.value269            else:270                lhs_value = lhs.value271                environment_key = rhs.value272                rhs_value = environment[environment_key]273 274            assert isinstance(lhs_value, str), "lhs must be a string"275            lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)276            groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))277        elif marker == "or":278            groups.append([])279        elif marker == "and":280            pass281        else:  # pragma: nocover282            raise TypeError(f"Unexpected marker {marker!r}")283 284    return any(all(item) for item in groups)285 286 287def _format_full_version(info: sys._version_info) -> str:288    version = f"{info.major}.{info.minor}.{info.micro}"289    kind = info.releaselevel290    if kind != "final":291        version += kind[0] + str(info.serial)292    return version293 294 295def default_environment() -> Environment:296    """Return the default marker environment for the current Python process.297 298    This is the base environment used by :meth:`Marker.evaluate`.299    """300    iver = _format_full_version(sys.implementation.version)301    implementation_name = sys.implementation.name302    return {303        "implementation_name": implementation_name,304        "implementation_version": iver,305        "os_name": os.name,306        "platform_machine": platform.machine(),307        "platform_release": platform.release(),308        "platform_system": platform.system(),309        "platform_version": platform.version(),310        "python_full_version": platform.python_version(),311        "platform_python_implementation": platform.python_implementation(),312        "python_version": ".".join(platform.python_version_tuple()[:2]),313        "sys_platform": sys.platform,314    }315 316 317class Marker:318    """Represents a parsed dependency marker expression.319 320    Marker expressions are parsed according to the321    :ref:`specification of dependency specifiers <pypug:dependency-specifiers>`.322 323    :param marker: The string representation of a marker expression.324    :raises InvalidMarker: If ``marker`` cannot be parsed.325 326    Instances are safe to serialize with :mod:`pickle`. They use a stable327    format so the same pickle can be loaded in future packaging releases.328 329    .. versionchanged:: 26.2330 331        Added a stable pickle format. Pickles created with packaging 26.2+ can332        be unpickled with future releases.  Backward compatibility with pickles333        from packaging < 26.2 is supported but may be removed in a future334        release.335    """336 337    __slots__ = ("_markers",)338 339    def __init__(self, marker: str) -> None:340        # Note: We create a Marker object without calling this constructor in341        #       packaging.requirements.Requirement. If any additional logic is342        #       added here, make sure to mirror/adapt Requirement.343 344        # If this fails and throws an error, the repr still expects _markers to345        # be defined.346        self._markers: MarkerList = []347 348        try:349            self._markers = _normalize_extra_values(_parse_marker(marker))350            # The attribute `_markers` can be described in terms of a recursive type:351            # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]]352            #353            # For example, the following expression:354            # python_version > "3.6" or (python_version == "3.6" and os_name == "unix")355            #356            # is parsed into:357            # [358            #     (<Variable('python_version')>, <Op('>')>, <Value('3.6')>),359            #     'and',360            #     [361            #         (<Variable('python_version')>, <Op('==')>, <Value('3.6')>),362            #         'or',363            #         (<Variable('os_name')>, <Op('==')>, <Value('unix')>)364            #     ]365            # ]366        except ParserSyntaxError as e:367            raise InvalidMarker(str(e)) from e368 369    @classmethod370    def _from_markers(cls, markers: MarkerList) -> Marker:371        """Create a Marker instance from a pre-parsed marker tree.372 373        This avoids re-parsing serialised marker strings when combining markers.374        """375        new = cls.__new__(cls)376        new._markers = markers377        return new378 379    def __str__(self) -> str:380        return _format_marker(self._markers)381 382    def __repr__(self) -> str:383        return f"<{self.__class__.__name__}({str(self)!r})>"384 385    def __hash__(self) -> int:386        return hash(str(self))387 388    def __eq__(self, other: object) -> bool:389        if not isinstance(other, Marker):390            return NotImplemented391 392        return str(self) == str(other)393 394    def __getstate__(self) -> str:395        # Return the marker expression string for compactness and stability.396        # Internal Node objects are excluded; the string is re-parsed on load.397        return str(self)398 399    def __setstate__(self, state: object) -> None:400        if isinstance(state, str):401            # New format (26.2+): just the marker expression string.402            try:403                self._markers = _normalize_extra_values(_parse_marker(state))404            except ParserSyntaxError as exc:405                raise TypeError(f"Cannot restore Marker from {state!r}") from exc406            return407        if isinstance(state, dict) and "_markers" in state:408            # Old format (packaging <= 26.1, no __slots__): plain __dict__.409            markers = state["_markers"]410            if isinstance(markers, list):411                self._markers = markers412                return413        if isinstance(state, tuple) and len(state) == 2:414            # Old format (packaging <= 26.1, __slots__): (None, {slot: value}).415            _, slot_dict = state416            if isinstance(slot_dict, dict) and "_markers" in slot_dict:417                markers = slot_dict["_markers"]418                if isinstance(markers, list):419                    self._markers = markers420                    return421        raise TypeError(f"Cannot restore Marker from {state!r}")422 423    def __and__(self, other: Marker) -> Marker:424        if not isinstance(other, Marker):425            return NotImplemented426        return self._from_markers([self._markers, "and", other._markers])427 428    def __or__(self, other: Marker) -> Marker:429        if not isinstance(other, Marker):430            return NotImplemented431        return self._from_markers([self._markers, "or", other._markers])432 433    def evaluate(434        self,435        environment: Mapping[str, str | AbstractSet[str]] | None = None,436        context: EvaluateContext = "metadata",437    ) -> bool:438        """Evaluate a marker.439 440        Return the boolean from evaluating this marker against the environment.441        The environment is determined from the current Python process unless442        passed in explicitly.443 444        :param environment: Mapping containing keys and values to override the445           detected environment.446        :param EvaluateContext context: The context in which the marker is447            evaluated, which influences what marker names are considered valid.448            Accepted values are ``"metadata"`` (for core metadata; default),449            ``"lock_file"``, and ``"requirement"`` (i.e. all other situations).450        :raises UndefinedComparison: If the marker uses a comparison on values451            that are not valid versions per the :ref:`specification of version452            specifiers <pypug:version-specifiers>`.453        :raises UndefinedEnvironmentName: If the marker references a value that454            is missing from the evaluation environment.455        :returns: ``True`` if the marker matches, otherwise ``False``.456 457        """458        current_environment = cast(459            "dict[str, str | AbstractSet[str]]", default_environment()460        )461        if context == "lock_file":462            current_environment.update(463                extras=frozenset(), dependency_groups=frozenset()464            )465        elif context == "metadata":466            current_environment["extra"] = ""467 468        if environment is not None:469            current_environment.update(environment)470            if "extra" in current_environment:471                # The API used to allow setting extra to None. We need to handle472                # this case for backwards compatibility. Also skip running473                # normalize name if extra is empty.474                extra = cast("str | None", current_environment["extra"])475                current_environment["extra"] = canonicalize_name(extra) if extra else ""476 477        return _evaluate_markers(478            self._markers, _repair_python_full_version(current_environment)479        )480 481 482def _repair_python_full_version(483    env: dict[str, str | AbstractSet[str]],484) -> dict[str, str | AbstractSet[str]]:485    """486    Work around platform.python_version() returning something that is not PEP 440487    compliant for non-tagged Python builds.488    """489    python_full_version = cast("str", env["python_full_version"])490    if python_full_version.endswith("+"):491        env["python_full_version"] = f"{python_full_version}local"492    return env493 
codekingpro/portable-devtools · Team Ai