diff --git a/.coveragerc b/.coveragerc index 599863fe..da205e5a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -4,5 +4,6 @@ omit = packaging/_compat.py [report] exclude_lines = + pragma: no cover @abc.abstractmethod @abc.abstractproperty diff --git a/.gitignore b/.gitignore index 126d348b..bd4a65a8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,7 @@ .cache/ .coverage .idea -.venv +.venv* __pycache__/ _build/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f665a633..977e44c2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,6 +11,7 @@ Changelog ~~~~~~~~~~~~~~~~~ * Fix string representation of PEP 508 direct URL requirements with markers. +* Add the `packaging.tags` module. * Better handling of file URLs diff --git a/dev-requirements.txt b/dev-requirements.txt index e4cc4987..0b044288 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,4 +1,8 @@ # Install our development requirements +black; python_version >= '3.6' +coverage +flake8 +pep8-naming pretend pytest tox diff --git a/docs/index.rst b/docs/index.rst index 91897cc5..4fe5fb02 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ API specifiers markers requirements + tags utils diff --git a/docs/tags.rst b/docs/tags.rst new file mode 100644 index 00000000..c6f70360 --- /dev/null +++ b/docs/tags.rst @@ -0,0 +1,101 @@ +Tags +==== + +.. currentmodule:: packaging.tags + +Wheels encode the Python interpreter, ABI, and platform that they support in +their filenames using *`platform compatibility tags`_*. This module provides +support for both parsing these tags as well as discovering what tags the +running Python interpreter supports. + +Usage +----- + +.. doctest:: + + >>> from packaging.tags import Tag, sys_tags + >>> import sys + >>> looking_for = Tag("py{major}".format(major=sys.version_info.major), "none", "any") + >>> supported_tags = list(sys_tags()) + >>> looking_for in supported_tags + True + >>> really_old = Tag("py1", "none", "any") + >>> wheels = {really_old, looking_for} + >>> best_wheel = None + >>> for supported_tag in supported_tags: + ... for wheel_tag in wheels: + ... if supported_tag == wheel_tag: + ... best_wheel = wheel_tag + ... break + >>> best_wheel == looking_for + True + +Reference +--------- + +.. attribute:: INTERPRETER_SHORT_NAMES + + A dictionary mapping interpreter names to their `abbreviation codes`_ + (e.g. ``"cpython"`` is ``"cp"``). All interpreter names are lower-case. + +.. class:: Tag(interpreter, abi, platform) + + A representation of the tag triple for a wheel. Instances are considered + immutable and thus are hashable. Equality checking is also supported. + + :param str interpreter: The interpreter name, e.g. ``"py"`` + (see :attr:`INTERPRETER_SHORT_NAMES` for mapping + well-known interpreter names to their short names). + :param str abi: The ABI that a wheel supports, e.g. ``"cp37m"``. + :param str platform: The OS/platform the wheel supports, + e.g. ``"win_amd64"``. + + .. attribute:: interpreter + + The interpreter name. + + .. attribute:: abi + + The supported ABI. + + .. attribute:: platform + + The OS/platform. + + +.. function:: parse_tag(tag) + + Parse the provided *tag* into a set of :class:`Tag` instances. + + The returning of a set is required due to the possibility that the tag is a + `compressed tag set`_, e.g. ``"py2.py3-none-any"``. + + :param str tag: The tag to parse, e.g. ``"py3-none-any"``. + + +.. function:: sys_tags() + + Create an iterable of tags that the running interpreter supports. + + The iterable is ordered so that the best-matching tag is first in the + sequence. The exact preferential order to tags is interpreter-specific, but + in general the tag importance is in the order of: + + 1. Interpreter + 2. Platform + 3. ABI + + This order is due to the fact that an ABI is inherently tied to the + platform, but platform-specific code is not necessarily tied to the ABI. The + interpreter is the most important tag as it dictates basic support for any + wheel. + + The function returns an iterable in order to allow for the possible + short-circuiting of tag generation if the entire sequence is not necessary + and calculating some tags happens to be expensive. + + +.. _abbreviation codes: https://www.python.org/dev/peps/pep-0425/#python-tag +.. _compressed tag set: https://www.python.org/dev/peps/pep-0425/#compressed-tag-sets +.. _platform compatibility tags: https://packaging.python.org/specifications/platform-compatibility-tags/ +.. _PEP 425: https://www.python.org/dev/peps/pep-0425/ diff --git a/packaging/tags.py b/packaging/tags.py new file mode 100644 index 00000000..c472b584 --- /dev/null +++ b/packaging/tags.py @@ -0,0 +1,354 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import + +import distutils.util +import platform +import re +import sys +import sysconfig +import warnings + +import attr + + +INTERPRETER_SHORT_NAMES = { + "python": "py", # Generic. + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} + + +_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 + + +@attr.s(frozen=True, repr=False) +class Tag(object): + interpreter = attr.ib(converter=str.lower) + abi = attr.ib(converter=str.lower) + platform = attr.ib(converter=str.lower) + + def __str__(self): + return "{}-{}-{}".format(self.interpreter, self.abi, self.platform) + + def __repr__(self): + return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + + +def parse_tag(tag): + tags = set() + interpreters, abis, platforms = tag.split("-") + for interpreter in interpreters.split("."): + for abi in abis.split("."): + for platform_ in platforms.split("."): + tags.add(Tag(interpreter, abi, platform_)) + return frozenset(tags) + + +def _normalize_string(string): + return string.replace(".", "_").replace("-", "_") + + +def _cpython_interpreter(py_version): + # TODO: Is using py_version_nodot for interpreter version critical? + return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) + + +# TODO: This code is simpler compared to pep425tags as CPython 2.7 didn't seem +# to need the fallbacks. Is that acceptable? +def _cpython_abi(py_version): + soabi = sysconfig.get_config_var("SOABI") + if soabi: + options = soabi.split("-", 2)[1] + else: + found_options = [str(py_version[0]), str(py_version[1])] + if sysconfig.get_config_var("Py_DEBUG"): + found_options.append("d") + if sysconfig.get_config_var("WITH_PYMALLOC"): + found_options.append("m") + if sysconfig.get_config_var("Py_UNICODE_SIZE") == 4: + found_options.append("u") + options = "".join(found_options) + return "cp{options}".format(options=options) + + +def _cpython_tags(py_version, interpreter, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + for tag in (Tag(interpreter, "abi3", platform) for platform in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform) for platform in platforms): + yield tag + # PEP 384 was first implemented in Python 3.2. + for minor_version in range(py_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{major}{minor}".format( + major=py_version[0], minor=minor_version + ) + yield Tag(interpreter, "abi3", platform_) + + +def _pypy_interpreter(): + return "pp{py_major}{pypy_major}{pypy_minor}".format( + py_major=sys.version_info[0], + pypy_major=sys.pypy_version_info.major, + pypy_minor=sys.pypy_version_info.minor, + ) + + +def _generic_abi(): + abi = sysconfig.get_config_var("SOABI") + if abi: + return _normalize_string(abi) + else: + return "none" + + +def _pypy_tags(py_version, interpreter, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform) for platform in platforms): + yield tag + + +def _generic_tags(interpreter, py_version, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + if abi != "none": + tags = (Tag(interpreter, "none", platform_) for platform_ in platforms) + for tag in tags: + yield tag + + +def _py_interpreter_range(py_version): + """ + Yield Python versions in descending order. + + After the latest version, the major-only version will be yielded, and then + all following versions up to 'end'. + """ + yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + yield "py{major}".format(major=py_version[0]) + for minor in range(py_version[1] - 1, -1, -1): + yield "py{major}{minor}".format(major=py_version[0], minor=minor) + + +def _independent_tags(interpreter, py_version, platforms): + """ + Return the sequence of tags that are consistent across implementations. + + The tags consist of: + - py*-none- + - -none-any + - py*-none-any + """ + for version in _py_interpreter_range(py_version): + for platform_ in platforms: + yield Tag(version, "none", platform_) + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(py_version): + yield Tag(version, "none", "any") + + +def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): + if is_32bit: + if arch.startswith("ppc"): + return "ppc" + else: + return "i386" + else: + return arch + + +def _mac_binary_formats(version, cpu_arch): + formats = [cpu_arch] + if cpu_arch == "x86_64": + if version >= (10, 4): + formats.extend(["intel", "fat64", "fat32"]) + else: + return [] + elif cpu_arch == "i386": + if version >= (10, 4): + formats.extend(["intel", "fat32", "fat"]) + else: + return [] + elif cpu_arch == "ppc64": + # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? + if version > (10, 5) or version < (10, 4): + return [] + else: + formats.append("fat64") + elif cpu_arch == "ppc": + if version <= (10, 6): + formats.extend(["fat32", "fat"]) + else: + return [] + + formats.append("universal") + return formats + + +def _mac_platforms(version=None, arch=None): + version_str, _, cpu_arch = platform.mac_ver() + if version is None: + version = tuple(map(int, version_str.split(".")[:2])) + if arch is None: + arch = _mac_arch(cpu_arch) + platforms = [] + for minor_version in range(version[1], -1, -1): + compat_version = version[0], minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + platforms.append( + "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + ) + return platforms + + +# From PEP 513. +def _is_manylinux_compatible(name, glibc_version): + # Check for presence of _manylinux module. + try: + import _manylinux + + return bool(getattr(_manylinux, name + "_compatible")) + except (ImportError, AttributeError): + # Fall through to heuristic check below. + pass + + return _have_compatible_glibc(*glibc_version) + + +def _glibc_version_string(): + # Returns glibc version string, or None if not using glibc. + import ctypes + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + process_namespace = ctypes.CDLL(None) + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str = gnu_get_libc_version() + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +# Separated out from have_compatible_glibc for easier unit testing. +def _check_glibc_version(version_str, required_major, minimum_minor): + # Parse string and check against requested version. + # + # We use a regexp instead of str.split because we want to discard any + # random junk that might come after the minor version -- this might happen + # in patched/forked versions of glibc (e.g. Linaro's version of glibc + # uses version strings like "2.20-2014.11"). See gh-3588. + m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) + if not m: + warnings.warn( + "Expected glibc version with 2 components major.minor," + " got: %s" % version_str, + RuntimeWarning, + ) + return False + return ( + int(m.group("major")) == required_major + and int(m.group("minor")) >= minimum_minor + ) + + +def _have_compatible_glibc(required_major, minimum_minor): + version_str = _glibc_version_string() + if version_str is None: + return False + return _check_glibc_version(version_str, required_major, minimum_minor) + + +def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): + linux = _normalize_string(distutils.util.get_platform()) + if linux == "linux_x86_64" and is_32bit: + linux = "linux_i686" + # manylinux1: CentOS 5 w/ glibc 2.5. + # manylinux2010: CentOS 6 w/ glibc 2.12. + manylinux_support = ("manylinux2010", (2, 12)), ("manylinux1", (2, 5)) + manylinux_support_iter = iter(manylinux_support) + for name, glibc_version in manylinux_support_iter: + if _is_manylinux_compatible(name, glibc_version): + platforms = [linux.replace("linux", name)] + break + else: + platforms = [] + # Support for a later manylinux implies support for an earlier version. + platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter] + platforms.append(linux) + return platforms + + +def _generic_platforms(): + platform = _normalize_string(distutils.util.get_platform()) + return [platform] + + +def _interpreter_name(): + name = platform.python_implementation().lower() + return INTERPRETER_SHORT_NAMES.get(name) or name + + +def _generic_interpreter(name, py_version): + version = sysconfig.get_config_var("py_version_nodot") + if not version: + version = "".join(map(str, py_version[:2])) + return "{name}{version}".format(name=name, version=version) + + +def sys_tags(): + """ + Returns the sequence of tag triples for the running interpreter. + + The order of the sequence corresponds to priority order for the + interpreter, from most to least important. + """ + py_version = sys.version_info[:2] + interpreter_name = _interpreter_name() + if platform.system() == "Darwin": + platforms = _mac_platforms() + elif platform.system() == "Linux": + platforms = _linux_platforms() + else: + platforms = _generic_platforms() + + if interpreter_name == "cp": + interpreter = _cpython_interpreter(py_version) + abi = _cpython_abi(py_version) + for tag in _cpython_tags(py_version, interpreter, abi, platforms): + yield tag + elif interpreter_name == "pp": + interpreter = _pypy_interpreter() + abi = _generic_abi() + for tag in _pypy_tags(py_version, interpreter, abi, platforms): + yield tag + else: + interpreter = _generic_interpreter(interpreter_name, py_version) + abi = _generic_abi() + for tag in _generic_tags(interpreter, py_version, abi, platforms): + yield tag + for tag in _independent_tags(interpreter, py_version, platforms): + yield tag diff --git a/setup.py b/setup.py index 23007c78..874512b2 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ author=about["__author__"], author_email=about["__email__"], python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", - install_requires=["pyparsing>=2.0.2", "six"], # Needed to avoid issue #91 + install_requires=["attrs", "pyparsing>=2.0.2", "six"], # Needed to avoid issue #91 classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", diff --git a/tests/test_tags.py b/tests/test_tags.py new file mode 100644 index 00000000..0bb4fd61 --- /dev/null +++ b/tests/test_tags.py @@ -0,0 +1,552 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import collections + +try: + import ctypes +except ImportError: + ctypes = None +import distutils.util + +import platform +import sys +import sysconfig +import types +import warnings + +import pytest + +from packaging import tags + + +@pytest.fixture +def example_tag(): + return tags.Tag("py3", "none", "any") + + +def test_tag_lowercasing(): + tag = tags.Tag("PY3", "None", "ANY") + assert tag.interpreter == "py3" + assert tag.abi == "none" + assert tag.platform == "any" + + +def test_tag_equality(): + args = "py3", "none", "any" + assert tags.Tag(*args) == tags.Tag(*args) + + +def test_tag_hashing(example_tag): + tags = {example_tag} # Should not raise TypeError. + assert example_tag in tags + + +def test_tag_str(example_tag): + assert str(example_tag) == "py3-none-any" + + +def test_tag_repr(example_tag): + assert repr(example_tag) == "".format( + tag_id=id(example_tag) + ) + + +def test_tag_attribute_access(example_tag): + assert example_tag.interpreter == "py3" + assert example_tag.abi == "none" + assert example_tag.platform == "any" + + +def test_parse_tag_simple(example_tag): + parsed_tags = tags.parse_tag(str(example_tag)) + assert parsed_tags == {example_tag} + + +def test_parse_tag_multi_interpreter(example_tag): + expected = {example_tag, tags.Tag("py2", "none", "any")} + given = tags.parse_tag("py2.py3-none-any") + assert given == expected + + +def test_parse_tag_multi_platform(): + expected = { + tags.Tag("cp37", "cp37m", platform) + for platform in ( + "macosx_10_6_intel", + "macosx_10_9_intel", + "macosx_10_9_x86_64", + "macosx_10_10_intel", + "macosx_10_10_x86_64", + ) + } + given = tags.parse_tag( + "cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel.macosx_10_9_x86_64." + "macosx_10_10_intel.macosx_10_10_x86_64" + ) + assert given == expected + + +@pytest.mark.parametrize( + "name,expected", + [("CPython", "cp"), ("PyPy", "pp"), ("Jython", "jy"), ("IronPython", "ip")], +) +def test__interpreter_name_cpython(name, expected, monkeypatch): + if platform.python_implementation().lower() != name: + monkeypatch.setattr(platform, "python_implementation", lambda: name) + assert tags._interpreter_name() == expected + + +@pytest.mark.parametrize( + "arch, is_32bit, expected", + [ + ("i386", True, "i386"), + ("ppc", True, "ppc"), + ("x86_64", False, "x86_64"), + ("x86_64", True, "i386"), + ("ppc64", False, "ppc64"), + ("ppc64", True, "ppc"), + ], +) +def test_macos_architectures(arch, is_32bit, expected): + assert tags._mac_arch(arch, is_32bit=is_32bit) == expected + + +@pytest.mark.parametrize( + "version,arch,expected", + [ + ((10, 17), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), + ((10, 4), "x86_64", ["x86_64", "intel", "fat64", "fat32", "universal"]), + ((10, 3), "x86_64", []), + ((10, 17), "i386", ["i386", "intel", "fat32", "fat", "universal"]), + ((10, 4), "i386", ["i386", "intel", "fat32", "fat", "universal"]), + ((10, 3), "i386", []), + ((10, 17), "ppc64", []), + ((10, 6), "ppc64", []), + ((10, 5), "ppc64", ["ppc64", "fat64", "universal"]), + ((10, 3), "ppc64", []), + ((10, 17), "ppc", []), + ((10, 7), "ppc", []), + ((10, 6), "ppc", ["ppc", "fat32", "fat", "universal"]), + ((10, 0), "ppc", ["ppc", "fat32", "fat", "universal"]), + ((11, 0), "riscv", ["riscv", "universal"]), + ], +) +def test_macos_binary_formats(version, arch, expected): + assert tags._mac_binary_formats(version, arch) == expected + + +def test_mac_platforms(): + platforms = tags._mac_platforms((10, 5), "x86_64") + assert platforms == [ + "macosx_10_5_x86_64", + "macosx_10_5_intel", + "macosx_10_5_fat64", + "macosx_10_5_fat32", + "macosx_10_5_universal", + "macosx_10_4_x86_64", + "macosx_10_4_intel", + "macosx_10_4_fat64", + "macosx_10_4_fat32", + "macosx_10_4_universal", + ] + + assert len(tags._mac_platforms((10, 17), "x86_64")) == 14 * 5 + + assert not tags._mac_platforms((10, 0), "x86_64") + + +def test_macos_version_detection(monkeypatch): + if platform.system() != "Darwin": + monkeypatch.setattr( + platform, "mac_ver", lambda: ("10.14", ("", "", ""), "x86_64") + ) + version = platform.mac_ver()[0].split(".") + expected = "macosx_{major}_{minor}".format(major=version[0], minor=version[1]) + platforms = tags._mac_platforms(arch="x86_64") + assert platforms[0].startswith(expected) + + +@pytest.mark.parametrize("arch", ["x86_64", "i386"]) +def test_macos_arch_detection(arch, monkeypatch): + if platform.system() != "Darwin" or platform.mac_ver()[2] != arch: + monkeypatch.setattr(platform, "mac_ver", lambda: ("10.14", ("", "", ""), arch)) + assert tags._mac_platforms((10, 14))[0].endswith(arch) + + +def test_cpython_abi_py3(monkeypatch): + has_soabi = bool(sysconfig.get_config_var("SOABI")) + if platform.python_implementation() != "CPython" or not has_soabi: + monkeypatch.setattr( + sysconfig, "get_config_var", lambda key: "'cpython-37m-darwin'" + ) + soabi = sysconfig.get_config_var("SOABI").split("-", 2)[1] + result = tags._cpython_abi(sys.version_info[:2]) + assert result == "cp{soabi}".format(soabi=soabi) + + +@pytest.mark.parametrize( + "debug,pymalloc,unicode_width", + [ + (False, False, 2), + (True, False, 2), + (False, True, 2), + (False, False, 4), + (True, True, 2), + (False, True, 4), + (True, True, 4), + ], +) +def test_cpython_abi_py2(debug, pymalloc, unicode_width, monkeypatch): + has_soabi = sysconfig.get_config_var("SOABI") + if platform.python_implementation() != "CPython" or has_soabi: + diff_debug = debug != sysconfig.get_config_var("Py_DEBUG") + diff_malloc = pymalloc != sysconfig.get_config_var("WITH_PYMALLOC") + unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE") + diff_unicode_size = unicode_size != unicode_width + if diff_debug or diff_malloc or diff_unicode_size: + config_vars = { + "SOABI": None, + "Py_DEBUG": int(debug), + "WITH_PYMALLOC": int(pymalloc), + "Py_UNICODE_SIZE": unicode_width, + } + monkeypatch.setattr(sysconfig, "get_config_var", config_vars.__getitem__) + else: + config_vars = { + "SOABI": None, + "Py_DEBUG": int(debug), + "WITH_PYMALLOC": int(pymalloc), + "Py_UNICODE_SIZE": unicode_width, + } + monkeypatch.setattr(sysconfig, "get_config_var", config_vars.__getitem__) + options = "" + if debug: + options += "d" + if pymalloc: + options += "m" + if unicode_width == 4: + options += "u" + assert "cp33{}".format(options) == tags._cpython_abi((3, 3)) + + +def test_independent_tags(): + result = list(tags._independent_tags("cp33", (3, 3), ["plat1", "plat2"])) + assert result == [ + tags.Tag("py33", "none", "plat1"), + tags.Tag("py33", "none", "plat2"), + tags.Tag("py3", "none", "plat1"), + tags.Tag("py3", "none", "plat2"), + tags.Tag("py32", "none", "plat1"), + tags.Tag("py32", "none", "plat2"), + tags.Tag("py31", "none", "plat1"), + tags.Tag("py31", "none", "plat2"), + tags.Tag("py30", "none", "plat1"), + tags.Tag("py30", "none", "plat2"), + tags.Tag("cp33", "none", "any"), + tags.Tag("py33", "none", "any"), + tags.Tag("py3", "none", "any"), + tags.Tag("py32", "none", "any"), + tags.Tag("py31", "none", "any"), + tags.Tag("py30", "none", "any"), + ] + + +def test_cpython_tags(): + result = list(tags._cpython_tags((3, 3), "cp33", "cp33m", ["plat1", "plat2"])) + assert result == [ + tags.Tag("cp33", "cp33m", "plat1"), + tags.Tag("cp33", "cp33m", "plat2"), + tags.Tag("cp33", "abi3", "plat1"), + tags.Tag("cp33", "abi3", "plat2"), + tags.Tag("cp33", "none", "plat1"), + tags.Tag("cp33", "none", "plat2"), + tags.Tag("cp32", "abi3", "plat1"), + tags.Tag("cp32", "abi3", "plat2"), + ] + + +def test_sys_tags_on_mac_cpython(monkeypatch): + if platform.python_implementation() != "CPython": + monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") + monkeypatch.setattr(tags, "_cpython_abi", lambda py_version: "cp33m") + if platform.system() != "Darwin": + monkeypatch.setattr(platform, "system", lambda: "Darwin") + monkeypatch.setattr(tags, "_mac_platforms", lambda: ["macosx_10_5_x86_64"]) + abi = tags._cpython_abi(sys.version_info[:2]) + platforms = tags._mac_platforms() + result = list(tags.sys_tags()) + assert result[0] == tags.Tag( + "cp{major}{minor}".format(major=sys.version_info[0], minor=sys.version_info[1]), + abi, + platforms[0], + ) + assert result[-1] == tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + + +def test_generic_abi(monkeypatch): + abi = sysconfig.get_config_var("SOABI") + if abi: + abi = abi.replace(".", "_").replace("-", "_") + else: + abi = "none" + assert abi == tags._generic_abi() + + monkeypatch.setattr(sysconfig, "get_config_var", lambda key: "cpython-37m-darwin") + assert tags._generic_abi() == "cpython_37m_darwin" + + monkeypatch.setattr(sysconfig, "get_config_var", lambda key: None) + assert tags._generic_abi() == "none" + + +def test_pypy_interpreter(monkeypatch): + if hasattr(sys, "pypy_version_info"): + major, minor = sys.pypy_version_info[:2] + else: + attributes = ["major", "minor", "micro", "releaselevel", "serial"] + PyPyVersion = collections.namedtuple("version_info", attributes) + major, minor = 6, 0 + pypy_version = PyPyVersion( + major=major, minor=minor, micro=1, releaselevel="final", serial=0 + ) + monkeypatch.setattr(sys, "pypy_version_info", pypy_version, raising=False) + expected = "pp{}{}{}".format(sys.version_info[0], major, minor) + assert expected == tags._pypy_interpreter() + + +def test_pypy_tags(monkeypatch): + if platform.python_implementation() != "PyPy": + monkeypatch.setattr(platform, "python_implementation", lambda: "PyPy") + monkeypatch.setattr(tags, "_pypy_interpreter", lambda: "pp360") + interpreter = tags._pypy_interpreter() + result = list(tags._pypy_tags((3, 3), interpreter, "pypy3_60", ["plat1", "plat2"])) + assert result == [ + tags.Tag(interpreter, "pypy3_60", "plat1"), + tags.Tag(interpreter, "pypy3_60", "plat2"), + tags.Tag(interpreter, "none", "plat1"), + tags.Tag(interpreter, "none", "plat2"), + ] + + +def test_sys_tags_on_mac_pypy(monkeypatch): + if platform.python_implementation() != "PyPy": + monkeypatch.setattr(platform, "python_implementation", lambda: "PyPy") + monkeypatch.setattr(tags, "_pypy_interpreter", lambda: "pp360") + if platform.system() != "Darwin": + monkeypatch.setattr(platform, "system", lambda: "Darwin") + monkeypatch.setattr(tags, "_mac_platforms", lambda: ["macosx_10_5_x86_64"]) + interpreter = tags._pypy_interpreter() + abi = tags._generic_abi() + platforms = tags._mac_platforms() + result = list(tags.sys_tags()) + assert result[0] == tags.Tag(interpreter, abi, platforms[0]) + assert result[-1] == tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + + +def test_generic_interpreter(): + version = sysconfig.get_config_var("py_version_nodot") + if not version: + version = "".join(sys.version_info[:2]) + result = tags._generic_interpreter("sillywalk", sys.version_info[:2]) + assert result == "sillywalk{version}".format(version=version) + + +def test_generic_interpreter_no_config_var(monkeypatch): + monkeypatch.setattr(sysconfig, "get_config_var", lambda _: None) + assert tags._generic_interpreter("sillywalk", (3, 6)) == "sillywalk36" + + +def test_generic_platforms(): + platform = distutils.util.get_platform().replace("-", "_") + platform = platform.replace(".", "_") + assert tags._generic_platforms() == [platform] + + +def test_generic_tags(): + result = list(tags._generic_tags("sillywalk33", (3, 3), "abi", ["plat1", "plat2"])) + assert result == [ + tags.Tag("sillywalk33", "abi", "plat1"), + tags.Tag("sillywalk33", "abi", "plat2"), + tags.Tag("sillywalk33", "none", "plat1"), + tags.Tag("sillywalk33", "none", "plat2"), + ] + + no_abi = tags._generic_tags("sillywalk34", (3, 4), "none", ["plat1", "plat2"]) + assert list(no_abi) == [ + tags.Tag("sillywalk34", "none", "plat1"), + tags.Tag("sillywalk34", "none", "plat2"), + ] + + +def test_sys_tags_on_windows_cpython(monkeypatch): + if platform.python_implementation() != "CPython": + monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") + monkeypatch.setattr(tags, "_cpython_abi", lambda py_version: "cp33m") + if platform.system() != "Windows": + monkeypatch.setattr(platform, "system", lambda: "Windows") + monkeypatch.setattr(tags, "_generic_platforms", lambda: ["win_amd64"]) + abi = tags._cpython_abi(sys.version_info[:2]) + platforms = tags._generic_platforms() + result = list(tags.sys_tags()) + interpreter = "cp{major}{minor}".format( + major=sys.version_info[0], minor=sys.version_info[1] + ) + expected = tags.Tag(interpreter, abi, platforms[0]) + assert result[0] == expected + expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + assert result[-1] == expected + + +def test_is_manylinux_compatible_module_support(monkeypatch): + monkeypatch.setattr(tags, "_have_compatible_glibc", lambda *args: False) + module_name = "_manylinux" + module = types.ModuleType(module_name) + module.manylinux1_compatible = True + monkeypatch.setitem(sys.modules, module_name, module) + assert tags._is_manylinux_compatible("manylinux1", (2, 5)) + module.manylinux1_compatible = False + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + del module.manylinux1_compatible + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + monkeypatch.setitem(sys.modules, module_name, None) + assert not tags._is_manylinux_compatible("manylinux1", (2, 5)) + + +def test_is_manylinux_compatible_glibc_support(monkeypatch): + monkeypatch.setitem(sys.modules, "_manylinux", None) + monkeypatch.setattr( + tags, "_have_compatible_glibc", lambda major, minor: (major, minor) <= (2, 5) + ) + assert tags._is_manylinux_compatible("manylinux1", (2, 0)) + assert tags._is_manylinux_compatible("manylinux1", (2, 5)) + assert not tags._is_manylinux_compatible("manylinux1", (2, 10)) + + +@pytest.mark.parametrize( + "version_str,major,minor,expected", + [ + ("2.4", 2, 4, True), + ("2.4", 2, 5, False), + ("2.4", 2, 3, True), + ("3.4", 2, 4, False), + ], +) +def test_check_glibc_version(version_str, major, minor, expected): + assert expected == tags._check_glibc_version(version_str, major, minor) + + +@pytest.mark.parametrize("version_str", ["glibc-2.4.5", "2"]) +def test_check_glibc_version_warning(version_str): + with warnings.catch_warnings(record=True) as w: + tags._check_glibc_version(version_str, 2, 4) + assert len(w) == 1 + assert issubclass(w[0].category, RuntimeWarning) + + +@pytest.mark.skipif(not ctypes, reason="requires ctypes") +@pytest.mark.parametrize( + "version_str,expected", + [ + # Be very explicit about bytes and Unicode for Python 2 testing. + (b"2.4", "2.4"), + (u"2.4", "2.4"), + ], +) +def test_glibc_version_string(version_str, expected, monkeypatch): + class LibcVersion: + def __init__(self, version_str): + self.version_str = version_str + + def __call__(self): + return version_str + + class ProcessNamespace: + def __init__(self, libc_version): + self.gnu_get_libc_version = libc_version + + process_namespace = ProcessNamespace(LibcVersion(version_str)) + monkeypatch.setattr(ctypes, "CDLL", lambda _: process_namespace) + + assert tags._glibc_version_string() == expected + + del process_namespace.gnu_get_libc_version + assert tags._glibc_version_string() is None + + +def test_have_compatible_glibc(monkeypatch): + if platform.system() == "Linux": + # Assuming no one is running this test with a version of glibc released in + # 1997. + assert tags._have_compatible_glibc(2, 0) + else: + monkeypatch.setattr(tags, "_glibc_version_string", lambda: "2.4") + assert tags._have_compatible_glibc(2, 4) + monkeypatch.setattr(tags, "_glibc_version_string", lambda: None) + assert not tags._have_compatible_glibc(2, 4) + + +def test_linux_platforms_64bit_on_64bit_os(monkeypatch): + is_64bit_os = distutils.util.get_platform().endswith("_x86_64") + if platform.system() != "Linux" or not is_64bit_os: + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) + linux_platform = tags._linux_platforms(is_32bit=False)[-1] + assert linux_platform == "linux_x86_64" + + +def test_linux_platforms_32bit_on_64bit_os(monkeypatch): + is_64bit_os = distutils.util.get_platform().endswith("_x86_64") + if platform.system() != "Linux" or not is_64bit_os: + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + monkeypatch.setattr(tags, "_is_manylinux_compatible", lambda *args: False) + linux_platform = tags._linux_platforms(is_32bit=True)[-1] + assert linux_platform == "linux_i686" + + +def test_linux_platforms_manylinux1(monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux1" + ) + if platform.system() != "Linux": + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + platforms = tags._linux_platforms(is_32bit=False) + assert platforms == ["manylinux1_x86_64", "linux_x86_64"] + + +def test_linux_platforms_manylinux2010(monkeypatch): + monkeypatch.setattr( + tags, "_is_manylinux_compatible", lambda name, _: name == "manylinux2010" + ) + if platform.system() != "Linux": + monkeypatch.setattr(distutils.util, "get_platform", lambda: "linux_x86_64") + platforms = tags._linux_platforms(is_32bit=False) + expected = ["manylinux2010_x86_64", "manylinux1_x86_64", "linux_x86_64"] + assert platforms == expected + + +def test_sys_tags_linux_cpython(monkeypatch): + if platform.python_implementation() != "CPython": + monkeypatch.setattr(platform, "python_implementation", lambda: "CPython") + monkeypatch.setattr(tags, "_cpython_abi", lambda py_version: "cp33m") + if platform.system() != "Linux": + monkeypatch.setattr(platform, "system", lambda: "Linux") + monkeypatch.setattr(tags, "_linux_platforms", lambda: ["linux_x86_64"]) + abi = tags._cpython_abi(sys.version_info[:2]) + platforms = tags._linux_platforms() + result = list(tags.sys_tags()) + expected_interpreter = "cp{major}{minor}".format( + major=sys.version_info[0], minor=sys.version_info[1] + ) + assert result[0] == tags.Tag(expected_interpreter, abi, platforms[0]) + expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + assert result[-1] == expected + + +def test_generic_sys_tags(monkeypatch): + monkeypatch.setattr(platform, "system", lambda: "Generic") + monkeypatch.setattr(tags, "_interpreter_name", lambda: "generic") + + result = list(tags.sys_tags()) + expected = tags.Tag("py{}0".format(sys.version_info[0]), "none", "any") + assert result[-1] == expected