Merge pull request #6518 from cjerdonek/issue-6371-ignore-requires-python

Fix #6371: make pip install respect --ignore-requires-python
This commit is contained in:
Chris Jerdonek 2019-05-22 22:38:20 -07:00 committed by GitHub
commit eeb74aeb29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 20 deletions

2
news/6371.bugfix Normal file
View File

@ -0,0 +1,2 @@
Fix ``pip install`` to respect ``--ignore-requires-python`` when evaluating
links.

View File

@ -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,
)

View File

@ -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)

View File

@ -256,6 +256,49 @@ def _get_html_page(link, session=None):
return None
def _check_link_requires_python(
link, # type: Link
version_info, # type: Tuple[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 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(
link.requires_python, version_info=version_info,
)
except specifiers.InvalidSpecifier:
logger.debug(
"Ignoring invalid Requires-Python (%r) for link: %s",
link.requires_python, link,
)
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(
'Ignoring failed Requires-Python check (%s not in: %r) '
'for link: %s',
version, link.requires_python, link,
)
return True
class CandidateEvaluator(object):
"""
@ -269,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
"""
@ -277,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
@ -354,23 +403,15 @@ class CandidateEvaluator(object):
py_version = match.group(1)
if py_version != self._py_version:
return (False, 'Python version is incorrect')
try:
support_this_python = check_requires_python(
link.requires_python, version_info=self._py_version_info,
)
except specifiers.InvalidSpecifier:
logger.debug("Package %s has an invalid Requires-Python entry: %s",
link.filename, link.requires_python)
else:
if not support_this_python:
logger.debug(
"The package %s is incompatible with the python "
"version in use. Acceptable python versions are: %s",
link, link.requires_python,
)
# Return None for the reason text to suppress calling
# _log_skipped_link().
return (False, None)
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
# _log_skipped_link().
return (False, None)
logger.debug('Found link %s, version: %s', link, version)
@ -558,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.
@ -582,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(
@ -617,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

View File

@ -8,11 +8,78 @@ from pip._vendor import html5lib, requests
from pip._internal.download import PipSession
from pip._internal.index import (
CandidateEvaluator, Link, PackageFinder, _clean_link, _determine_base_url,
CandidateEvaluator, Link, PackageFinder, Search,
_check_link_requires_python, _clean_link, _determine_base_url,
_egg_info_matches, _find_name_version_sep, _get_html_page,
)
@pytest.mark.parametrize('requires_python, expected', [
('== 3.6.4', False),
('== 3.6.5', True),
# Test an invalid Requires-Python value.
('invalid', True),
])
def test_check_link_requires_python(requires_python, expected):
version_info = (3, 6, 5)
link = Link('https://example.com', requires_python=requires_python)
actual = _check_link_requires_python(link, version_info)
assert actual == expected
def check_caplog(caplog, expected_level, expected_message):
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == expected_level
assert record.message == 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,
)
assert actual == expected_return
check_caplog(caplog, expected_level, expected_message)
def test_check_link_requires_python__invalid_requires(caplog):
"""
Test the log message for an invalid Requires-Python.
"""
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
expected_message = (
"Ignoring invalid Requires-Python ('invalid') for link: "
"https://example.com"
)
check_caplog(caplog, 'DEBUG', expected_message)
class TestCandidateEvaluator:
@pytest.mark.parametrize("version_info, expected", [
@ -37,6 +104,32 @@ class TestCandidateEvaluator:
index = sys.version.find('.', 2)
assert evaluator._py_version == sys.version[:index]
@pytest.mark.parametrize(
'py_version_info,ignore_requires_python,expected', [
((3, 6, 5), None, (True, '1.12')),
# Test an incompatible Python.
((3, 6, 4), None, (False, None)),
# Test an incompatible Python with ignore_requires_python=True.
((3, 6, 4), True, (True, '1.12')),
],
)
def test_evaluate_link(
self, py_version_info, ignore_requires_python, expected,
):
link = Link(
'https://example.com/#egg=twine-1.12',
requires_python='== 3.6.5',
)
search = Search(
supplied='twine', canonical='twine', formats=['source'],
)
evaluator = CandidateEvaluator(
[], py_version_info=py_version_info,
ignore_requires_python=ignore_requires_python,
)
actual = evaluator.evaluate_link(link, search=search)
assert actual == expected
def test_sort_locations_file_expand_dir(data):
"""

View File

@ -0,0 +1,22 @@
import pytest
from pip._vendor.packaging import specifiers
from pip._internal.utils.packaging import check_requires_python
@pytest.mark.parametrize('version_info, requires_python, expected', [
((3, 6, 5), '== 3.6.4', False),
((3, 6, 5), '== 3.6.5', True),
((3, 6, 5), None, True),
])
def test_check_requires_python(version_info, requires_python, expected):
actual = check_requires_python(requires_python, version_info)
assert actual == expected
def test_check_requires_python__invalid():
"""
Test an invalid Requires-Python value.
"""
with pytest.raises(specifiers.InvalidSpecifier):
check_requires_python('invalid', (3, 6, 5))