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 absolute_import 6 7import distutils.util 8 9try: 10 from importlib.machinery import EXTENSION_SUFFIXES 11except ImportError: # pragma: no cover 12 import imp 13 14 EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] 15 del imp 16import logging 17import os 18import platform 19import re 20import struct 21import sys 22import sysconfig 23import warnings 24 25from ._typing import TYPE_CHECKING, cast 26 27if TYPE_CHECKING: # pragma: no cover 28 from typing import ( 29 Dict, 30 FrozenSet, 31 IO, 32 Iterable, 33 Iterator, 34 List, 35 Optional, 36 Sequence, 37 Tuple, 38 Union, 39 ) 40 41 PythonVersion = Sequence[int] 42 MacVersion = Tuple[int, int] 43 GlibcVersion = Tuple[int, int] 44 45 46logger = logging.getLogger(__name__) 47 48INTERPRETER_SHORT_NAMES = { 49 "python": "py", # Generic. 50 "cpython": "cp", 51 "pypy": "pp", 52 "ironpython": "ip", 53 "jython": "jy", 54} # type: Dict[str, str] 55 56 57_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 58 59 60class Tag(object): 61 """ 62 A representation of the tag triple for a wheel. 63 64 Instances are considered immutable and thus are hashable. Equality checking 65 is also supported. 66 """ 67 68 __slots__ = ["_interpreter", "_abi", "_platform"] 69 70 def __init__(self, interpreter, abi, platform): 71 # type: (str, str, str) -> None 72 self._interpreter = interpreter.lower() 73 self._abi = abi.lower() 74 self._platform = platform.lower() 75 76 @property 77 def interpreter(self): 78 # type: () -> str 79 return self._interpreter 80 81 @property 82 def abi(self): 83 # type: () -> str 84 return self._abi 85 86 @property 87 def platform(self): 88 # type: () -> str 89 return self._platform 90 91 def __eq__(self, other): 92 # type: (object) -> bool 93 if not isinstance(other, Tag): 94 return NotImplemented 95 96 return ( 97 (self.platform == other.platform) 98 and (self.abi == other.abi) 99 and (self.interpreter == other.interpreter) 100 ) 101 102 def __hash__(self): 103 # type: () -> int 104 return hash((self._interpreter, self._abi, self._platform)) 105 106 def __str__(self): 107 # type: () -> str 108 return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) 109 110 def __repr__(self): 111 # type: () -> str 112 return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) 113 114 115def parse_tag(tag): 116 # type: (str) -> FrozenSet[Tag] 117 """ 118 Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. 119 120 Returning a set is required due to the possibility that the tag is a 121 compressed tag set. 122 """ 123 tags = set() 124 interpreters, abis, platforms = tag.split("-") 125 for interpreter in interpreters.split("."): 126 for abi in abis.split("."): 127 for platform_ in platforms.split("."): 128 tags.add(Tag(interpreter, abi, platform_)) 129 return frozenset(tags) 130 131 132def _warn_keyword_parameter(func_name, kwargs): 133 # type: (str, Dict[str, bool]) -> bool 134 """ 135 Backwards-compatibility with Python 2.7 to allow treating 'warn' as keyword-only. 136 """ 137 if not kwargs: 138 return False 139 elif len(kwargs) > 1 or "warn" not in kwargs: 140 kwargs.pop("warn", None) 141 arg = next(iter(kwargs.keys())) 142 raise TypeError( 143 "{}() got an unexpected keyword argument {!r}".format(func_name, arg) 144 ) 145 return kwargs["warn"] 146 147 148def _get_config_var(name, warn=False): 149 # type: (str, bool) -> Union[int, str, None] 150 value = sysconfig.get_config_var(name) 151 if value is None and warn: 152 logger.debug( 153 "Config variable '%s' is unset, Python ABI tag may be incorrect", name 154 ) 155 return value 156 157 158def _normalize_string(string): 159 # type: (str) -> str 160 return string.replace(".", "_").replace("-", "_") 161 162 163def _abi3_applies(python_version): 164 # type: (PythonVersion) -> bool 165 """ 166 Determine if the Python version supports abi3. 167 168 PEP 384 was first implemented in Python 3.2. 169 """ 170 return len(python_version) > 1 and tuple(python_version) >= (3, 2) 171 172 173def _cpython_abis(py_version, warn=False): 174 # type: (PythonVersion, bool) -> List[str] 175 py_version = tuple(py_version) # To allow for version comparison. 176 abis = [] 177 version = _version_nodot(py_version[:2]) 178 debug = pymalloc = ucs4 = "" 179 with_debug = _get_config_var("Py_DEBUG", warn) 180 has_refcount = hasattr(sys, "gettotalrefcount") 181 # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled 182 # extension modules is the best option. 183 # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 184 has_ext = "_d.pyd" in EXTENSION_SUFFIXES 185 if with_debug or (with_debug is None and (has_refcount or has_ext)): 186 debug = "d" 187 if py_version < (3, 8): 188 with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) 189 if with_pymalloc or with_pymalloc is None: 190 pymalloc = "m" 191 if py_version < (3, 3): 192 unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) 193 if unicode_size == 4 or ( 194 unicode_size is None and sys.maxunicode == 0x10FFFF 195 ): 196 ucs4 = "u" 197 elif debug: 198 # Debug builds can also load "normal" extension modules. 199 # We can also assume no UCS-4 or pymalloc requirement. 200 abis.append("cp{version}".format(version=version)) 201 abis.insert( 202 0, 203 "cp{version}{debug}{pymalloc}{ucs4}".format( 204 version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 205 ), 206 ) 207 return abis 208 209 210def cpython_tags( 211 python_version=None, # type: Optional[PythonVersion] 212 abis=None, # type: Optional[Iterable[str]] 213 platforms=None, # type: Optional[Iterable[str]] 214 **kwargs # type: bool 215): 216 # type: (...) -> Iterator[Tag] 217 """ 218 Yields the tags for a CPython interpreter. 219 220 The tags consist of: 221 - cp<python_version>-<abi>-<platform> 222 - cp<python_version>-abi3-<platform> 223 - cp<python_version>-none-<platform> 224 - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2. 225 226 If python_version only specifies a major version then user-provided ABIs and 227 the 'none' ABItag will be used. 228 229 If 'abi3' or 'none' are specified in 'abis' then they will be yielded at 230 their normal position and not at the beginning. 231 """ 232 warn = _warn_keyword_parameter("cpython_tags", kwargs) 233 if not python_version: 234 python_version = sys.version_info[:2] 235 236 interpreter = "cp{}".format(_version_nodot(python_version[:2])) 237 238 if abis is None: 239 if len(python_version) > 1: 240 abis = _cpython_abis(python_version, warn) 241 else: 242 abis = [] 243 abis = list(abis) 244 # 'abi3' and 'none' are explicitly handled later. 245 for explicit_abi in ("abi3", "none"): 246 try: 247 abis.remove(explicit_abi) 248 except ValueError: 249 pass 250 251 platforms = list(platforms or _platform_tags()) 252 for abi in abis: 253 for platform_ in platforms: 254 yield Tag(interpreter, abi, platform_) 255 if _abi3_applies(python_version): 256 for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): 257 yield tag 258 for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): 259 yield tag 260 261 if _abi3_applies(python_version): 262 for minor_version in range(python_version[1] - 1, 1, -1): 263 for platform_ in platforms: 264 interpreter = "cp{version}".format( 265 version=_version_nodot((python_version[0], minor_version)) 266 ) 267 yield Tag(interpreter, "abi3", platform_) 268 269 270def _generic_abi(): 271 # type: () -> Iterator[str] 272 abi = sysconfig.get_config_var("SOABI") 273 if abi: 274 yield _normalize_string(abi) 275 276 277def generic_tags( 278 interpreter=None, # type: Optional[str] 279 abis=None, # type: Optional[Iterable[str]] 280 platforms=None, # type: Optional[Iterable[str]] 281 **kwargs # type: bool 282): 283 # type: (...) -> Iterator[Tag] 284 """ 285 Yields the tags for a generic interpreter. 286 287 The tags consist of: 288 - <interpreter>-<abi>-<platform> 289 290 The "none" ABI will be added if it was not explicitly provided. 291 """ 292 warn = _warn_keyword_parameter("generic_tags", kwargs) 293 if not interpreter: 294 interp_name = interpreter_name() 295 interp_version = interpreter_version(warn=warn) 296 interpreter = "".join([interp_name, interp_version]) 297 if abis is None: 298 abis = _generic_abi() 299 platforms = list(platforms or _platform_tags()) 300 abis = list(abis) 301 if "none" not in abis: 302 abis.append("none") 303 for abi in abis: 304 for platform_ in platforms: 305 yield Tag(interpreter, abi, platform_) 306 307 308def _py_interpreter_range(py_version): 309 # type: (PythonVersion) -> Iterator[str] 310 """ 311 Yields Python versions in descending order. 312 313 After the latest version, the major-only version will be yielded, and then 314 all previous versions of that major version. 315 """ 316 if len(py_version) > 1: 317 yield "py{version}".format(version=_version_nodot(py_version[:2])) 318 yield "py{major}".format(major=py_version[0]) 319 if len(py_version) > 1: 320 for minor in range(py_version[1] - 1, -1, -1): 321 yield "py{version}".format(version=_version_nodot((py_version[0], minor))) 322 323 324def compatible_tags( 325 python_version=None, # type: Optional[PythonVersion] 326 interpreter=None, # type: Optional[str] 327 platforms=None, # type: Optional[Iterable[str]] 328): 329 # type: (...) -> Iterator[Tag] 330 """ 331 Yields the sequence of tags that are compatible with a specific version of Python. 332 333 The tags consist of: 334 - py*-none-<platform> 335 - <interpreter>-none-any # ... if `interpreter` is provided. 336 - py*-none-any 337 """ 338 if not python_version: 339 python_version = sys.version_info[:2] 340 platforms = list(platforms or _platform_tags()) 341 for version in _py_interpreter_range(python_version): 342 for platform_ in platforms: 343 yield Tag(version, "none", platform_) 344 if interpreter: 345 yield Tag(interpreter, "none", "any") 346 for version in _py_interpreter_range(python_version): 347 yield Tag(version, "none", "any") 348 349 350def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): 351 # type: (str, bool) -> str 352 if not is_32bit: 353 return arch 354 355 if arch.startswith("ppc"): 356 return "ppc" 357 358 return "i386" 359 360 361def _mac_binary_formats(version, cpu_arch): 362 # type: (MacVersion, str) -> List[str] 363 formats = [cpu_arch] 364 if cpu_arch == "x86_64": 365 if version < (10, 4): 366 return [] 367 formats.extend(["intel", "fat64", "fat32"]) 368 369 elif cpu_arch == "i386": 370 if version < (10, 4): 371 return [] 372 formats.extend(["intel", "fat32", "fat"]) 373 374 elif cpu_arch == "ppc64": 375 # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? 376 if version > (10, 5) or version < (10, 4): 377 return [] 378 formats.append("fat64") 379 380 elif cpu_arch == "ppc": 381 if version > (10, 6): 382 return [] 383 formats.extend(["fat32", "fat"]) 384 385 formats.append("universal") 386 return formats 387 388 389def mac_platforms(version=None, arch=None): 390 # type: (Optional[MacVersion], Optional[str]) -> Iterator[str] 391 """ 392 Yields the platform tags for a macOS system. 393 394 The `version` parameter is a two-item tuple specifying the macOS version to 395 generate platform tags for. The `arch` parameter is the CPU architecture to 396 generate platform tags for. Both parameters default to the appropriate value 397 for the current system. 398 """ 399 version_str, _, cpu_arch = platform.mac_ver() # type: ignore 400 if version is None: 401 version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) 402 else: 403 version = version 404 if arch is None: 405 arch = _mac_arch(cpu_arch) 406 else: 407 arch = arch 408 for minor_version in range(version[1], -1, -1): 409 compat_version = version[0], minor_version 410 binary_formats = _mac_binary_formats(compat_version, arch) 411 for binary_format in binary_formats: 412 yield "macosx_{major}_{minor}_{binary_format}".format( 413 major=compat_version[0], 414 minor=compat_version[1], 415 binary_format=binary_format, 416 ) 417 418 419# From PEP 513. 420def _is_manylinux_compatible(name, glibc_version): 421 # type: (str, GlibcVersion) -> bool 422 # Check for presence of _manylinux module. 423 try: 424 import _manylinux # noqa 425 426 return bool(getattr(_manylinux, name + "_compatible")) 427 except (ImportError, AttributeError): 428 # Fall through to heuristic check below. 429 pass 430 431 return _have_compatible_glibc(*glibc_version) 432 433 434def _glibc_version_string(): 435 # type: () -> Optional[str] 436 # Returns glibc version string, or None if not using glibc. 437 return _glibc_version_string_confstr() or _glibc_version_string_ctypes() 438 439 440def _glibc_version_string_confstr(): 441 # type: () -> Optional[str] 442 """ 443 Primary implementation of glibc_version_string using os.confstr. 444 """ 445 # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely 446 # to be broken or missing. This strategy is used in the standard library 447 # platform module. 448 # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183 449 try: 450 # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17". 451 version_string = os.confstr( # type: ignore[attr-defined] # noqa: F821 452 "CS_GNU_LIBC_VERSION" 453 ) 454 assert version_string is not None 455 _, version = version_string.split() # type: Tuple[str, str] 456 except (AssertionError, AttributeError, OSError, ValueError): 457 # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... 458 return None 459 return version 460 461 462def _glibc_version_string_ctypes(): 463 # type: () -> Optional[str] 464 """ 465 Fallback implementation of glibc_version_string using ctypes. 466 """ 467 try: 468 import ctypes 469 except ImportError: 470 return None 471 472 # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen 473 # manpage says, "If filename is NULL, then the returned handle is for the 474 # main program". This way we can let the linker do the work to figure out 475 # which libc our process is actually using. 476 # 477 # Note: typeshed is wrong here so we are ignoring this line. 478 process_namespace = ctypes.CDLL(None) # type: ignore 479 try: 480 gnu_get_libc_version = process_namespace.gnu_get_libc_version 481 except AttributeError: 482 # Symbol doesn't exist -> therefore, we are not linked to 483 # glibc. 484 return None 485 486 # Call gnu_get_libc_version, which returns a string like "2.5" 487 gnu_get_libc_version.restype = ctypes.c_char_p 488 version_str = gnu_get_libc_version() # type: str 489 # py2 / py3 compatibility: 490 if not isinstance(version_str, str): 491 version_str = version_str.decode("ascii") 492 493 return version_str 494 495 496# Separated out from have_compatible_glibc for easier unit testing. 497def _check_glibc_version(version_str, required_major, minimum_minor): 498 # type: (str, int, int) -> bool 499 # Parse string and check against requested version. 500 # 501 # We use a regexp instead of str.split because we want to discard any 502 # random junk that might come after the minor version -- this might happen 503 # in patched/forked versions of glibc (e.g. Linaro's version of glibc 504 # uses version strings like "2.20-2014.11"). See gh-3588. 505 m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str) 506 if not m: 507 warnings.warn( 508 "Expected glibc version with 2 components major.minor," 509 " got: %s" % version_str, 510 RuntimeWarning, 511 ) 512 return False 513 return ( 514 int(m.group("major")) == required_major 515 and int(m.group("minor")) >= minimum_minor 516 ) 517 518 519def _have_compatible_glibc(required_major, minimum_minor): 520 # type: (int, int) -> bool 521 version_str = _glibc_version_string() 522 if version_str is None: 523 return False 524 return _check_glibc_version(version_str, required_major, minimum_minor) 525 526 527# Python does not provide platform information at sufficient granularity to 528# identify the architecture of the running executable in some cases, so we 529# determine it dynamically by reading the information from the running 530# process. This only applies on Linux, which uses the ELF format. 531class _ELFFileHeader(object): 532 # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header 533 class _InvalidELFFileHeader(ValueError): 534 """ 535 An invalid ELF file header was found. 536 """ 537 538 ELF_MAGIC_NUMBER = 0x7F454C46 539 ELFCLASS32 = 1 540 ELFCLASS64 = 2 541 ELFDATA2LSB = 1 542 ELFDATA2MSB = 2 543 EM_386 = 3 544 EM_S390 = 22 545 EM_ARM = 40 546 EM_X86_64 = 62 547 EF_ARM_ABIMASK = 0xFF000000 548 EF_ARM_ABI_VER5 = 0x05000000 549 EF_ARM_ABI_FLOAT_HARD = 0x00000400 550 551 def __init__(self, file): 552 # type: (IO[bytes]) -> None 553 def unpack(fmt): 554 # type: (str) -> int 555 try: 556 (result,) = struct.unpack( 557 fmt, file.read(struct.calcsize(fmt)) 558 ) # type: (int, ) 559 except struct.error: 560 raise _ELFFileHeader._InvalidELFFileHeader() 561 return result 562 563 self.e_ident_magic = unpack(">I") 564 if self.e_ident_magic != self.ELF_MAGIC_NUMBER: 565 raise _ELFFileHeader._InvalidELFFileHeader() 566 self.e_ident_class = unpack("B") 567 if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}: 568 raise _ELFFileHeader._InvalidELFFileHeader() 569 self.e_ident_data = unpack("B") 570 if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}: 571 raise _ELFFileHeader._InvalidELFFileHeader() 572 self.e_ident_version = unpack("B") 573 self.e_ident_osabi = unpack("B") 574 self.e_ident_abiversion = unpack("B") 575 self.e_ident_pad = file.read(7) 576 format_h = "<H" if self.e_ident_data == self.ELFDATA2LSB else ">H" 577 format_i = "<I" if self.e_ident_data == self.ELFDATA2LSB else ">I" 578 format_q = "<Q" if self.e_ident_data == self.ELFDATA2LSB else ">Q" 579 format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q 580 self.e_type = unpack(format_h) 581 self.e_machine = unpack(format_h) 582 self.e_version = unpack(format_i) 583 self.e_entry = unpack(format_p) 584 self.e_phoff = unpack(format_p) 585 self.e_shoff = unpack(format_p) 586 self.e_flags = unpack(format_i) 587 self.e_ehsize = unpack(format_h) 588 self.e_phentsize = unpack(format_h) 589 self.e_phnum = unpack(format_h) 590 self.e_shentsize = unpack(format_h) 591 self.e_shnum = unpack(format_h) 592 self.e_shstrndx = unpack(format_h) 593 594 595def _get_elf_header(): 596 # type: () -> Optional[_ELFFileHeader] 597 try: 598 with open(sys.executable, "rb") as f: 599 elf_header = _ELFFileHeader(f) 600 except (IOError, OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader): 601 return None 602 return elf_header 603 604 605def _is_linux_armhf(): 606 # type: () -> bool 607 # hard-float ABI can be detected from the ELF header of the running 608 # process 609 # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf 610 elf_header = _get_elf_header() 611 if elf_header is None: 612 return False 613 result = elf_header.e_ident_class == elf_header.ELFCLASS32 614 result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB 615 result &= elf_header.e_machine == elf_header.EM_ARM 616 result &= ( 617 elf_header.e_flags & elf_header.EF_ARM_ABIMASK 618 ) == elf_header.EF_ARM_ABI_VER5 619 result &= ( 620 elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD 621 ) == elf_header.EF_ARM_ABI_FLOAT_HARD 622 return result 623 624 625def _is_linux_i686(): 626 # type: () -> bool 627 elf_header = _get_elf_header() 628 if elf_header is None: 629 return False 630 result = elf_header.e_ident_class == elf_header.ELFCLASS32 631 result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB 632 result &= elf_header.e_machine == elf_header.EM_386 633 return result 634 635 636def _have_compatible_manylinux_abi(arch): 637 # type: (str) -> bool 638 if arch == "armv7l": 639 return _is_linux_armhf() 640 if arch == "i686": 641 return _is_linux_i686() 642 return True 643 644 645def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): 646 # type: (bool) -> Iterator[str] 647 linux = _normalize_string(distutils.util.get_platform()) 648 if is_32bit: 649 if linux == "linux_x86_64": 650 linux = "linux_i686" 651 elif linux == "linux_aarch64": 652 linux = "linux_armv7l" 653 manylinux_support = [] 654 _, arch = linux.split("_", 1) 655 if _have_compatible_manylinux_abi(arch): 656 if arch in {"x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"}: 657 manylinux_support.append( 658 ("manylinux2014", (2, 17)) 659 ) # CentOS 7 w/ glibc 2.17 (PEP 599) 660 if arch in {"x86_64", "i686"}: 661 manylinux_support.append( 662 ("manylinux2010", (2, 12)) 663 ) # CentOS 6 w/ glibc 2.12 (PEP 571) 664 manylinux_support.append( 665 ("manylinux1", (2, 5)) 666 ) # CentOS 5 w/ glibc 2.5 (PEP 513) 667 manylinux_support_iter = iter(manylinux_support) 668 for name, glibc_version in manylinux_support_iter: 669 if _is_manylinux_compatible(name, glibc_version): 670 yield linux.replace("linux", name) 671 break 672 # Support for a later manylinux implies support for an earlier version. 673 for name, _ in manylinux_support_iter: 674 yield linux.replace("linux", name) 675 yield linux 676 677 678def _generic_platforms(): 679 # type: () -> Iterator[str] 680 yield _normalize_string(distutils.util.get_platform()) 681 682 683def _platform_tags(): 684 # type: () -> Iterator[str] 685 """ 686 Provides the platform tags for this installation. 687 """ 688 if platform.system() == "Darwin": 689 return mac_platforms() 690 elif platform.system() == "Linux": 691 return _linux_platforms() 692 else: 693 return _generic_platforms() 694 695 696def interpreter_name(): 697 # type: () -> str 698 """ 699 Returns the name of the running interpreter. 700 """ 701 try: 702 name = sys.implementation.name # type: ignore 703 except AttributeError: # pragma: no cover 704 # Python 2.7 compatibility. 705 name = platform.python_implementation().lower() 706 return INTERPRETER_SHORT_NAMES.get(name) or name 707 708 709def interpreter_version(**kwargs): 710 # type: (bool) -> str 711 """ 712 Returns the version of the running interpreter. 713 """ 714 warn = _warn_keyword_parameter("interpreter_version", kwargs) 715 version = _get_config_var("py_version_nodot", warn=warn) 716 if version: 717 version = str(version) 718 else: 719 version = _version_nodot(sys.version_info[:2]) 720 return version 721 722 723def _version_nodot(version): 724 # type: (PythonVersion) -> str 725 if any(v >= 10 for v in version): 726 sep = "_" 727 else: 728 sep = "" 729 return sep.join(map(str, version)) 730 731 732def sys_tags(**kwargs): 733 # type: (bool) -> Iterator[Tag] 734 """ 735 Returns the sequence of tag triples for the running interpreter. 736 737 The order of the sequence corresponds to priority order for the 738 interpreter, from most to least important. 739 """ 740 warn = _warn_keyword_parameter("sys_tags", kwargs) 741 742 interp_name = interpreter_name() 743 if interp_name == "cp": 744 for tag in cpython_tags(warn=warn): 745 yield tag 746 else: 747 for tag in generic_tags(): 748 yield tag 749 750 for tag in compatible_tags(): 751 yield tag 752