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

193 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 operator 

8import os 

9import platform 

10import sys 

11from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast 

12 

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

14from ._parser import parse_marker as _parse_marker 

15from ._tokenizer import ParserSyntaxError 

16from .specifiers import InvalidSpecifier, Specifier 

17from .utils import canonicalize_name 

18 

19__all__ = [ 

20 "Environment", 

21 "EvaluateContext", 

22 "InvalidMarker", 

23 "Marker", 

24 "UndefinedComparison", 

25 "UndefinedEnvironmentName", 

26 "default_environment", 

27] 

28 

29 

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

31 return __all__ 

32 

33 

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

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

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

37 

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

39 

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

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

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

43""" 

44 

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

46MARKERS_REQUIRING_VERSION = { 

47 "implementation_version", 

48 "platform_release", 

49 "python_full_version", 

50 "python_version", 

51} 

52 

53 

54class InvalidMarker(ValueError): 

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

56 

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

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

59 """ 

60 

61 

62class UndefinedComparison(ValueError): 

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

64 

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

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

67 <pypug:version-specifiers>`. 

68 """ 

69 

70 

71class UndefinedEnvironmentName(ValueError): 

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

73 

74 

75class Environment(TypedDict): 

76 """ 

77 A dictionary that represents a Python environment as captured by 

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

79 """ 

80 

81 implementation_name: str 

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

83 

84 implementation_version: str 

85 """ 

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

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

88 """ 

89 

90 os_name: str 

91 """ 

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

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

94 """ 

95 

96 platform_machine: str 

97 """ 

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

99 

100 An empty string if the value cannot be determined. 

101 """ 

102 

103 platform_release: str 

104 """ 

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

106 

107 An empty string if the value cannot be determined. 

108 """ 

109 

110 platform_system: str 

111 """ 

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

113 

114 An empty string if the value cannot be determined. 

115 """ 

116 

117 platform_version: str 

118 """ 

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

120 

121 An empty string if the value cannot be determined. 

122 """ 

123 

124 python_full_version: str 

125 """ 

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

127 

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

129 the patchlevel (it defaults to 0). 

130 """ 

131 

132 platform_python_implementation: str 

133 """ 

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

135 """ 

136 

137 python_version: str 

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

139 

140 sys_platform: str 

141 """ 

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

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

144 

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

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

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

148 was built. 

149 """ 

150 

151 

152def _normalize_extras( 

153 result: MarkerList | MarkerAtom | str, 

154) -> MarkerList | MarkerAtom | str: 

155 if not isinstance(result, tuple): 

156 return result 

157 

158 lhs, op, rhs = result 

159 if isinstance(lhs, Variable) and lhs.value == "extra": 

160 normalized_extra = canonicalize_name(rhs.value) 

161 rhs = Value(normalized_extra) 

162 elif isinstance(rhs, Variable) and rhs.value == "extra": 

163 normalized_extra = canonicalize_name(lhs.value) 

164 lhs = Value(normalized_extra) 

165 return lhs, op, rhs 

166 

167 

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

169 """ 

170 Normalize extra values. 

171 """ 

172 

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

174 

175 

176def _format_marker( 

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

178) -> str: 

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

180 

181 # Sometimes we have a structure like [[...]] which is a single item list 

182 # where the single item is itself it's own list. In that case we want skip 

183 # the rest of this function so that we don't get extraneous () on the 

184 # outside. 

185 if ( 

186 isinstance(marker, list) 

187 and len(marker) == 1 

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

189 ): 

190 return _format_marker(marker[0]) 

191 

192 if isinstance(marker, list): 

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

194 if first: 

195 return " ".join(inner) 

196 else: 

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

198 elif isinstance(marker, tuple): 

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

200 else: 

201 return marker 

202 

203 

204_operators: dict[str, Operator] = { 

205 "in": lambda lhs, rhs: lhs in rhs, 

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

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

208 "<=": operator.eq, 

209 "==": operator.eq, 

210 "!=": operator.ne, 

211 ">=": operator.eq, 

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

213} 

214 

215 

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

217 op_str = op.serialize() 

218 if key in MARKERS_REQUIRING_VERSION: 

219 try: 

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

221 except InvalidSpecifier: 

222 pass 

223 else: 

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

225 

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

227 if oper is None: 

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

229 

230 return oper(lhs, rhs) 

231 

232 

233def _normalize( 

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

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

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

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

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

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

240 if key == "extra": 

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

242 # Both sides are normalized at this point already 

243 return (lhs, rhs) 

244 if key in MARKERS_ALLOWING_SET: 

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

246 return (canonicalize_name(lhs), canonicalize_name(rhs)) 

247 else: 

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

249 

250 # other environment markers don't have such standards 

251 return lhs, rhs 

252 

253 

254def _evaluate_markers( 

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

256) -> bool: 

257 groups: list[list[bool]] = [[]] 

258 

259 for marker in markers: 

260 if isinstance(marker, list): 

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

262 elif isinstance(marker, tuple): 

263 lhs, op, rhs = marker 

264 

265 if isinstance(lhs, Variable): 

266 environment_key = lhs.value 

267 lhs_value = environment[environment_key] 

268 rhs_value = rhs.value 

269 else: 

270 lhs_value = lhs.value 

271 environment_key = rhs.value 

272 rhs_value = environment[environment_key] 

273 

274 assert isinstance(lhs_value, str), "lhs must be a string" 

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

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

277 elif marker == "or": 

278 groups.append([]) 

279 elif marker == "and": 

280 pass 

281 else: # pragma: nocover 

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

283 

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

285 

286 

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

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

289 kind = info.releaselevel 

290 if kind != "final": 

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

292 return version 

293 

294 

295def default_environment() -> Environment: 

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

297 

298 This is the base environment used by :meth:`Marker.evaluate`. 

299 """ 

300 iver = _format_full_version(sys.implementation.version) 

301 implementation_name = sys.implementation.name 

302 return { 

303 "implementation_name": implementation_name, 

304 "implementation_version": iver, 

305 "os_name": os.name, 

306 "platform_machine": platform.machine(), 

307 "platform_release": platform.release(), 

308 "platform_system": platform.system(), 

309 "platform_version": platform.version(), 

310 "python_full_version": platform.python_version(), 

311 "platform_python_implementation": platform.python_implementation(), 

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

313 "sys_platform": sys.platform, 

314 } 

315 

316 

317class Marker: 

318 """Represents a parsed dependency marker expression. 

319 

320 Marker expressions are parsed according to the 

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

322 

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

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

325 

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

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

328 

329 .. versionchanged:: 26.2 

330 

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

332 be unpickled with future releases. Backward compatibility with pickles 

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

334 release. 

335 """ 

336 

337 __slots__ = ("_markers",) 

338 

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

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

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

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

343 

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

345 # be defined. 

346 self._markers: MarkerList = [] 

347 

348 try: 

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

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

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

352 # 

353 # For example, the following expression: 

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

355 # 

356 # is parsed into: 

357 # [ 

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

359 # 'and', 

360 # [ 

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

362 # 'or', 

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

364 # ] 

365 # ] 

366 except ParserSyntaxError as e: 

367 raise InvalidMarker(str(e)) from e 

368 

369 @classmethod 

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

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

372 

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

374 """ 

375 new = cls.__new__(cls) 

376 new._markers = markers 

377 return new 

378 

379 def __str__(self) -> str: 

380 return _format_marker(self._markers) 

381 

382 def __repr__(self) -> str: 

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

384 

385 def __hash__(self) -> int: 

386 return hash(str(self)) 

387 

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

389 if not isinstance(other, Marker): 

390 return NotImplemented 

391 

392 return str(self) == str(other) 

393 

394 def __getstate__(self) -> str: 

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

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

397 return str(self) 

398 

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

400 if isinstance(state, str): 

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

402 try: 

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

404 except ParserSyntaxError as exc: 

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

406 return 

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

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

409 markers = state["_markers"] 

410 if isinstance(markers, list): 

411 self._markers = markers 

412 return 

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

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

415 _, slot_dict = state 

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

417 markers = slot_dict["_markers"] 

418 if isinstance(markers, list): 

419 self._markers = markers 

420 return 

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

422 

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

424 if not isinstance(other, Marker): 

425 return NotImplemented 

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

427 

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

429 if not isinstance(other, Marker): 

430 return NotImplemented 

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

432 

433 def evaluate( 

434 self, 

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

436 context: EvaluateContext = "metadata", 

437 ) -> bool: 

438 """Evaluate a marker. 

439 

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

441 The environment is determined from the current Python process unless 

442 passed in explicitly. 

443 

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

445 detected environment. 

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

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

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

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

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

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

452 specifiers <pypug:version-specifiers>`. 

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

454 is missing from the evaluation environment. 

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

456 

457 """ 

458 current_environment = cast( 

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

460 ) 

461 if context == "lock_file": 

462 current_environment.update( 

463 extras=frozenset(), dependency_groups=frozenset() 

464 ) 

465 elif context == "metadata": 

466 current_environment["extra"] = "" 

467 

468 if environment is not None: 

469 current_environment.update(environment) 

470 if "extra" in current_environment: 

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

472 # this case for backwards compatibility. Also skip running 

473 # normalize name if extra is empty. 

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

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

476 

477 return _evaluate_markers( 

478 self._markers, _repair_python_full_version(current_environment) 

479 ) 

480 

481 

482def _repair_python_full_version( 

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

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

485 """ 

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

487 compliant for non-tagged Python builds. 

488 """ 

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

490 if python_full_version.endswith("+"): 

491 env["python_full_version"] = f"{python_full_version}local" 

492 return env