mirror of
https://github.com/pypa/pip
synced 2023-12-13 21:30:23 +01:00
Merge pull request #6577 from cjerdonek/issue-5369-download-python-version
Update pip download to respect --python-version
This commit is contained in:
commit
9c8b2ea759
9 changed files with 177 additions and 47 deletions
2
news/5369.bugfix
Normal file
2
news/5369.bugfix
Normal file
|
@ -0,0 +1,2 @@
|
|||
Update ``pip download`` to respect the given ``--python-version`` when checking
|
||||
``"Requires-Python"``.
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue