diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 05dc2339443fc2777608f81f337302fa1d4d02d5..cb470f760a5b0cc0ea1947d3757c433138ee045c 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -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.""" diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index a4550bcfed37f5732def5b9b5614bf89541b9d7a..4a8498bda48e20374ed3b3ed6a45141f8a75e89b 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -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(