diff --git a/poetry/repositories/pypi_repository.py b/poetry/repositories/pypi_repository.py index fd5a73d6f36..79d467081ff 100644 --- a/poetry/repositories/pypi_repository.py +++ b/poetry/repositories/pypi_repository.py @@ -1,5 +1,7 @@ import logging +import sys import os +import platform import tarfile import zipfile @@ -56,6 +58,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._name = "PyPI" self._url = url @@ -80,6 +90,20 @@ def __init__(self, url="https://pypi.org/", disable_cache=False, fallback=True): super(PyPiRepository, self).__init__() + @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 + def find_packages( self, name, # type: str @@ -421,11 +445,64 @@ def _get_info_from_urls( return info 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]] diff --git a/tests/repositories/test_pypi_repository.py b/tests/repositories/test_pypi_repository.py index 82c696c475e..bee32dc5bcd 100644 --- a/tests/repositories/test_pypi_repository.py +++ b/tests/repositories/test_pypi_repository.py @@ -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] @@ -103,6 +113,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)