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

Fix Platform-Specific Wheel File Selection & Assign Markers (See #944 / #955) #1144

Closed
wants to merge 6 commits into from
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
91 changes: 88 additions & 3 deletions poetry/repositories/pypi_repository.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import sys
import os

import platform

from collections import defaultdict
from typing import Dict
Expand Down Expand Up @@ -52,6 +53,14 @@ class PyPiRepository(Repository):

CACHE_VERSION = parse_constraint("0.12.0")

# Implementation name lookup specified by PEP425
IMP_NAME_LOOKUP = {
"cpython": "cp",
"ironpython": "ip",
"pypy": "pp",
"jython": "jy",
}

def __init__(self, url="https://pypi.org/", disable_cache=False, fallback=True):
self._url = url
self._disable_cache = disable_cache
Expand All @@ -78,6 +87,20 @@ def __init__(self, url="https://pypi.org/", disable_cache=False, fallback=True):

self._name = "PyPI"

@property
def sys_info(self): # type: () -> Dict[str, str]
"""
Return dictionary describing the OS and Python configuration.
"""
if not hasattr(self, "_sys_info"):
self._sys_info = {
"plat": platform.system().lower(),
"is32bit": sys.maxsize <= 2 ** 32,
"imp_name": sys.implementation.name,
"pyver": platform.python_version_tuple(),
}
return self._sys_info

@property
def url(self): # type: () -> str
return self._url
Expand Down Expand Up @@ -338,6 +361,15 @@ def _get_release_info(self, name, version): # type: (str, str) -> dict

data["requires_dist"] = info["requires_dist"]

if "platform" in info and data["platform"] == "":
self._log(
"Overwriting platform: `{}` with `{}`".format(
data["platform"], info["platform"]
),
level="debug",
)
data["platform"] = info["platform"]

if not data["requires_python"]:
data["requires_python"] = info["requires_python"]

Expand Down Expand Up @@ -441,11 +473,64 @@ def _get_info_from_urls(
return self._get_info_from_wheel(universal_python2_wheel)

if platform_specific_wheels and "sdist" not in urls:
# Pick the first wheel available and hope for the best
return self._get_info_from_wheel(platform_specific_wheels[0])
# Attempt to select the best platform-specific wheel
best_wheel = self._pick_platform_specific_wheel(
platform_specific_wheels
)
return self._get_info_from_wheel(best_wheel)

return self._get_info_from_sdist(urls["sdist"][0])

def _pick_platform_specific_wheel(
self, platform_specific_wheels
): # type: (list) -> str
# Format information for checking the PEP425 "Platform Tag"
os_map = {"windows": "win", "darwin": "macosx"}
os_name = os_map.get(self.sys_info["plat"], self.sys_info["plat"])
bit_label = "32" if self.sys_info["is32bit"] else "64"
# Format information for checking the PEP425 "Python Tag"
imp_abbr = self.IMP_NAME_LOOKUP.get(self.sys_info["imp_name"].lower(), "py")
py_abbr = "".join(self.sys_info["pyver"][:2])
self._log(
"Attempting to determine best wheel file for: {}".format(self.sys_info),
level="debug",
)

platform_matches = []
for url in platform_specific_wheels:
m = wheel_file_re.match(Link(url).filename)
plat = m.group("plat")
if os_name in plat:
# Check python version and the Python implementation or generic "py"
match_py = m.group("pyver") in [imp_abbr + py_abbr, "py" + py_abbr]
if match_py and (bit_label in plat or "x86_64" in plat):
self._log(
"Selected best platform, bit, and Python version match: {}".format(
url
),
level="debug",
)
return url
elif match_py:
platform_matches.insert(0, url)
if len(platform_matches) > 0:
# Return first platform match as more specificity couldn't be determined
self._log(
"Selecting first wheel file for platform {}: {}".format(
os_name, platform_matches[0]
),
level="debug",
)
return platform_matches[0]
# Could not pick the best wheel, return the first available and hope for the best
self._log(
"Matching was unsuccessful, selecting first wheel file: {}".format(
platform_specific_wheels[0]
),
level="debug",
)
return platform_specific_wheels[0]

def _get_info_from_wheel(
self, url
): # type: (str) -> Dict[str, Union[str, List, None]]
Expand Down
22 changes: 22 additions & 0 deletions poetry/utils/inspector.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import pkginfo
from requests import get

from poetry.version.markers import parse_marker
from poetry.utils.patterns import wheel_file_re

from ._compat import Path
from .helpers import parse_requires
from .setup_reader import SetupReader
Expand Down Expand Up @@ -73,6 +76,25 @@ def inspect_wheel(
if meta.requires_dist:
info["requires_dist"] = meta.requires_dist

basename = os.path.basename(str(file_path))
m = wheel_file_re.match(basename)
if m:
os_name = m.group("plat")
plat_map = {"windows": "win", "macosx": "darwin", "none": "any"}
for plat in ["none", "linux", "macosx", "windows", "cygwin"]:
if plat in os_name:
break
else:
plat = os_name
plat = plat_map.get(plat, plat)
if plat != "any":
logger.debug(
"Added the platform marker `{}` for wheel file `{}`".format(
basename, plat
)
)
info["platform"] = plat

return info

def inspect_sdist(
Expand Down
96 changes: 95 additions & 1 deletion tests/repositories/test_pypi_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@ class MockRepository(PyPiRepository):
JSON_FIXTURES = Path(__file__).parent / "fixtures" / "pypi.org" / "json"
DIST_FIXTURES = Path(__file__).parent / "fixtures" / "pypi.org" / "dists"

def __init__(self, fallback=False):
def __init__(
self, fallback=False, plat="Linux", is32bit=True, imp_name="py", pyver=(3, 7, 2)
):
super(MockRepository, self).__init__(
url="http://foo.bar", disable_cache=True, fallback=fallback
)

# Mock different hardware configurations
self._sys_info = {
"plat": plat.lower(),
"is32bit": is32bit,
"imp_name": imp_name,
"pyver": pyver,
}

def _get(self, url):
parts = url.split("/")[1:]
name = parts[0]
Expand Down Expand Up @@ -106,6 +116,90 @@ def test_fallback_on_downloading_packages():
]


# Mock platform specific wheels for testing
numpy_plat_spec_wheels = [
(
"numpy-1.16.2-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel."
"macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"
),
"numpy-1.16.2-cp27-cp27m-manylinux1_i686.whl",
"numpy-1.16.2-cp27-cp27m-manylinux1_x86_64.whl",
"numpy-1.16.2-cp27-cp27mu-manylinux1_i686.whl",
"numpy-1.16.2-cp27-cp27mu-manylinux1_x86_64.whl",
"numpy-1.16.2-cp27-cp27m-win32.whl",
"numpy-1.16.2-cp27-cp27m-win_amd64.whl",
(
"numpy-1.16.2-cp35-cp35m-macosx_10_6_intel.macosx_10_9_intel."
"macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"
),
"numpy-1.16.2-cp35-cp35m-manylinux1_i686.whl",
"numpy-1.16.2-cp35-cp35m-manylinux1_x86_64.whl",
"numpy-1.16.2-cp35-cp35m-win32.whl",
"numpy-1.16.2-cp35-cp35m-win_amd64.whl",
(
"numpy-1.16.2-cp36-cp36m-macosx_10_6_intel.macosx_10_9_intel."
"macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"
),
"numpy-1.16.2-cp36-cp36m-manylinux1_i686.whl",
"numpy-1.16.2-cp36-cp36m-manylinux1_x86_64.whl",
"numpy-1.16.2-cp36-cp36m-win32.whl",
"numpy-1.16.2-cp36-cp36m-win_amd64.whl",
(
"numpy-1.16.2-cp37-cp37m-macosx_10_6_intel.macosx_10_9_intel."
"macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"
),
"numpy-1.16.2-cp37-cp37m-manylinux1_i686.whl",
"numpy-1.16.2-cp37-cp37m-manylinux1_x86_64.whl",
"numpy-1.16.2-cp37-cp37m-win32.whl",
"numpy-1.16.2-cp37-cp37m-win_amd64.whl",
]


@pytest.mark.parametrize(
"plat,is32bit,imp_name,pyver,best_wheel",
[
(
"Linux",
False,
"CPython",
("3", "7", "2"),
"numpy-1.16.2-cp37-cp37m-manylinux1_x86_64.whl",
),
(
"Darwin",
False,
"CPython",
("2", "7", "3"),
(
"numpy-1.16.2-cp27-cp27m-macosx_10_6_intel.macosx_10_9_intel."
"macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl"
),
),
(
"Windows",
False,
"CPython",
("3", "6", "2"),
"numpy-1.16.2-cp36-cp36m-win_amd64.whl",
),
(
"Windows",
True,
"CPython",
("3", "5", "0a1"),
"numpy-1.16.2-cp35-cp35m-win32.whl",
),
],
)
def test_fallback_selects_correct_platform_wheel(
plat, is32bit, imp_name, pyver, best_wheel
):
repo = MockRepository(
fallback=True, plat=plat, is32bit=is32bit, imp_name=imp_name, pyver=pyver
)
assert best_wheel == repo._pick_platform_specific_wheel(numpy_plat_spec_wheels)


def test_fallback_inspects_sdist_first_if_no_matching_wheels_can_be_found():
repo = MockRepository(fallback=True)

Expand Down