• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
4
5import operator
6import os
7import platform
8import sys
9from typing import Any, Callable, Dict, List, Optional, Tuple, Union
10
11from ._parser import (
12    MarkerAtom,
13    MarkerList,
14    Op,
15    Value,
16    Variable,
17    parse_marker as _parse_marker,
18)
19from ._tokenizer import ParserSyntaxError
20from .specifiers import InvalidSpecifier, Specifier
21from .utils import canonicalize_name
22
23__all__ = [
24    "InvalidMarker",
25    "UndefinedComparison",
26    "UndefinedEnvironmentName",
27    "Marker",
28    "default_environment",
29]
30
31Operator = Callable[[str, str], bool]
32
33
34class InvalidMarker(ValueError):
35    """
36    An invalid marker was found, users should refer to PEP 508.
37    """
38
39
40class UndefinedComparison(ValueError):
41    """
42    An invalid operation was attempted on a value that doesn't support it.
43    """
44
45
46class UndefinedEnvironmentName(ValueError):
47    """
48    A name was attempted to be used that does not exist inside of the
49    environment.
50    """
51
52
53def _normalize_extra_values(results: Any) -> Any:
54    """
55    Normalize extra values.
56    """
57    if isinstance(results[0], tuple):
58        lhs, op, rhs = results[0]
59        if isinstance(lhs, Variable) and lhs.value == "extra":
60            normalized_extra = canonicalize_name(rhs.value)
61            rhs = Value(normalized_extra)
62        elif isinstance(rhs, Variable) and rhs.value == "extra":
63            normalized_extra = canonicalize_name(lhs.value)
64            lhs = Value(normalized_extra)
65        results[0] = lhs, op, rhs
66    return results
67
68
69def _format_marker(
70    marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True
71) -> str:
72
73    assert isinstance(marker, (list, tuple, str))
74
75    # Sometimes we have a structure like [[...]] which is a single item list
76    # where the single item is itself it's own list. In that case we want skip
77    # the rest of this function so that we don't get extraneous () on the
78    # outside.
79    if (
80        isinstance(marker, list)
81        and len(marker) == 1
82        and isinstance(marker[0], (list, tuple))
83    ):
84        return _format_marker(marker[0])
85
86    if isinstance(marker, list):
87        inner = (_format_marker(m, first=False) for m in marker)
88        if first:
89            return " ".join(inner)
90        else:
91            return "(" + " ".join(inner) + ")"
92    elif isinstance(marker, tuple):
93        return " ".join([m.serialize() for m in marker])
94    else:
95        return marker
96
97
98_operators: Dict[str, Operator] = {
99    "in": lambda lhs, rhs: lhs in rhs,
100    "not in": lambda lhs, rhs: lhs not in rhs,
101    "<": operator.lt,
102    "<=": operator.le,
103    "==": operator.eq,
104    "!=": operator.ne,
105    ">=": operator.ge,
106    ">": operator.gt,
107}
108
109
110def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
111    try:
112        spec = Specifier("".join([op.serialize(), rhs]))
113    except InvalidSpecifier:
114        pass
115    else:
116        return spec.contains(lhs, prereleases=True)
117
118    oper: Optional[Operator] = _operators.get(op.serialize())
119    if oper is None:
120        raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
121
122    return oper(lhs, rhs)
123
124
125def _normalize(*values: str, key: str) -> Tuple[str, ...]:
126    # PEP 685 – Comparison of extra names for optional distribution dependencies
127    # https://peps.python.org/pep-0685/
128    # > When comparing extra names, tools MUST normalize the names being
129    # > compared using the semantics outlined in PEP 503 for names
130    if key == "extra":
131        return tuple(canonicalize_name(v) for v in values)
132
133    # other environment markers don't have such standards
134    return values
135
136
137def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool:
138    groups: List[List[bool]] = [[]]
139
140    for marker in markers:
141        assert isinstance(marker, (list, tuple, str))
142
143        if isinstance(marker, list):
144            groups[-1].append(_evaluate_markers(marker, environment))
145        elif isinstance(marker, tuple):
146            lhs, op, rhs = marker
147
148            if isinstance(lhs, Variable):
149                environment_key = lhs.value
150                lhs_value = environment[environment_key]
151                rhs_value = rhs.value
152            else:
153                lhs_value = lhs.value
154                environment_key = rhs.value
155                rhs_value = environment[environment_key]
156
157            lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
158            groups[-1].append(_eval_op(lhs_value, op, rhs_value))
159        else:
160            assert marker in ["and", "or"]
161            if marker == "or":
162                groups.append([])
163
164    return any(all(item) for item in groups)
165
166
167def format_full_version(info: "sys._version_info") -> str:
168    version = "{0.major}.{0.minor}.{0.micro}".format(info)
169    kind = info.releaselevel
170    if kind != "final":
171        version += kind[0] + str(info.serial)
172    return version
173
174
175def default_environment() -> Dict[str, str]:
176    iver = format_full_version(sys.implementation.version)
177    implementation_name = sys.implementation.name
178    return {
179        "implementation_name": implementation_name,
180        "implementation_version": iver,
181        "os_name": os.name,
182        "platform_machine": platform.machine(),
183        "platform_release": platform.release(),
184        "platform_system": platform.system(),
185        "platform_version": platform.version(),
186        "python_full_version": platform.python_version(),
187        "platform_python_implementation": platform.python_implementation(),
188        "python_version": ".".join(platform.python_version_tuple()[:2]),
189        "sys_platform": sys.platform,
190    }
191
192
193class Marker:
194    def __init__(self, marker: str) -> None:
195        # Note: We create a Marker object without calling this constructor in
196        #       packaging.requirements.Requirement. If any additional logic is
197        #       added here, make sure to mirror/adapt Requirement.
198        try:
199            self._markers = _normalize_extra_values(_parse_marker(marker))
200            # The attribute `_markers` can be described in terms of a recursive type:
201            # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]]
202            #
203            # For example, the following expression:
204            # python_version > "3.6" or (python_version == "3.6" and os_name == "unix")
205            #
206            # is parsed into:
207            # [
208            #     (<Variable('python_version')>, <Op('>')>, <Value('3.6')>),
209            #     'and',
210            #     [
211            #         (<Variable('python_version')>, <Op('==')>, <Value('3.6')>),
212            #         'or',
213            #         (<Variable('os_name')>, <Op('==')>, <Value('unix')>)
214            #     ]
215            # ]
216        except ParserSyntaxError as e:
217            raise InvalidMarker(str(e)) from e
218
219    def __str__(self) -> str:
220        return _format_marker(self._markers)
221
222    def __repr__(self) -> str:
223        return f"<Marker('{self}')>"
224
225    def __hash__(self) -> int:
226        return hash((self.__class__.__name__, str(self)))
227
228    def __eq__(self, other: Any) -> bool:
229        if not isinstance(other, Marker):
230            return NotImplemented
231
232        return str(self) == str(other)
233
234    def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool:
235        """Evaluate a marker.
236
237        Return the boolean from evaluating the given marker against the
238        environment. environment is an optional argument to override all or
239        part of the determined environment.
240
241        The environment is determined from the current Python process.
242        """
243        current_environment = default_environment()
244        current_environment["extra"] = ""
245        if environment is not None:
246            current_environment.update(environment)
247            # The API used to allow setting extra to None. We need to handle this
248            # case for backwards compatibility.
249            if current_environment["extra"] is None:
250                current_environment["extra"] = ""
251
252        return _evaluate_markers(self._markers, current_environment)
253