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:
commit
eeb74aeb29
2
news/6371.bugfix
Normal file
2
news/6371.bugfix
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Fix ``pip install`` to respect ``--ignore-requires-python`` when evaluating
|
||||||
|
links.
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
22
tests/unit/test_packaging.py
Normal file
22
tests/unit/test_packaging.py
Normal 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))
|
Loading…
Reference in a new issue