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 16 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).
72 changes: 72 additions & 0 deletions src/pip/_internal/models/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name that have meaning.
"""
import re
from collections import OrderedDict

from pip._vendor.packaging.tags import Tag

Expand Down Expand Up @@ -42,6 +43,9 @@ 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
_add_standard_pypy_version_tags(self.pyversions)
ncoghlan marked this conversation as resolved.
Show resolved Hide resolved

# 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 +80,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 _add_standard_pypy_version_tags(pyversions):
# type: (List[str]) -> bool
"""Add standard PyPy tags for any legacy PyPy tags, avoiding duplicates

Returns True if adjustments were made, False otherwise

:param pyversions: the list of pyversion tags to be adjusted
"""
# 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 False # Nothing to do
print(legacy_pypy_tags)
ncoghlan marked this conversation as resolved.
Show resolved Hide resolved
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 False # Nothing to do

existing_tags = set(pyversions)
modified = False
for tag in standard_tags:
if tag not in existing_tags:
pyversions.append(tag)
ncoghlan marked this conversation as resolved.
Show resolved Hide resolved
modified = True
return modified
87 changes: 85 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,80 @@ def test_download_wheel(script, data):
assert script.site_packages / 'piptestpackage' not in result.files_created


def _get_abi_tag():
ncoghlan marked this conversation as resolved.
Show resolved Hide resolved
# If we're on CPython, use CPython-specific ABI tag (as 'SOABI' contains
# platform information that isn't included in 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:
raise RuntimeError("Failed to determine an ABI tag for this platform")


def test_download_platform_specific_wheel(script, data):
"""
Test using "pip download" to download a platform specific *.whl archive
"""
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"),
])
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',
)
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 +691,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