Browse Source

Introduce packaging.tags (#156)

pull/163/head
Brett Cannon 3 years ago committed by Donald Stufft
parent
commit
76bc450c18
  1. 1
      .coveragerc
  2. 2
      .gitignore
  3. 1
      CHANGELOG.rst
  4. 4
      dev-requirements.txt
  5. 1
      docs/index.rst
  6. 101
      docs/tags.rst
  7. 354
      packaging/tags.py
  8. 2
      setup.py
  9. 552
      tests/test_tags.py

1
.coveragerc

@ -4,5 +4,6 @@ omit = packaging/_compat.py
[report]
exclude_lines =
pragma: no cover
@abc.abstractmethod
@abc.abstractproperty

2
.gitignore vendored

@ -6,7 +6,7 @@
.cache/
.coverage
.idea
.venv
.venv*
__pycache__/
_build/

1
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

4
dev-requirements.txt

@ -1,4 +1,8 @@
# Install our development requirements
black; python_version >= '3.6'
coverage
flake8
pep8-naming
pretend
pytest
tox

1
docs/index.rst

@ -24,6 +24,7 @@ API
specifiers
markers
requirements
tags
utils

101
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/

354
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-<platform>
- <interpreter>-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<major>[0-9]+)\.(?P<minor>[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

2
setup.py

@ -48,7 +48,7 @@ setup(
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",

552
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) == "<py3-none-any @ {tag_id}>".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
Loading…
Cancel
Save