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