Skip to content

Commit

Permalink
Allow dotted version strings for --python-version.
Browse files Browse the repository at this point in the history
  • Loading branch information
cjerdonek committed Jun 9, 2019
1 parent 9c8b2ea commit 4bb225d
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 20 deletions.
2 changes: 2 additions & 0 deletions news/6585.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Allow ``--python-version`` to be passed as a dotted version string (e.g.
``3.7`` or ``3.7.3``).
57 changes: 42 additions & 15 deletions src/pip/_internal/cli/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from distutils.util import strtobool
from functools import partial
from optparse import SUPPRESS_HELP, Option, OptionGroup
from textwrap import dedent

from pip._internal.exceptions import CommandError
from pip._internal.locations import USER_CACHE_DIR, src_prefix
Expand Down Expand Up @@ -480,24 +481,49 @@ def only_binary():

# This was made a separate function for unit-testing purposes.
def _convert_python_version(value):
# type: (str) -> Tuple[int, ...]
# type: (str) -> Tuple[Tuple[int, ...], Optional[str]]
"""
Convert a string like "3" or "34" into a tuple of ints.
Convert a version string like "3", "37", or "3.7.3" into a tuple of ints.
:return: A 2-tuple (version_info, error_msg), where `error_msg` is
non-None if and only if there was a parsing error.
"""
if len(value) == 1:
parts = [value]
else:
parts = [value[0], value[1:]]
if not value:
# The empty string is the same as not providing a value.
return (None, None)

parts = value.split('.')
if len(parts) > 3:
return ((), 'at most three version parts are allowed')

if len(parts) == 1:
# Then we are in the case of "3" or "37".
value = parts[0]
if len(value) > 1:
parts = [value[0], value[1:]]

try:
version_info = tuple(int(part) for part in parts)
except ValueError:
return ((), 'each version part must be an integer')

return tuple(int(part) for part in parts)
return (version_info, None)


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.
Handle a provided --python-version value.
"""
version_info = _convert_python_version(value)
version_info, error_msg = _convert_python_version(value)
if error_msg is not None:
msg = (
'invalid --python-version value: {!r}: {}'.format(
value, error_msg,
)
)
raise_option_error(parser, option=option, msg=msg)

parser.values.python_version = version_info


Expand All @@ -509,12 +535,13 @@ def _handle_python_version(option, opt_str, value, parser):
action='callback',
callback=_handle_python_version, type='str',
default=None,
help=("Only use wheels compatible with Python "
"interpreter version <version>. If not specified, then the "
"current system interpreter minor version is used. A major "
"version (e.g. '2') can be specified to match all "
"minor revs of that major version. A minor version "
"(e.g. '34') can also be specified."),
help=dedent("""\
The Python interpreter version to use for wheel and "Requires-Python"
compatibility checks. Defaults to a version derived from the running
interpreter. The version can be specified using up to three dot-separated
integers (e.g. "3" for 3.0.0, "3.7" for 3.7.0, or "3.7.3"). A major-minor
version can also be given as a string without dots (e.g. "37" for 3.7.0).
"""),
) # type: Callable[..., Option]


Expand Down
19 changes: 14 additions & 5 deletions tests/unit/test_cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@


@pytest.mark.parametrize('value, expected', [
('2', (2,)),
('3', (3,)),
('34', (3, 4)),
('', (None, None)),
('2', ((2,), None)),
('3', ((3,), None)),
('3.7', ((3, 7), None)),
('3.7.3', ((3, 7, 3), None)),
# Test strings without dots of length bigger than 1.
('34', ((3, 4), None)),
# Test a 2-digit minor version.
('310', (3, 10)),
('310', ((3, 10), None)),
# Test some values that fail to parse.
('ab', ((), 'each version part must be an integer')),
('3a', ((), 'each version part must be an integer')),
('3.7.a', ((), 'each version part must be an integer')),
('3.7.3.1', ((), 'at most three version parts are allowed')),
])
def test_convert_python_version(value, expected):
actual = _convert_python_version(value)
assert actual == expected
assert actual == expected, 'actual: {!r}'.format(actual)

0 comments on commit 4bb225d

Please sign in to comment.