]>
Commit | Line | Data |
---|---|---|
53e6db90 DC |
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 | ||
5 | import operator | |
6 | import os | |
7 | import platform | |
8 | import sys | |
9 | from typing import Any, Callable, Dict, List, Optional, Tuple, Union | |
10 | ||
11 | from ._parser import ( | |
12 | MarkerAtom, | |
13 | MarkerList, | |
14 | Op, | |
15 | Value, | |
16 | Variable, | |
17 | parse_marker as _parse_marker, | |
18 | ) | |
19 | from ._tokenizer import ParserSyntaxError | |
20 | from .specifiers import InvalidSpecifier, Specifier | |
21 | from .utils import canonicalize_name | |
22 | ||
23 | __all__ = [ | |
24 | "InvalidMarker", | |
25 | "UndefinedComparison", | |
26 | "UndefinedEnvironmentName", | |
27 | "Marker", | |
28 | "default_environment", | |
29 | ] | |
30 | ||
31 | Operator = Callable[[str, str], bool] | |
32 | ||
33 | ||
34 | class InvalidMarker(ValueError): | |
35 | """ | |
36 | An invalid marker was found, users should refer to PEP 508. | |
37 | """ | |
38 | ||
39 | ||
40 | class UndefinedComparison(ValueError): | |
41 | """ | |
42 | An invalid operation was attempted on a value that doesn't support it. | |
43 | """ | |
44 | ||
45 | ||
46 | class UndefinedEnvironmentName(ValueError): | |
47 | """ | |
48 | A name was attempted to be used that does not exist inside of the | |
49 | environment. | |
50 | """ | |
51 | ||
52 | ||
53 | def _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 | ||
69 | def _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 | ||
110 | def _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 | ||
125 | def _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 | ||
137 | def _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 | ||
167 | def 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 | ||
175 | def 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 | ||
193 | class 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) |