Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/tags.py: 2%

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

302 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 logging 

8import platform 

9import re 

10import struct 

11import subprocess 

12import sys 

13import sysconfig 

14from importlib.machinery import EXTENSION_SUFFIXES 

15from typing import ( 

16 Iterable, 

17 Iterator, 

18 Sequence, 

19 Tuple, 

20 cast, 

21) 

22 

23from . import _manylinux, _musllinux 

24 

25logger = logging.getLogger(__name__) 

26 

27PythonVersion = Sequence[int] 

28AppleVersion = Tuple[int, int] 

29 

30INTERPRETER_SHORT_NAMES: dict[str, str] = { 

31 "python": "py", # Generic. 

32 "cpython": "cp", 

33 "pypy": "pp", 

34 "ironpython": "ip", 

35 "jython": "jy", 

36} 

37 

38 

39_32_BIT_INTERPRETER = struct.calcsize("P") == 4 

40 

41 

42class Tag: 

43 """ 

44 A representation of the tag triple for a wheel. 

45 

46 Instances are considered immutable and thus are hashable. Equality checking 

47 is also supported. 

48 """ 

49 

50 __slots__ = ["_abi", "_hash", "_interpreter", "_platform"] 

51 

52 def __init__(self, interpreter: str, abi: str, platform: str) -> None: 

53 self._interpreter = interpreter.lower() 

54 self._abi = abi.lower() 

55 self._platform = platform.lower() 

56 # The __hash__ of every single element in a Set[Tag] will be evaluated each time 

57 # that a set calls its `.disjoint()` method, which may be called hundreds of 

58 # times when scanning a page of links for packages with tags matching that 

59 # Set[Tag]. Pre-computing the value here produces significant speedups for 

60 # downstream consumers. 

61 self._hash = hash((self._interpreter, self._abi, self._platform)) 

62 

63 @property 

64 def interpreter(self) -> str: 

65 return self._interpreter 

66 

67 @property 

68 def abi(self) -> str: 

69 return self._abi 

70 

71 @property 

72 def platform(self) -> str: 

73 return self._platform 

74 

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

76 if not isinstance(other, Tag): 

77 return NotImplemented 

78 

79 return ( 

80 (self._hash == other._hash) # Short-circuit ASAP for perf reasons. 

81 and (self._platform == other._platform) 

82 and (self._abi == other._abi) 

83 and (self._interpreter == other._interpreter) 

84 ) 

85 

86 def __hash__(self) -> int: 

87 return self._hash 

88 

89 def __str__(self) -> str: 

90 return f"{self._interpreter}-{self._abi}-{self._platform}" 

91 

92 def __repr__(self) -> str: 

93 return f"<{self} @ {id(self)}>" 

94 

95 

96def parse_tag(tag: str) -> frozenset[Tag]: 

97 """ 

98 Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. 

99 

100 Returning a set is required due to the possibility that the tag is a 

101 compressed tag set. 

102 """ 

103 tags = set() 

104 interpreters, abis, platforms = tag.split("-") 

105 for interpreter in interpreters.split("."): 

106 for abi in abis.split("."): 

107 for platform_ in platforms.split("."): 

108 tags.add(Tag(interpreter, abi, platform_)) 

109 return frozenset(tags) 

110 

111 

112def _get_config_var(name: str, warn: bool = False) -> int | str | None: 

113 value: int | str | None = sysconfig.get_config_var(name) 

114 if value is None and warn: 

115 logger.debug( 

116 "Config variable '%s' is unset, Python ABI tag may be incorrect", name 

117 ) 

118 return value 

119 

120 

121def _normalize_string(string: str) -> str: 

122 return string.replace(".", "_").replace("-", "_").replace(" ", "_") 

123 

124 

125def _is_threaded_cpython(abis: list[str]) -> bool: 

126 """ 

127 Determine if the ABI corresponds to a threaded (`--disable-gil`) build. 

128 

129 The threaded builds are indicated by a "t" in the abiflags. 

130 """ 

131 if len(abis) == 0: 

132 return False 

133 # expect e.g., cp313 

134 m = re.match(r"cp\d+(.*)", abis[0]) 

135 if not m: 

136 return False 

137 abiflags = m.group(1) 

138 return "t" in abiflags 

139 

140 

141def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: 

142 """ 

143 Determine if the Python version supports abi3. 

144 

145 PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`) 

146 builds do not support abi3. 

147 """ 

148 return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading 

149 

150 

151def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]: 

152 py_version = tuple(py_version) # To allow for version comparison. 

153 abis = [] 

154 version = _version_nodot(py_version[:2]) 

155 threading = debug = pymalloc = ucs4 = "" 

156 with_debug = _get_config_var("Py_DEBUG", warn) 

157 has_refcount = hasattr(sys, "gettotalrefcount") 

158 # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled 

159 # extension modules is the best option. 

160 # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 

161 has_ext = "_d.pyd" in EXTENSION_SUFFIXES 

162 if with_debug or (with_debug is None and (has_refcount or has_ext)): 

163 debug = "d" 

164 if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn): 

165 threading = "t" 

166 if py_version < (3, 8): 

167 with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) 

168 if with_pymalloc or with_pymalloc is None: 

169 pymalloc = "m" 

170 if py_version < (3, 3): 

171 unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) 

172 if unicode_size == 4 or ( 

173 unicode_size is None and sys.maxunicode == 0x10FFFF 

174 ): 

175 ucs4 = "u" 

176 elif debug: 

177 # Debug builds can also load "normal" extension modules. 

178 # We can also assume no UCS-4 or pymalloc requirement. 

179 abis.append(f"cp{version}{threading}") 

180 abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}") 

181 return abis 

182 

183 

184def cpython_tags( 

185 python_version: PythonVersion | None = None, 

186 abis: Iterable[str] | None = None, 

187 platforms: Iterable[str] | None = None, 

188 *, 

189 warn: bool = False, 

190) -> Iterator[Tag]: 

191 """ 

192 Yields the tags for a CPython interpreter. 

193 

194 The tags consist of: 

195 - cp<python_version>-<abi>-<platform> 

196 - cp<python_version>-abi3-<platform> 

197 - cp<python_version>-none-<platform> 

198 - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2. 

199 

200 If python_version only specifies a major version then user-provided ABIs and 

201 the 'none' ABItag will be used. 

202 

203 If 'abi3' or 'none' are specified in 'abis' then they will be yielded at 

204 their normal position and not at the beginning. 

205 """ 

206 if not python_version: 

207 python_version = sys.version_info[:2] 

208 

209 interpreter = f"cp{_version_nodot(python_version[:2])}" 

210 

211 if abis is None: 

212 if len(python_version) > 1: 

213 abis = _cpython_abis(python_version, warn) 

214 else: 

215 abis = [] 

216 abis = list(abis) 

217 # 'abi3' and 'none' are explicitly handled later. 

218 for explicit_abi in ("abi3", "none"): 

219 try: 

220 abis.remove(explicit_abi) 

221 except ValueError: 

222 pass 

223 

224 platforms = list(platforms or platform_tags()) 

225 for abi in abis: 

226 for platform_ in platforms: 

227 yield Tag(interpreter, abi, platform_) 

228 

229 threading = _is_threaded_cpython(abis) 

230 use_abi3 = _abi3_applies(python_version, threading) 

231 if use_abi3: 

232 yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) 

233 yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) 

234 

235 if use_abi3: 

236 for minor_version in range(python_version[1] - 1, 1, -1): 

237 for platform_ in platforms: 

238 version = _version_nodot((python_version[0], minor_version)) 

239 interpreter = f"cp{version}" 

240 yield Tag(interpreter, "abi3", platform_) 

241 

242 

243def _generic_abi() -> list[str]: 

244 """ 

245 Return the ABI tag based on EXT_SUFFIX. 

246 """ 

247 # The following are examples of `EXT_SUFFIX`. 

248 # We want to keep the parts which are related to the ABI and remove the 

249 # parts which are related to the platform: 

250 # - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310 

251 # - mac: '.cpython-310-darwin.so' => cp310 

252 # - win: '.cp310-win_amd64.pyd' => cp310 

253 # - win: '.pyd' => cp37 (uses _cpython_abis()) 

254 # - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73 

255 # - graalpy: '.graalpy-38-native-x86_64-darwin.dylib' 

256 # => graalpy_38_native 

257 

258 ext_suffix = _get_config_var("EXT_SUFFIX", warn=True) 

259 if not isinstance(ext_suffix, str) or ext_suffix[0] != ".": 

260 raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')") 

261 parts = ext_suffix.split(".") 

262 if len(parts) < 3: 

263 # CPython3.7 and earlier uses ".pyd" on Windows. 

264 return _cpython_abis(sys.version_info[:2]) 

265 soabi = parts[1] 

266 if soabi.startswith("cpython"): 

267 # non-windows 

268 abi = "cp" + soabi.split("-")[1] 

269 elif soabi.startswith("cp"): 

270 # windows 

271 abi = soabi.split("-")[0] 

272 elif soabi.startswith("pypy"): 

273 abi = "-".join(soabi.split("-")[:2]) 

274 elif soabi.startswith("graalpy"): 

275 abi = "-".join(soabi.split("-")[:3]) 

276 elif soabi: 

277 # pyston, ironpython, others? 

278 abi = soabi 

279 else: 

280 return [] 

281 return [_normalize_string(abi)] 

282 

283 

284def generic_tags( 

285 interpreter: str | None = None, 

286 abis: Iterable[str] | None = None, 

287 platforms: Iterable[str] | None = None, 

288 *, 

289 warn: bool = False, 

290) -> Iterator[Tag]: 

291 """ 

292 Yields the tags for a generic interpreter. 

293 

294 The tags consist of: 

295 - <interpreter>-<abi>-<platform> 

296 

297 The "none" ABI will be added if it was not explicitly provided. 

298 """ 

299 if not interpreter: 

300 interp_name = interpreter_name() 

301 interp_version = interpreter_version(warn=warn) 

302 interpreter = "".join([interp_name, interp_version]) 

303 if abis is None: 

304 abis = _generic_abi() 

305 else: 

306 abis = list(abis) 

307 platforms = list(platforms or platform_tags()) 

308 if "none" not in abis: 

309 abis.append("none") 

310 for abi in abis: 

311 for platform_ in platforms: 

312 yield Tag(interpreter, abi, platform_) 

313 

314 

315def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: 

316 """ 

317 Yields Python versions in descending order. 

318 

319 After the latest version, the major-only version will be yielded, and then 

320 all previous versions of that major version. 

321 """ 

322 if len(py_version) > 1: 

323 yield f"py{_version_nodot(py_version[:2])}" 

324 yield f"py{py_version[0]}" 

325 if len(py_version) > 1: 

326 for minor in range(py_version[1] - 1, -1, -1): 

327 yield f"py{_version_nodot((py_version[0], minor))}" 

328 

329 

330def compatible_tags( 

331 python_version: PythonVersion | None = None, 

332 interpreter: str | None = None, 

333 platforms: Iterable[str] | None = None, 

334) -> Iterator[Tag]: 

335 """ 

336 Yields the sequence of tags that are compatible with a specific version of Python. 

337 

338 The tags consist of: 

339 - py*-none-<platform> 

340 - <interpreter>-none-any # ... if `interpreter` is provided. 

341 - py*-none-any 

342 """ 

343 if not python_version: 

344 python_version = sys.version_info[:2] 

345 platforms = list(platforms or platform_tags()) 

346 for version in _py_interpreter_range(python_version): 

347 for platform_ in platforms: 

348 yield Tag(version, "none", platform_) 

349 if interpreter: 

350 yield Tag(interpreter, "none", "any") 

351 for version in _py_interpreter_range(python_version): 

352 yield Tag(version, "none", "any") 

353 

354 

355def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: 

356 if not is_32bit: 

357 return arch 

358 

359 if arch.startswith("ppc"): 

360 return "ppc" 

361 

362 return "i386" 

363 

364 

365def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]: 

366 formats = [cpu_arch] 

367 if cpu_arch == "x86_64": 

368 if version < (10, 4): 

369 return [] 

370 formats.extend(["intel", "fat64", "fat32"]) 

371 

372 elif cpu_arch == "i386": 

373 if version < (10, 4): 

374 return [] 

375 formats.extend(["intel", "fat32", "fat"]) 

376 

377 elif cpu_arch == "ppc64": 

378 # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? 

379 if version > (10, 5) or version < (10, 4): 

380 return [] 

381 formats.append("fat64") 

382 

383 elif cpu_arch == "ppc": 

384 if version > (10, 6): 

385 return [] 

386 formats.extend(["fat32", "fat"]) 

387 

388 if cpu_arch in {"arm64", "x86_64"}: 

389 formats.append("universal2") 

390 

391 if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: 

392 formats.append("universal") 

393 

394 return formats 

395 

396 

397def mac_platforms( 

398 version: AppleVersion | None = None, arch: str | None = None 

399) -> Iterator[str]: 

400 """ 

401 Yields the platform tags for a macOS system. 

402 

403 The `version` parameter is a two-item tuple specifying the macOS version to 

404 generate platform tags for. The `arch` parameter is the CPU architecture to 

405 generate platform tags for. Both parameters default to the appropriate value 

406 for the current system. 

407 """ 

408 version_str, _, cpu_arch = platform.mac_ver() 

409 if version is None: 

410 version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2]))) 

411 if version == (10, 16): 

412 # When built against an older macOS SDK, Python will report macOS 10.16 

413 # instead of the real version. 

414 version_str = subprocess.run( 

415 [ 

416 sys.executable, 

417 "-sS", 

418 "-c", 

419 "import platform; print(platform.mac_ver()[0])", 

420 ], 

421 check=True, 

422 env={"SYSTEM_VERSION_COMPAT": "0"}, 

423 stdout=subprocess.PIPE, 

424 text=True, 

425 ).stdout 

426 version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2]))) 

427 else: 

428 version = version 

429 if arch is None: 

430 arch = _mac_arch(cpu_arch) 

431 else: 

432 arch = arch 

433 

434 if (10, 0) <= version and version < (11, 0): 

435 # Prior to Mac OS 11, each yearly release of Mac OS bumped the 

436 # "minor" version number. The major version was always 10. 

437 major_version = 10 

438 for minor_version in range(version[1], -1, -1): 

439 compat_version = major_version, minor_version 

440 binary_formats = _mac_binary_formats(compat_version, arch) 

441 for binary_format in binary_formats: 

442 yield f"macosx_{major_version}_{minor_version}_{binary_format}" 

443 

444 if version >= (11, 0): 

445 # Starting with Mac OS 11, each yearly release bumps the major version 

446 # number. The minor versions are now the midyear updates. 

447 minor_version = 0 

448 for major_version in range(version[0], 10, -1): 

449 compat_version = major_version, minor_version 

450 binary_formats = _mac_binary_formats(compat_version, arch) 

451 for binary_format in binary_formats: 

452 yield f"macosx_{major_version}_{minor_version}_{binary_format}" 

453 

454 if version >= (11, 0): 

455 # Mac OS 11 on x86_64 is compatible with binaries from previous releases. 

456 # Arm64 support was introduced in 11.0, so no Arm binaries from previous 

457 # releases exist. 

458 # 

459 # However, the "universal2" binary format can have a 

460 # macOS version earlier than 11.0 when the x86_64 part of the binary supports 

461 # that version of macOS. 

462 major_version = 10 

463 if arch == "x86_64": 

464 for minor_version in range(16, 3, -1): 

465 compat_version = major_version, minor_version 

466 binary_formats = _mac_binary_formats(compat_version, arch) 

467 for binary_format in binary_formats: 

468 yield f"macosx_{major_version}_{minor_version}_{binary_format}" 

469 else: 

470 for minor_version in range(16, 3, -1): 

471 compat_version = major_version, minor_version 

472 binary_format = "universal2" 

473 yield f"macosx_{major_version}_{minor_version}_{binary_format}" 

474 

475 

476def ios_platforms( 

477 version: AppleVersion | None = None, multiarch: str | None = None 

478) -> Iterator[str]: 

479 """ 

480 Yields the platform tags for an iOS system. 

481 

482 :param version: A two-item tuple specifying the iOS version to generate 

483 platform tags for. Defaults to the current iOS version. 

484 :param multiarch: The CPU architecture+ABI to generate platform tags for - 

485 (the value used by `sys.implementation._multiarch` e.g., 

486 `arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current 

487 multiarch value. 

488 """ 

489 if version is None: 

490 # if iOS is the current platform, ios_ver *must* be defined. However, 

491 # it won't exist for CPython versions before 3.13, which causes a mypy 

492 # error. 

493 _, release, _, _ = platform.ios_ver() # type: ignore[attr-defined, unused-ignore] 

494 version = cast("AppleVersion", tuple(map(int, release.split(".")[:2]))) 

495 

496 if multiarch is None: 

497 multiarch = sys.implementation._multiarch 

498 multiarch = multiarch.replace("-", "_") 

499 

500 ios_platform_template = "ios_{major}_{minor}_{multiarch}" 

501 

502 # Consider any iOS major.minor version from the version requested, down to 

503 # 12.0. 12.0 is the first iOS version that is known to have enough features 

504 # to support CPython. Consider every possible minor release up to X.9. There 

505 # highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra 

506 # candidates that won't ever match doesn't really hurt, and it saves us from 

507 # having to keep an explicit list of known iOS versions in the code. Return 

508 # the results descending order of version number. 

509 

510 # If the requested major version is less than 12, there won't be any matches. 

511 if version[0] < 12: 

512 return 

513 

514 # Consider the actual X.Y version that was requested. 

515 yield ios_platform_template.format( 

516 major=version[0], minor=version[1], multiarch=multiarch 

517 ) 

518 

519 # Consider every minor version from X.0 to the minor version prior to the 

520 # version requested by the platform. 

521 for minor in range(version[1] - 1, -1, -1): 

522 yield ios_platform_template.format( 

523 major=version[0], minor=minor, multiarch=multiarch 

524 ) 

525 

526 for major in range(version[0] - 1, 11, -1): 

527 for minor in range(9, -1, -1): 

528 yield ios_platform_template.format( 

529 major=major, minor=minor, multiarch=multiarch 

530 ) 

531 

532 

533def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: 

534 linux = _normalize_string(sysconfig.get_platform()) 

535 if not linux.startswith("linux_"): 

536 # we should never be here, just yield the sysconfig one and return 

537 yield linux 

538 return 

539 if is_32bit: 

540 if linux == "linux_x86_64": 

541 linux = "linux_i686" 

542 elif linux == "linux_aarch64": 

543 linux = "linux_armv8l" 

544 _, arch = linux.split("_", 1) 

545 archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch]) 

546 yield from _manylinux.platform_tags(archs) 

547 yield from _musllinux.platform_tags(archs) 

548 for arch in archs: 

549 yield f"linux_{arch}" 

550 

551 

552def _generic_platforms() -> Iterator[str]: 

553 yield _normalize_string(sysconfig.get_platform()) 

554 

555 

556def platform_tags() -> Iterator[str]: 

557 """ 

558 Provides the platform tags for this installation. 

559 """ 

560 if platform.system() == "Darwin": 

561 return mac_platforms() 

562 elif platform.system() == "iOS": 

563 return ios_platforms() 

564 elif platform.system() == "Linux": 

565 return _linux_platforms() 

566 else: 

567 return _generic_platforms() 

568 

569 

570def interpreter_name() -> str: 

571 """ 

572 Returns the name of the running interpreter. 

573 

574 Some implementations have a reserved, two-letter abbreviation which will 

575 be returned when appropriate. 

576 """ 

577 name = sys.implementation.name 

578 return INTERPRETER_SHORT_NAMES.get(name) or name 

579 

580 

581def interpreter_version(*, warn: bool = False) -> str: 

582 """ 

583 Returns the version of the running interpreter. 

584 """ 

585 version = _get_config_var("py_version_nodot", warn=warn) 

586 if version: 

587 version = str(version) 

588 else: 

589 version = _version_nodot(sys.version_info[:2]) 

590 return version 

591 

592 

593def _version_nodot(version: PythonVersion) -> str: 

594 return "".join(map(str, version)) 

595 

596 

597def sys_tags(*, warn: bool = False) -> Iterator[Tag]: 

598 """ 

599 Returns the sequence of tag triples for the running interpreter. 

600 

601 The order of the sequence corresponds to priority order for the 

602 interpreter, from most to least important. 

603 """ 

604 

605 interp_name = interpreter_name() 

606 if interp_name == "cp": 

607 yield from cpython_tags(warn=warn) 

608 else: 

609 yield from generic_tags() 

610 

611 if interp_name == "pp": 

612 interp = "pp3" 

613 elif interp_name == "cp": 

614 interp = "cp" + interpreter_version(warn=warn) 

615 else: 

616 interp = None 

617 yield from compatible_tags(interpreter=interp)