Skip to content

Commit 6086f71

Browse files
authored
Merge pull request #7960 from uranusjr/requires-python-2
Requires-Python implementation, take 2
2 parents c88fa39 + 557f767 commit 6086f71

File tree

7 files changed

+181
-12
lines changed

7 files changed

+181
-12
lines changed

src/pip/_internal/resolution/resolvelib/candidates.py

+58-4
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import logging
2+
import sys
23

4+
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
35
from pip._vendor.packaging.utils import canonicalize_name
6+
from pip._vendor.packaging.version import Version
47

58
from pip._internal.req.constructors import install_req_from_line
69
from pip._internal.req.req_install import InstallRequirement
10+
from pip._internal.utils.misc import normalize_version_info
11+
from pip._internal.utils.packaging import get_requires_python
712
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
813

914
from .base import Candidate, format_name
1015

1116
if MYPY_CHECK_RUNNING:
12-
from typing import Any, Optional, Sequence, Set
13-
14-
from pip._internal.models.link import Link
17+
from typing import Any, Optional, Sequence, Set, Tuple
1518

1619
from pip._vendor.packaging.version import _BaseVersion
1720
from pip._vendor.pkg_resources import Distribution
1821

22+
from pip._internal.models.link import Link
23+
1924
from .base import Requirement
2025
from .factory import Factory
2126

@@ -95,12 +100,32 @@ def dist(self):
95100
self._version == self.dist.parsed_version)
96101
return self._dist
97102

103+
def _get_requires_python_specifier(self):
104+
# type: () -> Optional[SpecifierSet]
105+
requires_python = get_requires_python(self.dist)
106+
if requires_python is None:
107+
return None
108+
try:
109+
spec = SpecifierSet(requires_python)
110+
except InvalidSpecifier as e:
111+
logger.warning(
112+
"Package %r has an invalid Requires-Python: %s", self.name, e,
113+
)
114+
return None
115+
return spec
116+
98117
def get_dependencies(self):
99118
# type: () -> Sequence[Requirement]
100-
return [
119+
deps = [
101120
self._factory.make_requirement_from_spec(str(r), self._ireq)
102121
for r in self.dist.requires()
103122
]
123+
python_dep = self._factory.make_requires_python_requirement(
124+
self._get_requires_python_specifier(),
125+
)
126+
if python_dep:
127+
deps.append(python_dep)
128+
return deps
104129

105130
def get_install_requirement(self):
106131
# type: () -> Optional[InstallRequirement]
@@ -179,3 +204,32 @@ def get_install_requirement(self):
179204
# depend on the base candidate, and we'll get the
180205
# install requirement from that.
181206
return None
207+
208+
209+
class RequiresPythonCandidate(Candidate):
210+
def __init__(self, py_version_info):
211+
# type: (Optional[Tuple[int, ...]]) -> None
212+
if py_version_info is not None:
213+
version_info = normalize_version_info(py_version_info)
214+
else:
215+
version_info = sys.version_info[:3]
216+
self._version = Version(".".join(str(c) for c in version_info))
217+
218+
@property
219+
def name(self):
220+
# type: () -> str
221+
# Avoid conflicting with the PyPI package "Python".
222+
return "<Python fom Requires-Python>"
223+
224+
@property
225+
def version(self):
226+
# type: () -> _BaseVersion
227+
return self._version
228+
229+
def get_dependencies(self):
230+
# type: () -> Sequence[Requirement]
231+
return []
232+
233+
def get_install_requirement(self):
234+
# type: () -> Optional[InstallRequirement]
235+
return None

src/pip/_internal/resolution/resolvelib/factory.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
22

3-
from .candidates import ExtrasCandidate, LinkCandidate
4-
from .requirements import ExplicitRequirement, SpecifierRequirement
3+
from .candidates import ExtrasCandidate, LinkCandidate, RequiresPythonCandidate
4+
from .requirements import (
5+
ExplicitRequirement,
6+
NoMatchRequirement,
7+
SpecifierRequirement,
8+
)
59

610
if MYPY_CHECK_RUNNING:
7-
from typing import Dict, Set
11+
from typing import Dict, Optional, Set, Tuple
12+
13+
from pip._vendor.packaging.specifiers import SpecifierSet
814

915
from pip._internal.index.package_finder import PackageFinder
1016
from pip._internal.models.link import Link
@@ -21,10 +27,14 @@ def __init__(
2127
finder, # type: PackageFinder
2228
preparer, # type: RequirementPreparer
2329
make_install_req, # type: InstallRequirementProvider
30+
ignore_requires_python, # type: bool
31+
py_version_info=None, # type: Optional[Tuple[int, ...]]
2432
):
2533
# type: (...) -> None
2634
self.finder = finder
2735
self.preparer = preparer
36+
self._python_candidate = RequiresPythonCandidate(py_version_info)
37+
self._ignore_requires_python = ignore_requires_python
2838
self._make_install_req_from_spec = make_install_req
2939
self._candidate_cache = {} # type: Dict[Link, LinkCandidate]
3040

@@ -56,3 +66,16 @@ def make_requirement_from_spec(self, specifier, comes_from):
5666
# type: (str, InstallRequirement) -> Requirement
5767
ireq = self._make_install_req_from_spec(specifier, comes_from)
5868
return self.make_requirement_from_install_req(ireq)
69+
70+
def make_requires_python_requirement(self, specifier):
71+
# type: (Optional[SpecifierSet]) -> Optional[Requirement]
72+
if self._ignore_requires_python or specifier is None:
73+
return None
74+
# The logic here is different from SpecifierRequirement, for which we
75+
# "find" candidates matching the specifier. But for Requires-Python,
76+
# there is always exactly one candidate (the one specified with
77+
# py_version_info). Here we decide whether to return that based on
78+
# whether Requires-Python matches that one candidate or not.
79+
if self._python_candidate.version in specifier:
80+
return ExplicitRequirement(self._python_candidate)
81+
return NoMatchRequirement(self._python_candidate.name)

src/pip/_internal/resolution/resolvelib/requirements.py

+24
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,30 @@ def is_satisfied_by(self, candidate):
3333
return candidate == self.candidate
3434

3535

36+
class NoMatchRequirement(Requirement):
37+
"""A requirement that never matches anything.
38+
39+
Note: Similar to ExplicitRequirement, the caller should handle name
40+
canonicalisation; this class does not perform it.
41+
"""
42+
def __init__(self, name):
43+
# type: (str) -> None
44+
self._name = name
45+
46+
@property
47+
def name(self):
48+
# type: () -> str
49+
return self._name
50+
51+
def find_matches(self):
52+
# type: () -> Sequence[Candidate]
53+
return []
54+
55+
def is_satisfied_by(self, candidate):
56+
# type: (Candidate) -> bool
57+
return False
58+
59+
3660
class SpecifierRequirement(Requirement):
3761
def __init__(self, ireq, factory):
3862
# type: (InstallRequirement, Factory) -> None

src/pip/_internal/resolution/resolvelib/resolver.py

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ def __init__(
4343
finder=finder,
4444
preparer=preparer,
4545
make_install_req=make_install_req,
46+
ignore_requires_python=ignore_requires_python,
47+
py_version_info=py_version_info,
4648
)
4749
self.ignore_dependencies = ignore_dependencies
4850
self._result = None # type: Optional[Result]

tests/functional/test_new_resolver.py

+54
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import json
22

3+
import pytest
4+
35
from tests.lib import create_basic_wheel_for_package
46

57

@@ -137,3 +139,55 @@ def test_new_resolver_installs_extras(script):
137139
assert "WARNING: Invalid extras specified" in result.stderr, str(result)
138140
assert ": missing" in result.stderr, str(result)
139141
assert_installed(script, base="0.1.0", dep="0.1.0")
142+
143+
144+
@pytest.mark.parametrize(
145+
"requires_python, ignore_requires_python, dep_version",
146+
[
147+
# Something impossible to satisfy.
148+
("<2", False, "0.1.0"),
149+
("<2", True, "0.2.0"),
150+
151+
# Something guaranteed to satisfy.
152+
(">=2", False, "0.2.0"),
153+
(">=2", True, "0.2.0"),
154+
],
155+
)
156+
def test_new_resolver_requires_python(
157+
script,
158+
requires_python,
159+
ignore_requires_python,
160+
dep_version,
161+
):
162+
create_basic_wheel_for_package(
163+
script,
164+
"base",
165+
"0.1.0",
166+
depends=["dep"],
167+
)
168+
create_basic_wheel_for_package(
169+
script,
170+
"dep",
171+
"0.1.0",
172+
)
173+
create_basic_wheel_for_package(
174+
script,
175+
"dep",
176+
"0.2.0",
177+
requires_python=requires_python,
178+
)
179+
180+
args = [
181+
"install",
182+
"--unstable-feature=resolver",
183+
"--no-cache-dir",
184+
"--no-index",
185+
"--find-links", script.scratch_path,
186+
]
187+
if ignore_requires_python:
188+
args.append("--ignore-requires-python")
189+
args.append("base")
190+
191+
script.pip(*args)
192+
193+
assert_installed(script, base="0.1.0", dep=dep_version)

tests/lib/__init__.py

+15-5
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,13 @@ def add_file(path, text):
979979

980980

981981
def create_basic_wheel_for_package(
982-
script, name, version, depends=None, extras=None, extra_files=None
982+
script,
983+
name,
984+
version,
985+
depends=None,
986+
extras=None,
987+
requires_python=None,
988+
extra_files=None,
983989
):
984990
if depends is None:
985991
depends = []
@@ -1007,14 +1013,18 @@ def hello():
10071013
for package in packages
10081014
]
10091015

1016+
metadata_updates = {
1017+
"Provides-Extra": list(extras),
1018+
"Requires-Dist": requires_dist,
1019+
}
1020+
if requires_python is not None:
1021+
metadata_updates["Requires-Python"] = requires_python
1022+
10101023
wheel_builder = make_wheel(
10111024
name=name,
10121025
version=version,
10131026
wheel_metadata_updates={"Tag": ["py2-none-any", "py3-none-any"]},
1014-
metadata_updates={
1015-
"Provides-Extra": list(extras),
1016-
"Requires-Dist": requires_dist,
1017-
},
1027+
metadata_updates=metadata_updates,
10181028
extra_metadata_files={"top_level.txt": name},
10191029
extra_files=extra_files,
10201030

tests/unit/resolution_resolvelib/conftest.py

+2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ def factory(finder, preparer):
5252
finder=finder,
5353
preparer=preparer,
5454
make_install_req=install_req_from_line,
55+
ignore_requires_python=False,
56+
py_version_info=None,
5557
)
5658

5759

0 commit comments

Comments
 (0)