Simplify the _check_dist_requires_python() call site.

This commit is contained in:
Chris Jerdonek 2019-05-23 22:59:30 -07:00
parent cc70cf5ba3
commit f82ea77217
5 changed files with 157 additions and 61 deletions

View File

@ -272,7 +272,7 @@ def _check_link_requires_python(
value if the given Python version isn't compatible.
"""
try:
support_this_python = check_requires_python(
is_compatible = check_requires_python(
link.requires_python, version_info=version_info,
)
except specifiers.InvalidSpecifier:
@ -281,7 +281,7 @@ def _check_link_requires_python(
link.requires_python, link,
)
else:
if not support_this_python:
if not is_compatible:
version = '.'.join(map(str, version_info))
if not ignore_requires_python:
logger.debug(

View File

@ -17,7 +17,6 @@ from itertools import chain
from pip._vendor.packaging import specifiers
from pip._internal import exceptions
from pip._internal.exceptions import (
BestVersionAlreadyInstalled, DistributionNotFound, HashError, HashErrors,
UnsupportedPythonVersion,
@ -25,7 +24,9 @@ from pip._internal.exceptions import (
from pip._internal.req.constructors import install_req_from_req_string
from pip._internal.utils.logging import indent_log
from pip._internal.utils.misc import dist_in_usersite, ensure_dir
from pip._internal.utils.packaging import check_requires_python, get_metadata
from pip._internal.utils.packaging import (
check_requires_python, get_requires_python,
)
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
if MYPY_CHECK_RUNNING:
@ -43,34 +44,53 @@ if MYPY_CHECK_RUNNING:
logger = logging.getLogger(__name__)
def check_dist_requires_python(
dist, # type: pkg_resources.Distribution
version_info, # type: Optional[Tuple[int, ...]]
def _check_dist_requires_python(
dist, # type: pkg_resources.Distribution
version_info, # type: Tuple[int, ...]
ignore_requires_python=False, # type: bool
):
# type: (...) -> None
"""
:param version_info: A 3-tuple of ints representing the Python
major-minor-micro version to check (e.g. `sys.version_info[:3]`).
Check whether the given Python version is compatible with a distribution'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.
:raises UnsupportedPythonVersion: When the given Python version isn't
compatible.
"""
pkg_info_dict = get_metadata(dist)
requires_python = pkg_info_dict.get('Requires-Python')
requires_python = get_requires_python(dist)
try:
if not check_requires_python(
is_compatible = check_requires_python(
requires_python, version_info=version_info,
):
raise exceptions.UnsupportedPythonVersion(
"%s requires Python '%s' but the running Python is %s" % (
dist.project_name,
requires_python,
'.'.join(map(str, version_info)),)
)
except specifiers.InvalidSpecifier as e:
)
except specifiers.InvalidSpecifier as exc:
logger.warning(
"Package %s has an invalid Requires-Python entry %s - %s",
dist.project_name, requires_python, e,
"Package %r has an invalid Requires-Python: %s",
dist.project_name, exc,
)
return
if is_compatible:
return
version = '.'.join(map(str, version_info))
if ignore_requires_python:
logger.debug(
'Ignoring failed Requires-Python check for package %r: '
'%s not in %r',
dist.project_name, version, requires_python,
)
return
raise UnsupportedPythonVersion(
'Package {!r} requires a different Python: {} not in {!r}'.format(
dist.project_name, version, requires_python,
))
class Resolver(object):
"""Resolves which packages need to be installed/uninstalled to perform \
@ -92,12 +112,18 @@ class Resolver(object):
force_reinstall, # type: bool
isolated, # type: bool
upgrade_strategy, # type: str
use_pep517=None # type: Optional[bool]
use_pep517=None, # type: Optional[bool]
py_version_info=None, # type: Optional[Tuple[int, ...]]
):
# type: (...) -> None
super(Resolver, self).__init__()
assert upgrade_strategy in self._allowed_strategies
if py_version_info is None:
py_version_info = sys.version_info[:3]
self._py_version_info = py_version_info
self.preparer = preparer
self.finder = finder
self.session = session
@ -329,13 +355,12 @@ class Resolver(object):
# Parse and return dependencies
dist = abstract_dist.dist()
try:
check_dist_requires_python(dist, version_info=sys.version_info[:3])
except UnsupportedPythonVersion as err:
if self.ignore_requires_python:
logger.warning(err.args[0])
else:
raise
# This will raise UnsupportedPythonVersion if the given Python
# version isn't compatible with the distribution's Requires-Python.
_check_dist_requires_python(
dist, version_info=self._py_version_info,
ignore_requires_python=self.ignore_requires_python,
)
more_reqs = [] # type: List[InstallRequirement]

View File

@ -57,6 +57,23 @@ def get_metadata(dist):
return feed_parser.close()
def get_requires_python(dist):
# type: (pkg_resources.Distribution) -> Optional[str]
"""
Return the "Requires-Python" metadata for a distribution, or None
if not present.
"""
pkg_info_dict = get_metadata(dist)
requires_python = pkg_info_dict.get('Requires-Python')
if requires_python is not None:
# Convert to a str to satisfy the type checker, since requires_python
# can be a Header object.
requires_python = str(requires_python)
return requires_python
def get_installer(dist):
# type: (Distribution) -> str
if dist.has_metadata('INSTALLER'):

View File

@ -1305,6 +1305,12 @@ def test_double_install_fail(script):
assert msg in result.stderr
def _get_expected_error_text():
return (
"Package 'pkga' requires a different Python: {} not in '<1.0'"
).format(sys.version.split()[0])
def test_install_incompatible_python_requires(script):
script.scratch_path.join("pkga").mkdir()
pkga_path = script.scratch_path / 'pkga'
@ -1315,8 +1321,7 @@ def test_install_incompatible_python_requires(script):
version='0.1')
"""))
result = script.pip('install', pkga_path, expect_error=True)
assert ("pkga requires Python '<1.0' "
"but the running Python is ") in result.stderr, str(result)
assert _get_expected_error_text() in result.stderr, str(result)
def test_install_incompatible_python_requires_editable(script):
@ -1330,8 +1335,7 @@ def test_install_incompatible_python_requires_editable(script):
"""))
result = script.pip(
'install', '--editable=%s' % pkga_path, expect_error=True)
assert ("pkga requires Python '<1.0' "
"but the running Python is ") in result.stderr, str(result)
assert _get_expected_error_text() in result.stderr, str(result)
def test_install_incompatible_python_requires_wheel(script, with_wheel):
@ -1347,8 +1351,7 @@ def test_install_incompatible_python_requires_wheel(script, with_wheel):
'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkga_path)
result = script.pip('install', './pkga/dist/pkga-0.1-py2.py3-none-any.whl',
expect_error=True)
assert ("pkga requires Python '<1.0' "
"but the running Python is ") in result.stderr
assert _get_expected_error_text() in result.stderr, str(result)
def test_install_compatible_python_requires(script):

View File

@ -1,32 +1,83 @@
import sys
import logging
import pytest
from mock import Mock
from mock import patch
from pip._internal.exceptions import UnsupportedPythonVersion
from pip._internal.resolve import check_dist_requires_python
from pip._internal.resolve import _check_dist_requires_python
class TestCheckRequiresPython(object):
class FakeDist(object):
@pytest.mark.parametrize(
("metadata", "should_raise"),
[
("Name: test\n", False),
("Name: test\nRequires-Python:", False),
("Name: test\nRequires-Python: invalid_spec", False),
("Name: test\nRequires-Python: <=1", True),
],
)
def test_check_requires(self, metadata, should_raise):
fake_dist = Mock(
has_metadata=lambda _: True,
get_metadata=lambda _: metadata)
version_info = sys.version_info[:3]
if should_raise:
with pytest.raises(UnsupportedPythonVersion):
check_dist_requires_python(
fake_dist, version_info=version_info,
)
else:
check_dist_requires_python(fake_dist, version_info=version_info)
def __init__(self, project_name):
self.project_name = project_name
@pytest.fixture
def dist():
return FakeDist('my-project')
@patch('pip._internal.resolve.get_requires_python')
class TestCheckDistRequiresPython(object):
"""
Test _check_dist_requires_python().
"""
def test_compatible(self, mock_get_requires, caplog, dist):
caplog.set_level(logging.DEBUG)
mock_get_requires.return_value = '== 3.6.5'
_check_dist_requires_python(
dist,
version_info=(3, 6, 5),
ignore_requires_python=False,
)
assert not len(caplog.records)
def test_invalid_specifier(self, mock_get_requires, caplog, dist):
caplog.set_level(logging.DEBUG)
mock_get_requires.return_value = 'invalid'
_check_dist_requires_python(
dist,
version_info=(3, 6, 5),
ignore_requires_python=False,
)
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == 'WARNING'
assert record.message == (
"Package 'my-project' has an invalid Requires-Python: "
"Invalid specifier: 'invalid'"
)
def test_incompatible(self, mock_get_requires, dist):
mock_get_requires.return_value = '== 3.6.4'
with pytest.raises(UnsupportedPythonVersion) as exc:
_check_dist_requires_python(
dist,
version_info=(3, 6, 5),
ignore_requires_python=False,
)
assert str(exc.value) == (
"Package 'my-project' requires a different Python: "
"3.6.5 not in '== 3.6.4'"
)
def test_incompatible_with_ignore_requires(
self, mock_get_requires, caplog, dist,
):
caplog.set_level(logging.DEBUG)
mock_get_requires.return_value = '== 3.6.4'
_check_dist_requires_python(
dist,
version_info=(3, 6, 5),
ignore_requires_python=True,
)
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelname == 'DEBUG'
assert record.message == (
"Ignoring failed Requires-Python check for package 'my-project': "
"3.6.5 not in '== 3.6.4'"
)