summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris Jerdonek <chris.jerdonek@gmail.com>2019-06-06 01:17:18 -0700
committerChris Jerdonek <chris.jerdonek@gmail.com>2019-06-06 13:20:29 -0700
commit8dbf88dff7ffa3a2a81d450549a117f181d073c1 (patch)
tree796da750281b9bc6fd676c9df0b2020fc7d90849
parente6b1070e145a5e95aca5d2c08aa3dce32922742e (diff)
downloadpip-8dbf88dff7ffa3a2a81d450549a117f181d073c1.tar.gz
Update pip-download to respect --python-version.
-rw-r--r--news/5369.bugfix2
-rw-r--r--src/pip/_internal/commands/download.py1
-rw-r--r--src/pip/_internal/index.py22
-rw-r--r--src/pip/_internal/legacy_resolve.py12
-rw-r--r--src/pip/_internal/utils/packaging.py11
-rw-r--r--tests/functional/test_download.py60
-rw-r--r--tests/unit/test_index.py61
7 files changed, 126 insertions, 43 deletions
diff --git a/news/5369.bugfix b/news/5369.bugfix
new file mode 100644
index 000000000..e17f88289
--- /dev/null
+++ b/news/5369.bugfix
@@ -0,0 +1,2 @@
+Update ``pip download`` to respect the given ``--python-version`` when checking
+``"Requires-Python"``.
diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py
index b2c9ce49e..abdd1c2d0 100644
--- a/src/pip/_internal/commands/download.py
+++ b/src/pip/_internal/commands/download.py
@@ -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,
diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py
index d6e30a483..16ace1a45 100644
--- a/src/pip/_internal/index.py
+++ b/src/pip/_internal/index.py
@@ -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,
)
diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py
index a6714cb1f..1cc8531d1 100644
--- a/src/pip/_internal/legacy_resolve.py
+++ b/src/pip/_internal/legacy_resolve.py
@@ -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
diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py
index 2d80920df..68aa86edb 100644
--- a/src/pip/_internal/utils/packaging.py
+++ b/src/pip/_internal/utils/packaging.py
@@ -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
diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py
index e318aa004..b216450db 100644
--- a/tests/functional/test_download.py
+++ b/tests/functional/test_download.py
@@ -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
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index 513826d61..1d6c48040 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -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)
+
+ # 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_default(self):
+ 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):