Skip to content

Commit

Permalink
Update pip-download to respect --python-version.
Browse files Browse the repository at this point in the history
  • Loading branch information
cjerdonek committed Jun 6, 2019
1 parent e6b1070 commit 8dbf88d
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 43 deletions.
2 changes: 2 additions & 0 deletions news/5369.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update ``pip download`` to respect the given ``--python-version`` when checking
``"Requires-Python"``.
1 change: 1 addition & 0 deletions src/pip/_internal/commands/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def run(self, options, args):
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,
Expand Down
22 changes: 13 additions & 9 deletions src/pip/_internal/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
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
Expand Down Expand Up @@ -258,16 +258,16 @@ 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
"""
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.
"""
Expand Down Expand Up @@ -311,16 +311,17 @@ def __init__(
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.
"""
Expand Down Expand Up @@ -666,6 +667,8 @@ def create(
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,
Expand All @@ -676,6 +679,7 @@ def create(
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,
)

Expand Down
12 changes: 8 additions & 4 deletions src/pip/_internal/legacy_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
)
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,
)
Expand All @@ -46,16 +48,16 @@

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
"""
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.
Expand Down Expand Up @@ -121,6 +123,8 @@ def __init__(

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

Expand Down
11 changes: 5 additions & 6 deletions src/pip/_internal/utils/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,15 @@
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
Expand Down
60 changes: 59 additions & 1 deletion tests/functional/test_download.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os.path
import textwrap

import pytest
Expand Down Expand Up @@ -388,7 +389,7 @@ def test_explicit_platform_only(self, data, script):
)


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
Expand Down Expand Up @@ -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
Expand Down
61 changes: 38 additions & 23 deletions tests/unit/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
_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),
Expand Down Expand Up @@ -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', [
Expand Down Expand Up @@ -151,30 +159,37 @@ def test_evaluate_link__incompatible_wheel(self):

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):
Expand Down

0 comments on commit 8dbf88d

Please sign in to comment.