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

Merge pull request #7960 from uranusjr/requires-python-2

Requires-Python implementation, take 2
This commit is contained in:
Paul Moore 2020-04-02 16:26:51 +01:00 committed by GitHub
commit 6086f71cde
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 181 additions and 12 deletions

View file

@ -1,21 +1,26 @@
import logging
import sys
from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet
from pip._vendor.packaging.utils import canonicalize_name
from pip._vendor.packaging.version import Version
from pip._internal.req.constructors import install_req_from_line
from pip._internal.req.req_install import InstallRequirement
from pip._internal.utils.misc import normalize_version_info
from pip._internal.utils.packaging import get_requires_python
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from .base import Candidate, format_name
if MYPY_CHECK_RUNNING:
from typing import Any, Optional, Sequence, Set
from pip._internal.models.link import Link
from typing import Any, Optional, Sequence, Set, Tuple
from pip._vendor.packaging.version import _BaseVersion
from pip._vendor.pkg_resources import Distribution
from pip._internal.models.link import Link
from .base import Requirement
from .factory import Factory
@ -95,12 +100,32 @@ class LinkCandidate(Candidate):
self._version == self.dist.parsed_version)
return self._dist
def _get_requires_python_specifier(self):
# type: () -> Optional[SpecifierSet]
requires_python = get_requires_python(self.dist)
if requires_python is None:
return None
try:
spec = SpecifierSet(requires_python)
except InvalidSpecifier as e:
logger.warning(
"Package %r has an invalid Requires-Python: %s", self.name, e,
)
return None
return spec
def get_dependencies(self):
# type: () -> Sequence[Requirement]
return [
deps = [
self._factory.make_requirement_from_spec(str(r), self._ireq)
for r in self.dist.requires()
]
python_dep = self._factory.make_requires_python_requirement(
self._get_requires_python_specifier(),
)
if python_dep:
deps.append(python_dep)
return deps
def get_install_requirement(self):
# type: () -> Optional[InstallRequirement]
@ -179,3 +204,32 @@ class ExtrasCandidate(LinkCandidate):
# depend on the base candidate, and we'll get the
# install requirement from that.
return None
class RequiresPythonCandidate(Candidate):
def __init__(self, py_version_info):
# type: (Optional[Tuple[int, ...]]) -> None
if py_version_info is not None:
version_info = normalize_version_info(py_version_info)
else:
version_info = sys.version_info[:3]
self._version = Version(".".join(str(c) for c in version_info))
@property
def name(self):
# type: () -> str
# Avoid conflicting with the PyPI package "Python".
return "<Python fom Requires-Python>"
@property
def version(self):
# type: () -> _BaseVersion
return self._version
def get_dependencies(self):
# type: () -> Sequence[Requirement]
return []
def get_install_requirement(self):
# type: () -> Optional[InstallRequirement]
return None

View file

@ -1,10 +1,16 @@
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
from .candidates import ExtrasCandidate, LinkCandidate
from .requirements import ExplicitRequirement, SpecifierRequirement
from .candidates import ExtrasCandidate, LinkCandidate, RequiresPythonCandidate
from .requirements import (
ExplicitRequirement,
NoMatchRequirement,
SpecifierRequirement,
)
if MYPY_CHECK_RUNNING:
from typing import Dict, Set
from typing import Dict, Optional, Set, Tuple
from pip._vendor.packaging.specifiers import SpecifierSet
from pip._internal.index.package_finder import PackageFinder
from pip._internal.models.link import Link
@ -21,10 +27,14 @@ class Factory(object):
finder, # type: PackageFinder
preparer, # type: RequirementPreparer
make_install_req, # type: InstallRequirementProvider
ignore_requires_python, # type: bool
py_version_info=None, # type: Optional[Tuple[int, ...]]
):
# type: (...) -> None
self.finder = finder
self.preparer = preparer
self._python_candidate = RequiresPythonCandidate(py_version_info)
self._ignore_requires_python = ignore_requires_python
self._make_install_req_from_spec = make_install_req
self._candidate_cache = {} # type: Dict[Link, LinkCandidate]
@ -56,3 +66,16 @@ class Factory(object):
# type: (str, InstallRequirement) -> Requirement
ireq = self._make_install_req_from_spec(specifier, comes_from)
return self.make_requirement_from_install_req(ireq)
def make_requires_python_requirement(self, specifier):
# type: (Optional[SpecifierSet]) -> Optional[Requirement]
if self._ignore_requires_python or specifier is None:
return None
# The logic here is different from SpecifierRequirement, for which we
# "find" candidates matching the specifier. But for Requires-Python,
# there is always exactly one candidate (the one specified with
# py_version_info). Here we decide whether to return that based on
# whether Requires-Python matches that one candidate or not.
if self._python_candidate.version in specifier:
return ExplicitRequirement(self._python_candidate)
return NoMatchRequirement(self._python_candidate.name)

View file

@ -33,6 +33,30 @@ class ExplicitRequirement(Requirement):
return candidate == self.candidate
class NoMatchRequirement(Requirement):
"""A requirement that never matches anything.
Note: Similar to ExplicitRequirement, the caller should handle name
canonicalisation; this class does not perform it.
"""
def __init__(self, name):
# type: (str) -> None
self._name = name
@property
def name(self):
# type: () -> str
return self._name
def find_matches(self):
# type: () -> Sequence[Candidate]
return []
def is_satisfied_by(self, candidate):
# type: (Candidate) -> bool
return False
class SpecifierRequirement(Requirement):
def __init__(self, ireq, factory):
# type: (InstallRequirement, Factory) -> None

View file

@ -43,6 +43,8 @@ class Resolver(BaseResolver):
finder=finder,
preparer=preparer,
make_install_req=make_install_req,
ignore_requires_python=ignore_requires_python,
py_version_info=py_version_info,
)
self.ignore_dependencies = ignore_dependencies
self._result = None # type: Optional[Result]

View file

@ -1,5 +1,7 @@
import json
import pytest
from tests.lib import create_basic_wheel_for_package
@ -137,3 +139,55 @@ def test_new_resolver_installs_extras(script):
assert "WARNING: Invalid extras specified" in result.stderr, str(result)
assert ": missing" in result.stderr, str(result)
assert_installed(script, base="0.1.0", dep="0.1.0")
@pytest.mark.parametrize(
"requires_python, ignore_requires_python, dep_version",
[
# Something impossible to satisfy.
("<2", False, "0.1.0"),
("<2", True, "0.2.0"),
# Something guaranteed to satisfy.
(">=2", False, "0.2.0"),
(">=2", True, "0.2.0"),
],
)
def test_new_resolver_requires_python(
script,
requires_python,
ignore_requires_python,
dep_version,
):
create_basic_wheel_for_package(
script,
"base",
"0.1.0",
depends=["dep"],
)
create_basic_wheel_for_package(
script,
"dep",
"0.1.0",
)
create_basic_wheel_for_package(
script,
"dep",
"0.2.0",
requires_python=requires_python,
)
args = [
"install",
"--unstable-feature=resolver",
"--no-cache-dir",
"--no-index",
"--find-links", script.scratch_path,
]
if ignore_requires_python:
args.append("--ignore-requires-python")
args.append("base")
script.pip(*args)
assert_installed(script, base="0.1.0", dep=dep_version)

View file

@ -979,7 +979,13 @@ def create_really_basic_wheel(name, version):
def create_basic_wheel_for_package(
script, name, version, depends=None, extras=None, extra_files=None
script,
name,
version,
depends=None,
extras=None,
requires_python=None,
extra_files=None,
):
if depends is None:
depends = []
@ -1007,14 +1013,18 @@ def create_basic_wheel_for_package(
for package in packages
]
metadata_updates = {
"Provides-Extra": list(extras),
"Requires-Dist": requires_dist,
}
if requires_python is not None:
metadata_updates["Requires-Python"] = requires_python
wheel_builder = make_wheel(
name=name,
version=version,
wheel_metadata_updates={"Tag": ["py2-none-any", "py3-none-any"]},
metadata_updates={
"Provides-Extra": list(extras),
"Requires-Dist": requires_dist,
},
metadata_updates=metadata_updates,
extra_metadata_files={"top_level.txt": name},
extra_files=extra_files,

View file

@ -52,6 +52,8 @@ def factory(finder, preparer):
finder=finder,
preparer=preparer,
make_install_req=install_req_from_line,
ignore_requires_python=False,
py_version_info=None,
)