From ad2b07898df8d62e9abd642c965f3dc5f38c965e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 21 May 2019 11:10:59 -0700 Subject: [PATCH] Fix pip-install to respect --ignore-requires-python. --- news/6371.bugfix | 2 ++ src/pip/_internal/cli/base_command.py | 7 ++++- src/pip/_internal/commands/install.py | 1 + src/pip/_internal/index.py | 40 +++++++++++++++++++++++---- tests/unit/test_index.py | 38 +++++++++++++++++-------- 5 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 news/6371.bugfix diff --git a/news/6371.bugfix b/news/6371.bugfix new file mode 100644 index 000000000..837603961 --- /dev/null +++ b/news/6371.bugfix @@ -0,0 +1,2 @@ +Fix ``pip install`` to respect ``--ignore-requires-python`` when evaluating +links. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 23fa6446f..8d71edc6f 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -326,11 +326,15 @@ class RequirementCommand(Command): platform=None, # type: Optional[str] python_versions=None, # type: Optional[List[str]] abi=None, # type: Optional[str] - implementation=None # type: Optional[str] + implementation=None, # type: Optional[str] + ignore_requires_python=None, # type: Optional[bool] ): # type: (...) -> PackageFinder """ Create a package finder appropriate to this requirement command. + + :param ignore_requires_python: Whether to ignore incompatible + "Requires-Python" values in links. Defaults to False. """ index_urls = [options.index_url] + options.extra_index_urls if options.no_index: @@ -352,4 +356,5 @@ class RequirementCommand(Command): abi=abi, implementation=implementation, prefer_binary=options.prefer_binary, + ignore_requires_python=ignore_requires_python, ) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 788268ef5..51a657e47 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -297,6 +297,7 @@ class InstallCommand(RequirementCommand): python_versions=python_versions, abi=options.abi, implementation=options.implementation, + ignore_requires_python=options.ignore_requires_python, ) build_delete = (not (options.no_clean or options.build_dir)) wheel_cache = WheelCache(options.cache_dir, options.format_control) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 8fdc6955d..ec2e8e561 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -256,10 +256,20 @@ def _get_html_page(link, session=None): return None -def _check_link_requires_python(link, version_info): +def _check_link_requires_python( + link, # type: Link + version_info, # type: Tuple[int, ...] + ignore_requires_python=False, # type: bool +): + # type: (...) -> bool """ - Return whether the link's Requires-Python supports the given Python - version. + 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 ignore_requires_python: Whether to ignore the "Requires-Python" + value if the given Python version isn't compatible. """ try: support_this_python = check_requires_python( @@ -273,11 +283,18 @@ def _check_link_requires_python(link, version_info): else: if not support_this_python: version = '.'.join(map(str, version_info)) + if not ignore_requires_python: + logger.debug( + 'Link requires a different Python (%s not in: %r): %s', + version, link.requires_python, link, + ) + return False + logger.debug( - 'Link requires a different Python (%s not in: %r): %s', + 'Ignoring failed Requires-Python check (%s not in: %r) ' + 'for link: %s', version, link.requires_python, link, ) - return False return True @@ -295,6 +312,7 @@ class CandidateEvaluator(object): prefer_binary=False, # type: bool allow_all_prereleases=False, # type: bool py_version_info=None, # type: Optional[Tuple[int, ...]] + ignore_requires_python=None, # type: Optional[bool] ): # type: (...) -> None """ @@ -303,12 +321,17 @@ class CandidateEvaluator(object): 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 ignore_requires_python: Whether to ignore incompatible + "Requires-Python" values in links. Defaults to False. """ if py_version_info is None: py_version_info = sys.version_info[:3] + if ignore_requires_python is None: + ignore_requires_python = False py_version = '.'.join(map(str, py_version_info[:2])) + self._ignore_requires_python = ignore_requires_python self._prefer_binary = prefer_binary self._py_version = py_version self._py_version_info = py_version_info @@ -383,6 +406,7 @@ class CandidateEvaluator(object): supports_python = _check_link_requires_python( link, version_info=self._py_version_info, + ignore_requires_python=self._ignore_requires_python, ) if not supports_python: # Return None for the reason text to suppress calling @@ -575,7 +599,8 @@ class PackageFinder(object): versions=None, # type: Optional[List[str]] abi=None, # type: Optional[str] implementation=None, # type: Optional[str] - prefer_binary=False # type: bool + prefer_binary=False, # type: bool + ignore_requires_python=None, # type: Optional[bool] ): # type: (...) -> PackageFinder """Create a PackageFinder. @@ -599,6 +624,8 @@ class PackageFinder(object): to pep425tags.py in the get_supported() method. :param prefer_binary: Whether to prefer an old, but valid, binary dist over a new source dist. + :param ignore_requires_python: Whether to ignore incompatible + "Requires-Python" values in links. Defaults to False. """ if session is None: raise TypeError( @@ -634,6 +661,7 @@ class PackageFinder(object): candidate_evaluator = CandidateEvaluator( valid_tags=valid_tags, prefer_binary=prefer_binary, allow_all_prereleases=allow_all_prereleases, + ignore_requires_python=ignore_requires_python, ) # If we don't have TLS enabled, then WARN if anyplace we're looking diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 09e8e0eb3..24eb7c6e0 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -34,20 +34,34 @@ def check_caplog(caplog, expected_level, expected_message): assert record.message == expected_message -def test_check_link_requires_python__incompatible_python(caplog): - """ - Test the log message for an incompatible Python. - """ - link = Link('https://example.com', requires_python='== 3.6.4') - caplog.set_level(logging.DEBUG) - actual = _check_link_requires_python(link, version_info=(3, 6, 5)) - assert actual == False - - expected_message = ( +@pytest.mark.parametrize('ignore_requires_python, expected', [ + (None, ( + False, 'DEBUG', "Link requires a different Python (3.6.5 not in: '== 3.6.4'): " "https://example.com" + )), + (True, ( + True, 'DEBUG', + "Ignoring failed Requires-Python check (3.6.5 not in: '== 3.6.4') " + "for link: https://example.com" + )), +]) +def test_check_link_requires_python__incompatible_python( + caplog, ignore_requires_python, expected, +): + """ + Test an incompatible Python. + """ + expected_return, expected_level, expected_message = expected + link = Link('https://example.com', requires_python='== 3.6.4') + caplog.set_level(logging.DEBUG) + actual = _check_link_requires_python( + link, version_info=(3, 6, 5), + ignore_requires_python=ignore_requires_python, ) - check_caplog(caplog, 'DEBUG', expected_message) + assert actual == expected_return + + check_caplog(caplog, expected_level, expected_message) def test_check_link_requires_python__invalid_requires(caplog): @@ -57,7 +71,7 @@ def test_check_link_requires_python__invalid_requires(caplog): link = Link('https://example.com', requires_python='invalid') caplog.set_level(logging.DEBUG) actual = _check_link_requires_python(link, version_info=(3, 6, 5)) - assert actual == True + assert actual expected_message = ( "Ignoring invalid Requires-Python ('invalid') for link: "