Merge pull request #6577 from cjerdonek/issue-5369-download-python-version

Update pip download to respect --python-version
This commit is contained in:
Chris Jerdonek 2019-06-07 13:55:37 -07:00 committed by GitHub
commit 9c8b2ea759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 177 additions and 47 deletions

2
news/5369.bugfix Normal file
View File

@ -0,0 +1,2 @@
Update ``pip download`` to respect the given ``--python-version`` when checking
``"Requires-Python"``.

View File

@ -152,6 +152,7 @@ class DownloadCommand(RequirementCommand):
upgrade_strategy="to-satisfy-only",
force_reinstall=False,
ignore_dependencies=options.ignore_dependencies,
py_version_info=options.python_version,
ignore_requires_python=False,
ignore_installed=True,
isolated=options.isolated_mode,

View File

@ -34,7 +34,7 @@ from pip._internal.utils.compat import ipaddress
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import (
ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS, WHEEL_EXTENSION, normalize_path,
path_to_url, redact_password_from_url,
normalize_version_info, path_to_url, redact_password_from_url,
)
from pip._internal.utils.packaging import check_requires_python
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
@ -258,7 +258,7 @@ def _get_html_page(link, session=None):
def _check_link_requires_python(
link, # type: Link
version_info, # type: Tuple[int, ...]
version_info, # type: Tuple[int, int, int]
ignore_requires_python=False, # type: bool
):
# type: (...) -> bool
@ -266,8 +266,8 @@ def _check_link_requires_python(
Return whether the given Python version is compatible with a link's
"Requires-Python" value.
:param version_info: The Python version to use to check, as a 3-tuple
of ints (major-minor-micro).
:param version_info: A 3-tuple of ints representing the Python
major-minor-micro version to check.
:param ignore_requires_python: Whether to ignore the "Requires-Python"
value if the given Python version isn't compatible.
"""
@ -311,16 +311,17 @@ class CandidateEvaluator(object):
valid_tags, # type: List[Pep425Tag]
prefer_binary=False, # type: bool
allow_all_prereleases=False, # type: bool
py_version_info=None, # type: Optional[Tuple[int, ...]]
py_version_info=None, # type: Optional[Tuple[int, int, int]]
ignore_requires_python=None, # type: Optional[bool]
):
# type: (...) -> None
"""
:param allow_all_prereleases: Whether to allow all pre-releases.
:param py_version_info: The Python version, as a 3-tuple of ints
representing a major-minor-micro version, to use to check both
the Python version embedded in the filename and the package's
"Requires-Python" metadata. Defaults to `sys.version_info[:3]`.
:param py_version_info: A 3-tuple of ints representing the Python
major-minor-micro version to use to check both the Python version
embedded in the filename and the package's "Requires-Python"
metadata. If None (the default), then `sys.version_info[:3]`
will be used.
:param ignore_requires_python: Whether to ignore incompatible
"Requires-Python" values in links. Defaults to False.
"""
@ -666,6 +667,8 @@ class PackageFinder(object):
else:
versions = None
py_version_info = normalize_version_info(py_version_info)
# The valid tags to check potential found wheel candidates against
valid_tags = get_supported(
versions=versions,
@ -676,6 +679,7 @@ class PackageFinder(object):
candidate_evaluator = CandidateEvaluator(
valid_tags=valid_tags, prefer_binary=prefer_binary,
allow_all_prereleases=allow_all_prereleases,
py_version_info=py_version_info,
ignore_requires_python=ignore_requires_python,
)

View File

@ -23,7 +23,9 @@ from pip._internal.exceptions import (
)
from pip._internal.req.constructors import install_req_from_req_string
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import dist_in_usersite, ensure_dir
from pip._internal.utils.misc import (
dist_in_usersite, ensure_dir, normalize_version_info,
)
from pip._internal.utils.packaging import (
check_requires_python, get_requires_python,
)
@ -46,7 +48,7 @@ logger = logging.getLogger(__name__)
def _check_dist_requires_python(
dist, # type: pkg_resources.Distribution
version_info, # type: Tuple[int, ...]
version_info, # type: Tuple[int, int, int]
ignore_requires_python=False, # type: bool
):
# type: (...) -> None
@ -54,8 +56,8 @@ def _check_dist_requires_python(
Check whether the given Python version is compatible with a distribution's
"Requires-Python" value.
:param version_info: The Python version to use to check, as a 3-tuple
of ints (major-minor-micro).
:param version_info: A 3-tuple of ints representing the Python
major-minor-micro version to check.
:param ignore_requires_python: Whether to ignore the "Requires-Python"
value if the given Python version isn't compatible.
@ -121,6 +123,8 @@ class Resolver(object):
if py_version_info is None:
py_version_info = sys.version_info[:3]
else:
py_version_info = normalize_version_info(py_version_info)
self._py_version_info = py_version_info

View File

@ -45,13 +45,23 @@ else:
if MYPY_CHECK_RUNNING:
from typing import (
Optional, Tuple, Iterable, List, Match, Union, Any, Mapping, Text,
AnyStr, Container
Any, AnyStr, Container, Iterable, List, Mapping, Match, Optional, Text,
Union,
)
from pip._vendor.pkg_resources import Distribution
from pip._internal.models.link import Link
from pip._internal.utils.ui import SpinnerInterface
try:
from typing import cast, Tuple
VersionInfo = Tuple[int, int, int]
except ImportError:
# typing's cast() isn't supported in code comments, so we need to
# define a dummy, no-op version.
def cast(typ, val):
return val
VersionInfo = None
__all__ = ['rmtree', 'display_path', 'backup_dir',
'ask', 'splitext',
@ -94,6 +104,29 @@ except ImportError:
logger.debug('lzma module is not available')
def normalize_version_info(py_version_info):
# type: (Optional[Tuple[int, ...]]) -> Optional[Tuple[int, int, int]]
"""
Convert a tuple of ints representing a Python version to one of length
three.
:param py_version_info: a tuple of ints representing a Python version,
or None to specify no version. The tuple can have any length.
:return: a tuple of length three if `py_version_info` is non-None.
Otherwise, return `py_version_info` unchanged (i.e. None).
"""
if py_version_info is None:
return None
if len(py_version_info) < 3:
py_version_info += (3 - len(py_version_info)) * (0,)
elif len(py_version_info) > 3:
py_version_info = py_version_info[:3]
return cast(VersionInfo, py_version_info)
def ensure_dir(path):
# type: (AnyStr) -> None
"""os.path.makedirs without EEXIST."""

View File

@ -22,16 +22,15 @@ logger = logging.getLogger(__name__)
def check_requires_python(requires_python, version_info):
# type: (Optional[str], Tuple[int, ...]) -> bool
"""
Check if the given Python version matches a `requires_python` specifier.
Check if the given Python version matches a "Requires-Python" specifier.
:param version_info: A 3-tuple of ints representing the Python
:param version_info: A 3-tuple of ints representing a Python
major-minor-micro version to check (e.g. `sys.version_info[:3]`).
Returns `True` if the version of python in use matches the requirement.
Returns `False` if the version of python in use does not matches the
requirement.
:return: `True` if the given Python version satisfies the requirement.
Otherwise, return `False`.
Raises an InvalidSpecifier if `requires_python` have an invalid format.
:raises InvalidSpecifier: If `requires_python` has an invalid format.
"""
if requires_python is None:
# The package provides no information

View File

@ -1,3 +1,4 @@
import os.path
import textwrap
import pytest
@ -388,7 +389,7 @@ class TestDownloadPlatformManylinuxes(object):
)
def test_download_specify_python_version(script, data):
def test_download__python_version(script, data):
"""
Test using "pip download --python-version" to download a .whl archive
supported for a specific interpreter
@ -477,6 +478,63 @@ def test_download_specify_python_version(script, data):
)
def make_wheel_with_python_requires(script, package_name, python_requires):
"""
Create a wheel using the given python_requires.
:return: the path to the wheel file.
"""
package_dir = script.scratch_path / package_name
package_dir.mkdir()
text = textwrap.dedent("""\
from setuptools import setup
setup(name='{}',
python_requires='{}',
version='1.0')
""").format(package_name, python_requires)
package_dir.join('setup.py').write(text)
script.run(
'python', 'setup.py', 'bdist_wheel', '--universal', cwd=package_dir,
)
file_name = '{}-1.0-py2.py3-none-any.whl'.format(package_name)
return package_dir / 'dist' / file_name
def test_download__python_version_used_for_python_requires(
script, data, with_wheel,
):
"""
Test that --python-version is used for the Requires-Python check.
"""
wheel_path = make_wheel_with_python_requires(
script, 'mypackage', python_requires='==3.2',
)
wheel_dir = os.path.dirname(wheel_path)
def make_args(python_version):
return [
'download', '--no-index', '--find-links', wheel_dir,
'--only-binary=:all:',
'--dest', '.',
'--python-version', python_version,
'mypackage==1.0',
]
args = make_args('33')
result = script.pip(*args, expect_error=True)
expected_err = (
"ERROR: Package 'mypackage' requires a different Python: "
"3.3.0 not in '==3.2'"
)
assert expected_err in result.stderr, 'stderr: {}'.format(result.stderr)
# Now try with a --python-version that satisfies the Requires-Python.
args = make_args('32')
script.pip(*args) # no exception
def test_download_specify_abi(script, data):
"""
Test using "pip download --abi" to download a .whl archive

View File

@ -13,6 +13,8 @@ from pip._internal.index import (
_egg_info_matches, _find_name_version_sep, _get_html_page,
)
CURRENT_PY_VERSION_INFO = sys.version_info[:3]
@pytest.mark.parametrize('requires_python, expected', [
('== 3.6.4', False),
@ -82,27 +84,33 @@ def test_check_link_requires_python__invalid_requires(caplog):
class TestCandidateEvaluator:
@pytest.mark.parametrize("version_info, expected", [
@pytest.mark.parametrize('py_version_info, expected_py_version', [
((2, 7, 14), '2.7'),
((3, 6, 5), '3.6'),
# Check a minor version with two digits.
((3, 10, 1), '3.10'),
])
def test_init__py_version(self, version_info, expected):
def test_init__py_version_info(self, py_version_info, expected_py_version):
"""
Test the _py_version attribute.
Test the py_version_info argument.
"""
evaluator = CandidateEvaluator([], py_version_info=version_info)
assert evaluator._py_version == expected
evaluator = CandidateEvaluator([], py_version_info=py_version_info)
def test_init__py_version_default(self):
# The _py_version_info attribute should be set as is.
assert evaluator._py_version_info == py_version_info
assert evaluator._py_version == expected_py_version
def test_init__py_version_info_none(self):
"""
Test the _py_version attribute's default value.
Test passing None for the py_version_info argument.
"""
evaluator = CandidateEvaluator([])
evaluator = CandidateEvaluator([], py_version_info=None)
# Get the index of the second dot.
index = sys.version.find('.', 2)
assert evaluator._py_version == sys.version[:index]
current_major_minor = sys.version[:index] # e.g. "3.6"
assert evaluator._py_version_info == CURRENT_PY_VERSION_INFO
assert evaluator._py_version == current_major_minor
@pytest.mark.parametrize(
'py_version_info,ignore_requires_python,expected', [
@ -151,30 +159,37 @@ class TestCandidateEvaluator:
class TestPackageFinder:
@pytest.mark.parametrize('version_info, expected', [
((2,), ['2']),
((3,), ['3']),
((3, 6,), ['36']),
# Test a tuple of length 3.
((3, 6, 5), ['36']),
@pytest.mark.parametrize('py_version_info, expected', [
# Test tuples of varying lengths.
((), (None, (0, 0, 0))),
((2, ), (['2'], (2, 0, 0))),
((3, ), (['3'], (3, 0, 0))),
((3, 6,), (['36'], (3, 6, 0))),
((3, 6, 5), (['36'], (3, 6, 5))),
# Test a 2-digit minor version.
((3, 10), ['310']),
# Test falsey values.
(None, None),
((), None),
((3, 10), (['310'], (3, 10, 0))),
# Test passing None.
(None, (None, CURRENT_PY_VERSION_INFO)),
])
@patch('pip._internal.index.get_supported')
def test_create__py_version_info(
self, mock_get_supported, version_info, expected,
self, mock_get_supported, py_version_info, expected,
):
"""
Test that the py_version_info argument is handled correctly.
"""
PackageFinder.create(
[], [], py_version_info=version_info, session=object(),
expected_versions, expected_evaluator_info = expected
finder = PackageFinder.create(
[], [], py_version_info=py_version_info, session=object(),
)
actual = mock_get_supported.call_args[1]['versions']
assert actual == expected
assert actual == expected_versions
# For candidate_evaluator, we only need to test _py_version_info
# since setting _py_version correctly is tested in
# TestCandidateEvaluator.
evaluator = finder.candidate_evaluator
assert evaluator._py_version_info == expected_evaluator_info
def test_sort_locations_file_expand_dir(data):

View File

@ -29,8 +29,9 @@ from pip._internal.utils.glibc import check_glibc_version
from pip._internal.utils.hashes import Hashes, MissingHashes
from pip._internal.utils.misc import (
call_subprocess, egg_link_path, ensure_dir, format_command_args,
get_installed_distributions, get_prog, normalize_path, path_to_url,
redact_netloc, redact_password_from_url, remove_auth_from_url, rmtree,
get_installed_distributions, get_prog, normalize_path,
normalize_version_info, path_to_url, redact_netloc,
redact_password_from_url, remove_auth_from_url, rmtree,
split_auth_from_netloc, split_auth_netloc_from_url, untar_file, unzip_file,
)
from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
@ -700,6 +701,19 @@ class TestGlibc(object):
assert False
@pytest.mark.parametrize('version_info, expected', [
(None, None),
((), (0, 0, 0)),
((3, ), (3, 0, 0)),
((3, 6), (3, 6, 0)),
((3, 6, 2), (3, 6, 2)),
((3, 6, 2, 4), (3, 6, 2)),
])
def test_normalize_version_info(version_info, expected):
actual = normalize_version_info(version_info)
assert actual == expected
class TestGetProg(object):
@pytest.mark.parametrize(