Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restore legacy PyPy wheel interpreter tags #7655

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions news/7629.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Restore compatibility with legacy PyPy-specific interpreter wheel tags that use
the interpreter implementation version in the filename instead of the nominal
Python language version (for example, the expected PyPy-specific interpreter
tag is ``pp36`` for versions of PyPy that target Python 3.6 compatibility, but
older versions of pip instead looked for tags like ``pp372``, incorporating the
first two digits of the implementation version).
92 changes: 92 additions & 0 deletions src/pip/_internal/models/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
name that have meaning.
"""
import re
from collections import OrderedDict

from pip._vendor.packaging.tags import Tag

from pip._internal.exceptions import InvalidWheelFilename
from pip._internal.utils.deprecation import deprecated
from pip._internal.utils.typing import MYPY_CHECK_RUNNING

if MYPY_CHECK_RUNNING:
Expand Down Expand Up @@ -42,6 +44,28 @@ def __init__(self, filename):
self.abis = wheel_info.group('abi').split('.')
self.plats = wheel_info.group('plat').split('.')

# Adjust for any legacy custom PyPy tags found in pyversions
missing_tags = _infer_missing_pypy_version_tags(self.pyversions)
if missing_tags:
self.pyversions.extend(missing_tags)

deprecated(
ncoghlan marked this conversation as resolved.
Show resolved Hide resolved
reason=(
"{} includes legacy PyPy version tags without the "
"corresponding standard tags ({})."
).format(self.filename, missing_tags),
replacement=(
"for maintainers: use v0.34.0 or later of the wheel "
"project to create a correctly tagged {0} PyPy wheel. "
"For users: contact the maintainers of {0} to let "
"them know to regenerate their PyPy wheel(s)".format(
self.name
)
),
gone_in="21.0",
issue=7629,
)

# All the tag combinations from this file
self.file_tags = {
Tag(x, y, z) for x in self.pyversions
Expand Down Expand Up @@ -76,3 +100,71 @@ def supported(self, tags):
:param tags: the PEP 425 tags to check the wheel against.
"""
return not self.file_tags.isdisjoint(tags)


def _is_legacy_pypy_tag(pyversion_tag):
# type: (str) -> bool
"""Returns True if the given tag looks like a legacy custom PyPy tag

:param pyversion_tag: pyversion tags to be checked
"""
return (
len(pyversion_tag) == 5 and
pyversion_tag.startswith('pp') and
pyversion_tag[2:].isdigit()
)


# Note: the listed thresholds are the first non-alpha PyPy version that
# *doesn't* report the given Python version in sys.version_info. This
# means that PyPy 7.0.0 is handled as a Python 3.5 compatible release.
_PYPY3_COMPATIBILITY_TAG_THRESHOLDS = OrderedDict((
('pp32', (5, 2)),
('pp33', (5, 7)),
('pp35', (7, 1)),
('pp36', (8, 0))
# The legacy custom PyPy wheel tags are not supported on PyPy 8.0.0+
))


def _infer_missing_pypy_version_tags(pyversions):
# type: (List[str]) -> List[str]
"""Infer standard PyPy tags for any legacy PyPy tags, avoiding duplicates

Returns an ordered list of the missing tags (which may be empty)

:param pyversions: the list of pyversion tags to be checked
"""
# Several wheel versions prior to 0.34.0 produced non-standard tags
# for PyPy wheel archives. For backwards compatibility, we translate
# those legacy custom tags to standard tags for PyPy versions prior to
# PyPy 8.0.0.
legacy_pypy_tags = [tag for tag in pyversions if _is_legacy_pypy_tag(tag)]
if not legacy_pypy_tags:
return [] # Nothing to do

standard_tags = set()
py3_tag_thresholds = _PYPY3_COMPATIBILITY_TAG_THRESHOLDS.items()
for tag in legacy_pypy_tags:
py_major, pypy_major, pypy_minor = map(int, tag[2:])
if py_major == 2:
standard_tags.add('pp27')
continue
if py_major > 3:
continue
pypy_version = (pypy_major, pypy_minor)
for standard_tag, version_limit in py3_tag_thresholds:
if pypy_version < version_limit:
standard_tags.add(standard_tag)
break

if not standard_tags:
return [] # Nothing to do

existing_tags = set(pyversions)
missing_tags = standard_tags - existing_tags
if not missing_tags:
return [] # Nothing to do

# Order the tags to get consistent warning output
return sorted(missing_tags)
90 changes: 88 additions & 2 deletions tests/functional/test_download.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os.path
import shutil
import sys
import textwrap
from hashlib import sha256

Expand All @@ -11,6 +12,14 @@
from tests.lib.path import Path
from tests.lib.server import file_response

from pip._vendor.packaging.tags import ( # isort:skip
_cpython_abis, # For platform specific download testing
_generic_abi, # For platform specific download testing
_generic_platforms, # For platform specific download testing
interpreter_name, # For platform specific download testing
interpreter_version # For platform specific download testing
)


def fake_wheel(data, wheel_path):
wheel_name = os.path.basename(wheel_path)
Expand Down Expand Up @@ -61,6 +70,83 @@ def test_download_wheel(script, data):
assert script.site_packages / 'piptestpackage' not in result.files_created


def test_download_platform_specific_wheel(script, data):
"""
Test using "pip download" to download a platform specific *.whl archive
"""

def _get_abi_tag():
# If we're on CPython, use CPython-specific ABI tag (as 'SOABI'
# contains platform information that isn't part of the wheel ABI tag)
if interpreter_name() == 'cp':
# Unlike the generic ABI tags, CPython ABI tags are always defined
return _cpython_abis(sys.version_info)[0]
# Otherwise use the first generic wheel ABI tag
try:
return next(_generic_abi())
except StopIteration:
pass
raise RuntimeError("Failed to determine an ABI tag for this platform")

interp = interpreter_name() + interpreter_version()
abi = _get_abi_tag()
platform = next(_generic_platforms())
platform_wheel = 'fake-1.0-{}-{}-{}.whl'.format(interp, abi, platform)
fake_wheel(data, platform_wheel)

result = script.pip(
'download', '--no-index', '--find-links', data.find_links,
'--only-binary=:all:',
'--dest', '.',
'fake',
)
assert Path('scratch') / platform_wheel in result.files_created


@pytest.mark.parametrize("legacy_pypy_tag,standard_pypy_tag", [
("pp224", "pp27"),
("pp273", "pp27"),
("pp324", "pp32"),
("pp352", "pp33"),
("pp355", "pp33"),
("pp357", "pp35"),
("pp358", "pp35"),
("pp359", "pp35"),
("pp360", "pp35"),
("pp370", "pp35"),
("pp371", "pp36"),
("pp372", "pp36"),
("pp373", "pp36"),
("pp379", "pp36"),
])
def test_download_pypy_version_specific_wheel(
legacy_pypy_tag, standard_pypy_tag, script, data):
"""
Test using "pip download" to download a PyPy version specific *.whl archive
"""
# This can run on any platform, as the custom logic runs when the
# the PyPy specific wheel filename is processed, and doesn't depend on
# any details of the running interpreter.
interp = legacy_pypy_tag
abi = 'fake_abi'
platform_ = 'fake_platform'
pypy_wheel = 'fake-1.0-{}-{}-{}.whl'.format(interp, abi, platform_)
fake_wheel(data, pypy_wheel)

result = script.pip(
'download', '--no-index', '--find-links', data.find_links,
'--only-binary=:all:',
'--dest', '.',
'--implementation', standard_pypy_tag[:2],
'--python-version', standard_pypy_tag[2:],
'--abi', abi,
'--platform', platform_,
'fake',
allow_stderr_warning=True
)
assert Path('scratch') / pypy_wheel in result.files_created


@pytest.mark.network
def test_single_download_from_requirements_file(script):
"""
Expand Down Expand Up @@ -608,8 +694,8 @@ def test_download_specify_abi(script, data):

def test_download_specify_implementation(script, data):
"""
Test using "pip download --abi" to download a .whl archive
supported for a specific abi
Test using "pip download --implementation" to download a .whl archive
supported for a specific implementation
"""
fake_wheel(data, 'fake-1.0-py2.py3-none-any.whl')
result = script.pip(
Expand Down