codekingpro/portable-devtools
114k
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 5import operator6import os7import platform8import sys9from typing import Any, Callable, Dict, List, Optional, Tuple, Union10 11from ._parser import (12 MarkerAtom,13 MarkerList,14 Op,15 Value,16 Variable,17 parse_marker as _parse_marker,18)19from ._tokenizer import ParserSyntaxError20from .specifiers import InvalidSpecifier, Specifier21from .utils import canonicalize_name22 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 the49 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, rhs66 return results67 68 69def _format_marker(70 marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True71) -> str:72 73 assert isinstance(marker, (list, tuple, str))74 75 # Sometimes we have a structure like [[...]] which is a single item list76 # where the single item is itself it's own list. In that case we want skip77 # the rest of this function so that we don't get extraneous () on the78 # outside.79 if (80 isinstance(marker, list)81 and len(marker) == 182 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 marker96 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 pass115 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 dependencies127 # https://peps.python.org/pep-0685/128 # > When comparing extra names, tools MUST normalize the names being129 # > compared using the semantics outlined in PEP 503 for names130 if key == "extra":131 return tuple(canonicalize_name(v) for v in values)132 133 # other environment markers don't have such standards134 return values135 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 = marker147 148 if isinstance(lhs, Variable):149 environment_key = lhs.value150 lhs_value = environment[environment_key]151 rhs_value = rhs.value152 else:153 lhs_value = lhs.value154 environment_key = rhs.value155 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.releaselevel170 if kind != "final":171 version += kind[0] + str(info.serial)172 return version173 174 175def default_environment() -> Dict[str, str]:176 iver = format_full_version(sys.implementation.version)177 implementation_name = sys.implementation.name178 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 in196 # packaging.requirements.Requirement. If any additional logic is197 # 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 e218 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 NotImplemented231 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 the238 environment. environment is an optional argument to override all or239 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 this248 # 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 