diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 8d71edc6f21..959b82a1c66 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -324,7 +324,7 @@ def _build_package_finder( options, # type: Values session, # type: PipSession platform=None, # type: Optional[str] - python_versions=None, # type: Optional[List[str]] + py_version_info=None, # type: Optional[Tuple[int, ...]] abi=None, # type: Optional[str] implementation=None, # type: Optional[str] ignore_requires_python=None, # type: Optional[bool] @@ -352,7 +352,7 @@ def _build_package_finder( allow_all_prereleases=options.pre, session=session, platform=platform, - versions=python_versions, + py_version_info=py_version_info, abi=abi, implementation=implementation, prefer_binary=options.prefer_binary, diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 96c1bbdbf3d..f5d20dd4f31 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -24,7 +24,7 @@ from pip._internal.utils.ui import BAR_TYPES if MYPY_CHECK_RUNNING: - from typing import Any, Callable, Dict, Optional + from typing import Any, Callable, Dict, Optional, Tuple from optparse import OptionParser, Values from pip._internal.cli.parser import ConfigOptionParser @@ -478,11 +478,36 @@ def only_binary(): ) # type: Callable[..., Option] +# This was made a separate function for unit-testing purposes. +def _convert_python_version(value): + # type: (str) -> Tuple[int, ...] + """ + Convert a string like "3" or "34" into a tuple of ints. + """ + if len(value) == 1: + parts = [value] + else: + parts = [value[0], value[1:]] + + return tuple(int(part) for part in parts) + + +def _handle_python_version(option, opt_str, value, parser): + # type: (Option, str, str, OptionParser) -> None + """ + Convert a string like "3" or "34" into a tuple of ints. + """ + version_info = _convert_python_version(value) + parser.values.python_version = version_info + + python_version = partial( Option, '--python-version', dest='python_version', metavar='python_version', + action='callback', + callback=_handle_python_version, type='str', default=None, help=("Only use wheels compatible with Python " "interpreter version . If not specified, then the " diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index a5e9b840f9a..b2c9ce49e0d 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -88,11 +88,6 @@ def run(self, options, args): # of the RequirementSet code require that property. options.editables = [] - if options.python_version: - python_versions = [options.python_version] - else: - python_versions = None - cmdoptions.check_dist_restriction(options) options.src_dir = os.path.abspath(options.src_dir) @@ -105,7 +100,7 @@ def run(self, options, args): options=options, session=session, platform=options.platform, - python_versions=python_versions, + py_version_info=options.python_version, abi=options.abi, implementation=options.implementation, ) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a646991bbb4..6898cb276db 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -251,11 +251,6 @@ def run(self, options, args): cmdoptions.check_dist_restriction(options, check_target=True) - if options.python_version: - python_versions = [options.python_version] - else: - python_versions = None - options.src_dir = os.path.abspath(options.src_dir) install_options = options.install_options or [] if options.use_user_site: @@ -294,7 +289,7 @@ def run(self, options, args): options=options, session=session, platform=options.platform, - python_versions=python_versions, + py_version_info=options.python_version, abi=options.abi, implementation=options.implementation, ignore_requires_python=options.ignore_requires_python, diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 5b42a7c7220..88c409e191e 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -29,7 +29,7 @@ from pip._internal.models.format_control import FormatControl from pip._internal.models.index import PyPI from pip._internal.models.link import Link -from pip._internal.pep425tags import get_supported +from pip._internal.pep425tags import get_supported, version_info_to_nodot from pip._internal.utils.compat import ipaddress from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( @@ -604,7 +604,7 @@ def create( session=None, # type: Optional[PipSession] format_control=None, # type: Optional[FormatControl] platform=None, # type: Optional[str] - versions=None, # type: Optional[List[str]] + py_version_info=None, # type: Optional[Tuple[int, ...]] abi=None, # type: Optional[str] implementation=None, # type: Optional[str] prefer_binary=False, # type: bool @@ -624,8 +624,10 @@ def create( packages that can be built on the platform passed in. These packages will only be downloaded for distribution: they will not be built locally. - :param versions: A list of strings or None. This is passed directly - to pep425tags.py in the get_supported() method. + :param py_version_info: An optional tuple of ints representing the + Python version information to use (e.g. `sys.version_info[:3]`). + This can have length 1, 2, or 3. This is used to construct the + value passed to pep425tags.py's get_supported() function. :param abi: A string or None. This is passed directly to pep425tags.py in the get_supported() method. :param implementation: A string or None. This is passed directly @@ -659,6 +661,11 @@ def create( for host in (trusted_hosts if trusted_hosts else []) ] # type: List[SecureOrigin] + if py_version_info: + versions = [version_info_to_nodot(py_version_info)] + else: + versions = None + # The valid tags to check potential found wheel candidates against valid_tags = get_supported( versions=versions, diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 3b68f28d203..07dc148eec3 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -49,6 +49,12 @@ def get_abbr_impl(): return pyimpl +def version_info_to_nodot(version_info): + # type: (Tuple[int, ...]) -> str + # Only use up to the first two numbers. + return ''.join(map(str, version_info[:2])) + + def get_impl_ver(): # type: () -> str """Return implementation version.""" diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py new file mode 100644 index 00000000000..8f0a2f07581 --- /dev/null +++ b/tests/unit/test_cmdoptions.py @@ -0,0 +1,15 @@ +import pytest + +from pip._internal.cli.cmdoptions import _convert_python_version + + +@pytest.mark.parametrize('value, expected', [ + ('2', (2,)), + ('3', (3,)), + ('34', (3, 4)), + # Test a 2-digit minor version. + ('310', (3, 10)), +]) +def test_convert_python_version(value, expected): + actual = _convert_python_version(value) + assert actual == expected diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 094c23af4b3..513826d611e 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -3,7 +3,7 @@ import sys import pytest -from mock import Mock +from mock import Mock, patch from pip._vendor import html5lib, requests from pip._internal.download import PipSession @@ -149,6 +149,34 @@ def test_evaluate_link__incompatible_wheel(self): assert actual == expected +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']), + # Test a 2-digit minor version. + ((3, 10), ['310']), + # Test falsey values. + (None, None), + ((), None), + ]) + @patch('pip._internal.index.get_supported') + def test_create__py_version_info( + self, mock_get_supported, version_info, expected, + ): + """ + Test that the py_version_info argument is handled correctly. + """ + PackageFinder.create( + [], [], py_version_info=version_info, session=object(), + ) + actual = mock_get_supported.call_args[1]['versions'] + assert actual == expected + + def test_sort_locations_file_expand_dir(data): """ Test that a file:// dir gets listdir run with expand_dir diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index 03dbac87fd3..f570de62133 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -6,6 +6,21 @@ from pip._internal import pep425tags +@pytest.mark.parametrize('version_info, expected', [ + ((2,), '2'), + ((2, 8), '28'), + ((3,), '3'), + ((3, 6), '36'), + # Test a tuple of length 3. + ((3, 6, 5), '36'), + # Test a 2-digit minor version. + ((3, 10), '310'), +]) +def test_version_info_to_nodot(version_info, expected): + actual = pep425tags.version_info_to_nodot(version_info) + assert actual == expected + + class TestPEP425Tags(object): def mock_get_config_var(self, **kwd):