1
1
Fork 0
mirror of https://github.com/pypa/pip synced 2023-12-13 21:30:23 +01:00

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] platform=None, # type: Optional[str]
python_versions=None, # type: Optional[List[str]] python_versions=None, # type: Optional[List[str]]
abi=None, # type: Optional[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 # type: (...) -> PackageFinder
""" """
Create a package finder appropriate to this requirement command. 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 index_urls = [options.index_url] + options.extra_index_urls
if options.no_index: if options.no_index:
@ -352,4 +356,5 @@ class RequirementCommand(Command):
abi=abi, abi=abi,
implementation=implementation, implementation=implementation,
prefer_binary=options.prefer_binary, prefer_binary=options.prefer_binary,
ignore_requires_python=ignore_requires_python,
) )

View file

@ -297,6 +297,7 @@ class InstallCommand(RequirementCommand):
python_versions=python_versions, python_versions=python_versions,
abi=options.abi, abi=options.abi,
implementation=options.implementation, implementation=options.implementation,
ignore_requires_python=options.ignore_requires_python,
) )
build_delete = (not (options.no_clean or options.build_dir)) build_delete = (not (options.no_clean or options.build_dir))
wheel_cache = WheelCache(options.cache_dir, options.format_control) wheel_cache = WheelCache(options.cache_dir, options.format_control)

View file

@ -256,6 +256,49 @@ def _get_html_page(link, session=None):
return 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): class CandidateEvaluator(object):
""" """
@ -269,6 +312,7 @@ class CandidateEvaluator(object):
prefer_binary=False, # type: bool prefer_binary=False, # type: bool
allow_all_prereleases=False, # type: bool allow_all_prereleases=False, # type: bool
py_version_info=None, # type: Optional[Tuple[int, ...]] py_version_info=None, # type: Optional[Tuple[int, ...]]
ignore_requires_python=None, # type: Optional[bool]
): ):
# type: (...) -> None # type: (...) -> None
""" """
@ -277,12 +321,17 @@ class CandidateEvaluator(object):
representing a major-minor-micro version, to use to check both representing a major-minor-micro version, to use to check both
the Python version embedded in the filename and the package's the Python version embedded in the filename and the package's
"Requires-Python" metadata. Defaults to `sys.version_info[:3]`. "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: if py_version_info is None:
py_version_info = sys.version_info[:3] 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])) py_version = '.'.join(map(str, py_version_info[:2]))
self._ignore_requires_python = ignore_requires_python
self._prefer_binary = prefer_binary self._prefer_binary = prefer_binary
self._py_version = py_version self._py_version = py_version
self._py_version_info = py_version_info self._py_version_info = py_version_info
@ -354,23 +403,15 @@ class CandidateEvaluator(object):
py_version = match.group(1) py_version = match.group(1)
if py_version != self._py_version: if py_version != self._py_version:
return (False, 'Python version is incorrect') return (False, 'Python version is incorrect')
try:
support_this_python = check_requires_python( supports_python = _check_link_requires_python(
link.requires_python, version_info=self._py_version_info, link, version_info=self._py_version_info,
) ignore_requires_python=self._ignore_requires_python,
except specifiers.InvalidSpecifier: )
logger.debug("Package %s has an invalid Requires-Python entry: %s", if not supports_python:
link.filename, link.requires_python) # Return None for the reason text to suppress calling
else: # _log_skipped_link().
if not support_this_python: return (False, None)
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)
logger.debug('Found link %s, version: %s', link, version) logger.debug('Found link %s, version: %s', link, version)
@ -558,7 +599,8 @@ class PackageFinder(object):
versions=None, # type: Optional[List[str]] versions=None, # type: Optional[List[str]]
abi=None, # type: Optional[str] abi=None, # type: Optional[str]
implementation=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 # type: (...) -> PackageFinder
"""Create a PackageFinder. """Create a PackageFinder.
@ -582,6 +624,8 @@ class PackageFinder(object):
to pep425tags.py in the get_supported() method. to pep425tags.py in the get_supported() method.
:param prefer_binary: Whether to prefer an old, but valid, binary :param prefer_binary: Whether to prefer an old, but valid, binary
dist over a new source dist. 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: if session is None:
raise TypeError( raise TypeError(
@ -617,6 +661,7 @@ class PackageFinder(object):
candidate_evaluator = CandidateEvaluator( candidate_evaluator = CandidateEvaluator(
valid_tags=valid_tags, prefer_binary=prefer_binary, valid_tags=valid_tags, prefer_binary=prefer_binary,
allow_all_prereleases=allow_all_prereleases, 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 # 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.download import PipSession
from pip._internal.index import ( 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, _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: class TestCandidateEvaluator:
@pytest.mark.parametrize("version_info, expected", [ @pytest.mark.parametrize("version_info, expected", [
@ -37,6 +104,32 @@ class TestCandidateEvaluator:
index = sys.version.find('.', 2) index = sys.version.find('.', 2)
assert evaluator._py_version == sys.version[:index] 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): 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))