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