Skip to content

Commit

Permalink
Merge pull request #6518 from cjerdonek/issue-6371-ignore-requires-py…
Browse files Browse the repository at this point in the history
…thon

Fix #6371: make pip install respect --ignore-requires-python
  • Loading branch information
cjerdonek authored May 23, 2019
2 parents 9eccfae + 5528d35 commit eeb74ae
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 20 deletions.
2 changes: 2 additions & 0 deletions news/6371.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix ``pip install`` to respect ``--ignore-requires-python`` when evaluating
links.
7 changes: 6 additions & 1 deletion src/pip/_internal/cli/base_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,11 +326,15 @@ def _build_package_finder(
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:
Expand All @@ -352,4 +356,5 @@ def _build_package_finder(
abi=abi,
implementation=implementation,
prefer_binary=options.prefer_binary,
ignore_requires_python=ignore_requires_python,
)
1 change: 1 addition & 0 deletions src/pip/_internal/commands/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ def run(self, options, args):
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)
Expand Down
81 changes: 63 additions & 18 deletions src/pip/_internal/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

"""
Expand All @@ -269,6 +312,7 @@ def __init__(
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
"""
Expand All @@ -277,12 +321,17 @@ def __init__(
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
Expand Down Expand Up @@ -354,23 +403,15 @@ def evaluate_link(self, link, search):
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)

Expand Down Expand Up @@ -558,7 +599,8 @@ def create(
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.
Expand All @@ -582,6 +624,8 @@ def create(
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(
Expand Down Expand Up @@ -617,6 +661,7 @@ def create(
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
Expand Down
95 changes: 94 additions & 1 deletion tests/unit/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,78 @@

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", [
Expand All @@ -37,6 +104,32 @@ def test_init__py_version_default(self):
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):
"""
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/test_packaging.py
Original file line number Diff line number Diff line change
@@ -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))

0 comments on commit eeb74ae

Please sign in to comment.