Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_vendor/packaging/markers.py: 35%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

213 statements  

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 

5from __future__ import annotations 

6 

7import functools 

8import operator 

9import os 

10import platform 

11import sys 

12from collections.abc import Set as AbstractSet 

13from typing import TYPE_CHECKING, Callable, Literal, TypedDict, Union, cast 

14 

15from ._parser import MarkerAtom, MarkerList, Op, Value, Variable 

16from ._parser import parse_marker as _parse_marker 

17from ._tokenizer import ParserSyntaxError 

18from .specifiers import InvalidSpecifier, Specifier 

19from .utils import canonicalize_name 

20 

21if TYPE_CHECKING: 

22 from collections.abc import Mapping 

23 

24__all__ = [ 

25 "Environment", 

26 "EvaluateContext", 

27 "InvalidMarker", 

28 "Marker", 

29 "UndefinedComparison", 

30 "UndefinedEnvironmentName", 

31 "default_environment", 

32] 

33 

34 

35def __dir__() -> list[str]: 

36 return __all__ 

37 

38 

39Operator = Callable[[str, Union[str, AbstractSet[str]]], bool] 

40EvaluateContext = Literal["metadata", "lock_file", "requirement"] 

41"""A ``typing.Literal`` enumerating valid marker evaluation contexts. 

42 

43Valid values for the ``context`` passed to :meth:`Marker.evaluate` are: 

44 

45* ``"metadata"`` (for core metadata; default) 

46* ``"lock_file"`` (for lock files) 

47* ``"requirement"`` (i.e. all other situations) 

48 

49.. versionadded:: 25.0 

50""" 

51 

52MARKERS_ALLOWING_SET = {"extras", "dependency_groups"} 

53MARKERS_REQUIRING_VERSION = { 

54 "implementation_version", 

55 "platform_release", 

56 "python_full_version", 

57 "python_version", 

58} 

59 

60 

61class InvalidMarker(ValueError): 

62 """Raised when attempting to create a :class:`Marker` from invalid input. 

63 

64 This error indicates that the given marker string does not conform to the 

65 :ref:`specification of dependency specifiers <pypug:dependency-specifiers>`. 

66 """ 

67 

68 

69class UndefinedComparison(ValueError): 

70 """Raised when evaluating an unsupported marker comparison. 

71 

72 This can happen when marker values are compared as versions but do not 

73 conform to the :ref:`specification of version specifiers 

74 <pypug:version-specifiers>`. 

75 """ 

76 

77 

78class UndefinedEnvironmentName(KeyError): 

79 """Raised when evaluating a marker that references a missing environment key. 

80 

81 Subclasses :class:`KeyError` so that code catching the bare ``KeyError`` that 

82 a missing environment lookup historically produced keeps working. 

83 

84 .. versionchanged:: 26.3 

85 Now subclasses :class:`KeyError` (was :class:`ValueError`) and is raised by 

86 :meth:`Marker.evaluate` for missing environment keys, where a bare 

87 ``KeyError`` was raised before. 

88 """ 

89 

90 

91class Environment(TypedDict): 

92 """ 

93 A dictionary that represents a Python environment as captured by 

94 :func:`default_environment`. All fields are required. 

95 """ 

96 

97 implementation_name: str 

98 """The implementation's identifier, e.g. ``'cpython'``.""" 

99 

100 implementation_version: str 

101 """ 

102 The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or 

103 ``'7.3.13'`` for PyPy3.10 v7.3.13. 

104 """ 

105 

106 os_name: str 

107 """ 

108 The value of :py:data:`os.name`. The name of the operating system dependent module 

109 imported, e.g. ``'posix'``. 

110 """ 

111 

112 platform_machine: str 

113 """ 

114 Returns the machine type, e.g. ``'i386'``. 

115 

116 An empty string if the value cannot be determined. 

117 """ 

118 

119 platform_release: str 

120 """ 

121 The system's release, e.g. ``'2.2.0'`` or ``'NT'``. 

122 

123 An empty string if the value cannot be determined. 

124 """ 

125 

126 platform_system: str 

127 """ 

128 The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``. 

129 

130 An empty string if the value cannot be determined. 

131 """ 

132 

133 platform_version: str 

134 """ 

135 The system's release version, e.g. ``'#3 on degas'``. 

136 

137 An empty string if the value cannot be determined. 

138 """ 

139 

140 python_full_version: str 

141 """ 

142 The Python version as string ``'major.minor.patchlevel'``. 

143 

144 Note that unlike the Python :py:data:`sys.version`, this value will always include 

145 the patchlevel (it defaults to 0). 

146 """ 

147 

148 platform_python_implementation: str 

149 """ 

150 A string identifying the Python implementation, e.g. ``'CPython'``. 

151 """ 

152 

153 python_version: str 

154 """The Python version as string ``'major.minor'``.""" 

155 

156 sys_platform: str 

157 """ 

158 This string contains a platform identifier that can be used to append 

159 platform-specific components to :py:data:`sys.path`, for instance. 

160 

161 For Unix systems, except on Linux and AIX, this is the lowercased OS name as 

162 returned by ``uname -s`` with the first part of the version as returned by 

163 ``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python 

164 was built. 

165 """ 

166 

167 

168def _normalize_extras( 

169 result: MarkerList | MarkerAtom | str, 

170) -> MarkerList | MarkerAtom | str: 

171 if isinstance(result, list): 

172 return [_normalize_extras(r) for r in result] 

173 if not isinstance(result, tuple): 

174 return result 

175 

176 lhs, op, rhs = result 

177 if isinstance(lhs, Variable) and lhs.value == "extra" and isinstance(rhs, Value): 

178 normalized_extra = canonicalize_name(rhs.value) 

179 rhs = Value(normalized_extra) 

180 elif isinstance(rhs, Variable) and rhs.value == "extra" and isinstance(lhs, Value): 

181 normalized_extra = canonicalize_name(lhs.value) 

182 lhs = Value(normalized_extra) 

183 elif ( 

184 isinstance(rhs, Variable) 

185 and rhs.value in MARKERS_ALLOWING_SET 

186 and isinstance(lhs, Value) 

187 ): 

188 # PEP 685 (extras) / PEP 735 (dependency_groups): the set-valued membership 

189 # literal must also be normalized. evaluate() already canonicalizes both 

190 # operands for these keys (see _normalize), so normalizing the literal at 

191 # parse time keeps __str__/__eq__/__hash__ consistent with evaluate() -- e.g. 

192 # Marker('"Foo" in extras') and Marker('"foo" in extras') must compare and 

193 # hash equal (the membership variable is always the right-hand operand). 

194 lhs = Value(canonicalize_name(lhs.value)) 

195 return lhs, op, rhs 

196 

197 

198def _normalize_extra_values(results: MarkerList) -> MarkerList: 

199 """ 

200 Normalize extra values. 

201 """ 

202 

203 return [_normalize_extras(r) for r in results] 

204 

205 

206def _format_marker( 

207 marker: list[str] | MarkerAtom | str, first: bool | None = True 

208) -> str: 

209 assert isinstance(marker, (list, tuple, str)) 

210 

211 # Unwrap a redundant [[...]] wrapper, but keep the nesting context so a 

212 # nested group keeps the parentheses its and/or precedence needs. 

213 if ( 

214 isinstance(marker, list) 

215 and len(marker) == 1 

216 and isinstance(marker[0], (list, tuple)) 

217 ): 

218 return _format_marker(marker[0], first=first) 

219 

220 if isinstance(marker, list): 

221 inner = (_format_marker(m, first=False) for m in marker) 

222 if first: 

223 return " ".join(inner) 

224 else: 

225 return "(" + " ".join(inner) + ")" 

226 elif isinstance(marker, tuple): 

227 return " ".join([m.serialize() for m in marker]) 

228 else: 

229 return marker 

230 

231 

232_operators: dict[str, Operator] = { 

233 "in": lambda lhs, rhs: lhs in rhs, 

234 "not in": lambda lhs, rhs: lhs not in rhs, 

235 "<": lambda _lhs, _rhs: False, 

236 "<=": operator.eq, 

237 "==": operator.eq, 

238 "!=": operator.ne, 

239 ">=": operator.eq, 

240 ">": lambda _lhs, _rhs: False, 

241} 

242 

243 

244def _eval_op(lhs: str, op: Op, rhs: str | AbstractSet[str], *, key: str) -> bool: 

245 op_str = op.serialize() 

246 if key in MARKERS_REQUIRING_VERSION: 

247 try: 

248 spec = Specifier(f"{op_str}{rhs}") 

249 except InvalidSpecifier: 

250 pass 

251 else: 

252 return spec.contains(lhs, prereleases=True) 

253 

254 oper: Operator | None = _operators.get(op_str) 

255 if oper is None: 

256 raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") 

257 

258 return oper(lhs, rhs) 

259 

260 

261def _normalize( 

262 lhs: str, rhs: str | AbstractSet[str], key: str 

263) -> tuple[str, str | AbstractSet[str]]: 

264 # PEP 685 - Comparison of extra names for optional distribution dependencies 

265 # https://peps.python.org/pep-0685/ 

266 # > When comparing extra names, tools MUST normalize the names being 

267 # > compared using the semantics outlined in PEP 503 for names 

268 if key == "extra": 

269 assert isinstance(rhs, str), "extra value must be a string" 

270 # Both sides are normalized at this point already 

271 return (lhs, rhs) 

272 if key in MARKERS_ALLOWING_SET: 

273 if isinstance(rhs, str): # pragma: no cover 

274 return (canonicalize_name(lhs), canonicalize_name(rhs)) 

275 else: 

276 return (canonicalize_name(lhs), {canonicalize_name(v) for v in rhs}) 

277 

278 # other environment markers don't have such standards 

279 return lhs, rhs 

280 

281 

282def _lookup_environment( 

283 environment: dict[str, str | AbstractSet[str]], key: str 

284) -> str | AbstractSet[str]: 

285 try: 

286 return environment[key] 

287 except KeyError: 

288 raise UndefinedEnvironmentName(key) from None 

289 

290 

291def _evaluate_markers( 

292 markers: MarkerList, environment: dict[str, str | AbstractSet[str]] 

293) -> bool: 

294 groups: list[list[bool]] = [[]] 

295 

296 for marker in markers: 

297 if isinstance(marker, list): 

298 groups[-1].append(_evaluate_markers(marker, environment)) 

299 elif isinstance(marker, tuple): 

300 lhs, op, rhs = marker 

301 

302 if isinstance(lhs, Variable): 

303 environment_key = lhs.value 

304 lhs_value = _lookup_environment(environment, environment_key) 

305 rhs_value = rhs.value 

306 else: 

307 lhs_value = lhs.value 

308 environment_key = rhs.value 

309 rhs_value = _lookup_environment(environment, environment_key) 

310 

311 if not isinstance(lhs_value, str): 

312 raise UndefinedComparison( 

313 f"Set-valued marker {environment_key!r} can only be used " 

314 f'with the membership form (e.g. "<name>" in ' 

315 f"{environment_key}); it cannot appear on the left-hand " 

316 f"side of {op.serialize()!r}." 

317 ) 

318 lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) 

319 groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key)) 

320 elif marker == "or": 

321 groups.append([]) 

322 elif marker == "and": 

323 pass 

324 else: # pragma: nocover 

325 raise TypeError(f"Unexpected marker {marker!r}") 

326 

327 return any(all(item) for item in groups) 

328 

329 

330def _format_full_version(info: sys._version_info) -> str: 

331 version = f"{info.major}.{info.minor}.{info.micro}" 

332 kind = info.releaselevel 

333 if kind != "final": 

334 version += kind[0] + str(info.serial) 

335 return version 

336 

337 

338@functools.cache 

339def _cached_default_environment() -> Environment: 

340 """Build the default marker environment for the current Python process. 

341 

342 The values are derived from process-constant data (the running interpreter 

343 and the host platform), so this is cached and built only once. The result is 

344 shared between callers and must never be mutated; :func:`default_environment` 

345 returns a fresh copy. 

346 """ 

347 iver = _format_full_version(sys.implementation.version) 

348 implementation_name = sys.implementation.name 

349 return { 

350 "implementation_name": implementation_name, 

351 "implementation_version": iver, 

352 "os_name": os.name, 

353 "platform_machine": platform.machine(), 

354 "platform_release": platform.release(), 

355 "platform_system": platform.system(), 

356 "platform_version": platform.version(), 

357 "python_full_version": platform.python_version(), 

358 "platform_python_implementation": platform.python_implementation(), 

359 "python_version": ".".join(platform.python_version_tuple()[:2]), 

360 "sys_platform": sys.platform, 

361 } 

362 

363 

364def default_environment() -> Environment: 

365 """Return the default marker environment for the current Python process. 

366 

367 This is the base environment used by :meth:`Marker.evaluate`. A fresh copy 

368 is returned on every call so callers may freely mutate the result; a shallow 

369 copy suffices because all values are immutable strings. 

370 

371 .. versionchanged:: 26.3 

372 The environment is computed once per process and cached, since it is 

373 derived from process-constant data. Patching ``platform``/``sys``/``os`` 

374 after the first call has no effect; pass an explicit ``environment`` to 

375 :meth:`Marker.evaluate` to evaluate against different values. 

376 """ 

377 return cast("Environment", dict(_cached_default_environment())) 

378 

379 

380class Marker: 

381 """Represents a parsed dependency marker expression. 

382 

383 Marker expressions are parsed according to the 

384 :ref:`specification of dependency specifiers <pypug:dependency-specifiers>`. 

385 

386 :param marker: The string representation of a marker expression. 

387 :raises InvalidMarker: If ``marker`` cannot be parsed. 

388 

389 Instances are safe to serialize with :mod:`pickle`. They use a stable 

390 format so the same pickle can be loaded in future packaging releases. 

391 

392 .. versionchanged:: 26.2 

393 

394 Added a stable pickle format. Pickles created with packaging 26.2+ can 

395 be unpickled with future releases. Backward compatibility with pickles 

396 from pip._vendor.packaging < 26.2 is supported but may be removed in a future 

397 release. 

398 """ 

399 

400 __slots__ = ("_markers",) 

401 

402 def __init__(self, marker: str) -> None: 

403 # Note: We create a Marker object without calling this constructor in 

404 # packaging.requirements.Requirement. If any additional logic is 

405 # added here, make sure to mirror/adapt Requirement. 

406 

407 # If this fails and throws an error, the repr still expects _markers to 

408 # be defined. 

409 self._markers: MarkerList = [] 

410 

411 try: 

412 self._markers = _normalize_extra_values(_parse_marker(marker)) 

413 # The attribute `_markers` can be described in terms of a recursive type: 

414 # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]] 

415 # 

416 # For example, the following expression: 

417 # python_version > "3.6" or (python_version == "3.6" and os_name == "unix") 

418 # 

419 # is parsed into: 

420 # [ 

421 # (<Variable('python_version')>, <Op('>')>, <Value('3.6')>), 

422 # 'and', 

423 # [ 

424 # (<Variable('python_version')>, <Op('==')>, <Value('3.6')>), 

425 # 'or', 

426 # (<Variable('os_name')>, <Op('==')>, <Value('unix')>) 

427 # ] 

428 # ] 

429 except ParserSyntaxError as e: 

430 raise InvalidMarker(str(e)) from e 

431 

432 @classmethod 

433 def _from_markers(cls, markers: MarkerList) -> Marker: 

434 """Create a Marker instance from a pre-parsed marker tree. 

435 

436 This avoids re-parsing serialised marker strings when combining markers. 

437 """ 

438 new = cls.__new__(cls) 

439 new._markers = markers 

440 return new 

441 

442 def __str__(self) -> str: 

443 return _format_marker(self._markers) 

444 

445 def __repr__(self) -> str: 

446 return f"<{self.__class__.__name__}({str(self)!r})>" 

447 

448 def __hash__(self) -> int: 

449 return hash(str(self)) 

450 

451 def __eq__(self, other: object) -> bool: 

452 if not isinstance(other, Marker): 

453 return NotImplemented 

454 

455 return str(self) == str(other) 

456 

457 def __getstate__(self) -> str: 

458 # Return the marker expression string for compactness and stability. 

459 # Internal Node objects are excluded; the string is re-parsed on load. 

460 return str(self) 

461 

462 def __setstate__(self, state: object) -> None: 

463 if isinstance(state, str): 

464 # New format (26.2+): just the marker expression string. 

465 try: 

466 self._markers = _normalize_extra_values(_parse_marker(state)) 

467 except ParserSyntaxError as exc: 

468 raise TypeError(f"Cannot restore Marker from {state!r}") from exc 

469 return 

470 if isinstance(state, dict) and "_markers" in state: 

471 # Old format (packaging <= 26.1, no __slots__): plain __dict__. 

472 markers = state["_markers"] 

473 if isinstance(markers, list): 

474 self._markers = markers 

475 return 

476 if isinstance(state, tuple) and len(state) == 2: 

477 # Old format (packaging <= 26.1, __slots__): (None, {slot: value}). 

478 _, slot_dict = state 

479 if isinstance(slot_dict, dict) and "_markers" in slot_dict: 

480 markers = slot_dict["_markers"] 

481 if isinstance(markers, list): 

482 self._markers = markers 

483 return 

484 raise TypeError(f"Cannot restore Marker from {state!r}") 

485 

486 def __and__(self, other: Marker) -> Marker: 

487 """Combine this marker with another using ``and``. 

488 

489 .. versionadded:: 26.1 

490 """ 

491 if not isinstance(other, Marker): 

492 return NotImplemented 

493 return self._from_markers([self._markers, "and", other._markers]) 

494 

495 def __or__(self, other: Marker) -> Marker: 

496 """Combine this marker with another using ``or``. 

497 

498 .. versionadded:: 26.1 

499 """ 

500 if not isinstance(other, Marker): 

501 return NotImplemented 

502 return self._from_markers([self._markers, "or", other._markers]) 

503 

504 def evaluate( 

505 self, 

506 environment: Mapping[str, str | AbstractSet[str]] | None = None, 

507 context: EvaluateContext = "metadata", 

508 ) -> bool: 

509 """Evaluate a marker. 

510 

511 Return the boolean from evaluating this marker against the environment. 

512 The environment is determined from the current Python process unless 

513 passed in explicitly. 

514 

515 :param environment: Mapping containing keys and values to override the 

516 detected environment. 

517 :param EvaluateContext context: The context in which the marker is 

518 evaluated, which influences what marker names are considered valid. 

519 Accepted values are ``"metadata"`` (for core metadata; default), 

520 ``"lock_file"``, and ``"requirement"`` (i.e. all other situations). 

521 :raises UndefinedComparison: If the marker uses a comparison on values 

522 that are not valid versions per the :ref:`specification of version 

523 specifiers <pypug:version-specifiers>`. 

524 :raises UndefinedEnvironmentName: If the marker references a value that 

525 is missing from the evaluation environment. 

526 :returns: ``True`` if the marker matches, otherwise ``False``. 

527 

528 .. versionchanged:: 25.0 

529 Added the ``context`` parameter, which influences which marker names 

530 are considered valid. 

531 """ 

532 current_environment = cast( 

533 "dict[str, str | AbstractSet[str]]", default_environment() 

534 ) 

535 if context == "lock_file": 

536 current_environment |= { 

537 "extras": frozenset(), 

538 "dependency_groups": frozenset(), 

539 } 

540 elif context == "metadata": 

541 current_environment["extra"] = "" 

542 

543 if environment is not None: 

544 current_environment |= environment 

545 if "extra" in current_environment: 

546 # The API used to allow setting extra to None. We need to handle 

547 # this case for backwards compatibility. Also skip running 

548 # normalize name if extra is empty. 

549 extra = cast("str | None", current_environment["extra"]) 

550 current_environment["extra"] = canonicalize_name(extra) if extra else "" 

551 

552 return _evaluate_markers( 

553 self._markers, _repair_python_full_version(current_environment) 

554 ) 

555 

556 

557def _pep440_python_full_version(python_full_version: str) -> str: 

558 """ 

559 Work around platform.python_version() returning something that is not PEP 440 

560 compliant for non-tagged Python builds. 

561 """ 

562 if python_full_version.endswith("+"): 

563 return f"{python_full_version}local" 

564 return python_full_version 

565 

566 

567def _repair_python_full_version( 

568 env: dict[str, str | AbstractSet[str]], 

569) -> dict[str, str | AbstractSet[str]]: 

570 """ 

571 Work around platform.python_version() returning something that is not PEP 440 

572 compliant for non-tagged Python builds. 

573 """ 

574 python_full_version = cast("str", env["python_full_version"]) 

575 env["python_full_version"] = _pep440_python_full_version(python_full_version) 

576 return env