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

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

154 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 

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

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

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

32MARKERS_REQUIRING_VERSION = { 

33 "implementation_version", 

34 "platform_release", 

35 "python_full_version", 

36 "python_version", 

37} 

38 

39 

40class InvalidMarker(ValueError): 

41 """ 

42 An invalid marker was found, users should refer to PEP 508. 

43 """ 

44 

45 

46class UndefinedComparison(ValueError): 

47 """ 

48 An invalid operation was attempted on a value that doesn't support it. 

49 """ 

50 

51 

52class UndefinedEnvironmentName(ValueError): 

53 """ 

54 A name was attempted to be used that does not exist inside of the 

55 environment. 

56 """ 

57 

58 

59class Environment(TypedDict): 

60 implementation_name: str 

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

62 

63 implementation_version: str 

64 """ 

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

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

67 """ 

68 

69 os_name: str 

70 """ 

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

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

73 """ 

74 

75 platform_machine: str 

76 """ 

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

78 

79 An empty string if the value cannot be determined. 

80 """ 

81 

82 platform_release: str 

83 """ 

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

85 

86 An empty string if the value cannot be determined. 

87 """ 

88 

89 platform_system: str 

90 """ 

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

92 

93 An empty string if the value cannot be determined. 

94 """ 

95 

96 platform_version: str 

97 """ 

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

99 

100 An empty string if the value cannot be determined. 

101 """ 

102 

103 python_full_version: str 

104 """ 

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

106 

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

108 the patchlevel (it defaults to 0). 

109 """ 

110 

111 platform_python_implementation: str 

112 """ 

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

114 """ 

115 

116 python_version: str 

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

118 

119 sys_platform: str 

120 """ 

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

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

123 

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

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

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

127 was built. 

128 """ 

129 

130 

131def _normalize_extras( 

132 result: MarkerList | MarkerAtom | str, 

133) -> MarkerList | MarkerAtom | str: 

134 if not isinstance(result, tuple): 

135 return result 

136 

137 lhs, op, rhs = result 

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

139 normalized_extra = canonicalize_name(rhs.value) 

140 rhs = Value(normalized_extra) 

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

142 normalized_extra = canonicalize_name(lhs.value) 

143 lhs = Value(normalized_extra) 

144 return lhs, op, rhs 

145 

146 

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

148 """ 

149 Normalize extra values. 

150 """ 

151 

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

153 

154 

155def _format_marker( 

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

157) -> str: 

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

159 

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

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

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

163 # outside. 

164 if ( 

165 isinstance(marker, list) 

166 and len(marker) == 1 

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

168 ): 

169 return _format_marker(marker[0]) 

170 

171 if isinstance(marker, list): 

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

173 if first: 

174 return " ".join(inner) 

175 else: 

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

177 elif isinstance(marker, tuple): 

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

179 else: 

180 return marker 

181 

182 

183_operators: dict[str, Operator] = { 

184 "in": lambda lhs, rhs: lhs in rhs, 

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

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

187 "<=": operator.eq, 

188 "==": operator.eq, 

189 "!=": operator.ne, 

190 ">=": operator.eq, 

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

192} 

193 

194 

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

196 op_str = op.serialize() 

197 if key in MARKERS_REQUIRING_VERSION: 

198 try: 

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

200 except InvalidSpecifier: 

201 pass 

202 else: 

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

204 

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

206 if oper is None: 

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

208 

209 return oper(lhs, rhs) 

210 

211 

212def _normalize( 

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

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

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

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

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

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

219 if key == "extra": 

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

221 # Both sides are normalized at this point already 

222 return (lhs, rhs) 

223 if key in MARKERS_ALLOWING_SET: 

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

225 return (canonicalize_name(lhs), canonicalize_name(rhs)) 

226 else: 

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

228 

229 # other environment markers don't have such standards 

230 return lhs, rhs 

231 

232 

233def _evaluate_markers( 

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

235) -> bool: 

236 groups: list[list[bool]] = [[]] 

237 

238 for marker in markers: 

239 if isinstance(marker, list): 

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

241 elif isinstance(marker, tuple): 

242 lhs, op, rhs = marker 

243 

244 if isinstance(lhs, Variable): 

245 environment_key = lhs.value 

246 lhs_value = environment[environment_key] 

247 rhs_value = rhs.value 

248 else: 

249 lhs_value = lhs.value 

250 environment_key = rhs.value 

251 rhs_value = environment[environment_key] 

252 

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

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

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

256 elif marker == "or": 

257 groups.append([]) 

258 elif marker == "and": 

259 pass 

260 else: # pragma: nocover 

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

262 

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

264 

265 

266def format_full_version(info: sys._version_info) -> str: 

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

268 kind = info.releaselevel 

269 if kind != "final": 

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

271 return version 

272 

273 

274def default_environment() -> Environment: 

275 iver = format_full_version(sys.implementation.version) 

276 implementation_name = sys.implementation.name 

277 return { 

278 "implementation_name": implementation_name, 

279 "implementation_version": iver, 

280 "os_name": os.name, 

281 "platform_machine": platform.machine(), 

282 "platform_release": platform.release(), 

283 "platform_system": platform.system(), 

284 "platform_version": platform.version(), 

285 "python_full_version": platform.python_version(), 

286 "platform_python_implementation": platform.python_implementation(), 

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

288 "sys_platform": sys.platform, 

289 } 

290 

291 

292class Marker: 

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

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

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

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

297 

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

299 # be defined. 

300 self._markers: MarkerList = [] 

301 

302 try: 

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

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

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

306 # 

307 # For example, the following expression: 

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

309 # 

310 # is parsed into: 

311 # [ 

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

313 # 'and', 

314 # [ 

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

316 # 'or', 

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

318 # ] 

319 # ] 

320 except ParserSyntaxError as e: 

321 raise InvalidMarker(str(e)) from e 

322 

323 def __str__(self) -> str: 

324 return _format_marker(self._markers) 

325 

326 def __repr__(self) -> str: 

327 return f"<{self.__class__.__name__}('{self}')>" 

328 

329 def __hash__(self) -> int: 

330 return hash(str(self)) 

331 

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

333 if not isinstance(other, Marker): 

334 return NotImplemented 

335 

336 return str(self) == str(other) 

337 

338 def evaluate( 

339 self, 

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

341 context: EvaluateContext = "metadata", 

342 ) -> bool: 

343 """Evaluate a marker. 

344 

345 Return the boolean from evaluating the given marker against the 

346 environment. environment is an optional argument to override all or 

347 part of the determined environment. The *context* parameter specifies what 

348 context the markers are being evaluated for, which influences what markers 

349 are considered valid. Acceptable values are "metadata" (for core metadata; 

350 default), "lock_file", and "requirement" (i.e. all other situations). 

351 

352 The environment is determined from the current Python process. 

353 """ 

354 current_environment = cast( 

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

356 ) 

357 if context == "lock_file": 

358 current_environment.update( 

359 extras=frozenset(), dependency_groups=frozenset() 

360 ) 

361 elif context == "metadata": 

362 current_environment["extra"] = "" 

363 

364 if environment is not None: 

365 current_environment.update(environment) 

366 if "extra" in current_environment: 

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

368 # this case for backwards compatibility. Also skip running 

369 # normalize name if extra is empty. 

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

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

372 

373 return _evaluate_markers( 

374 self._markers, _repair_python_full_version(current_environment) 

375 ) 

376 

377 

378def _repair_python_full_version( 

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

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

381 """ 

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

383 compliant for non-tagged Python builds. 

384 """ 

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

386 if python_full_version.endswith("+"): 

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

388 return env