未验证 提交 4bbace23 编写于 作者: F Frost Ming

get dependencies from metadata

上级 4168a9b3
......@@ -128,3 +128,4 @@ dmypy.json
# Pyre type checker
.pyre/
.vscode/
caches/
from pdm.models.specifiers import PySpecSet
from pdm.models.repositories import PyPIRepository
from pdm.models.requirements import Requirement
from pdm.models.candidates import Candidate
from pdm.context import context
p = PySpecSet('>=2.7,!=3.0.*') & PySpecSet('!=3.1.*')
print(p)
class FakeProject:
config = {"cache_dir": "./caches"}
packages_root = None
context.init(FakeProject())
req = Requirement.from_line("-e ./tests/fixtures/projects/demo")
repo = PyPIRepository([])
can = Candidate(req, repo)
can.prepare_source()
deps = can.get_dependencies_from_metadata()
print(deps)
from functools import wraps
from pathlib import Path
from pip_shims import shims
from pdm.exceptions import ProjectNotInitialized
def require_initialize(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if not self.initialized:
raise ProjectNotInitialized()
return func(self, *args, **kwargs)
return wrapper
class Context:
"""A singleton context object that holds some global states.
Global states are evil but make it easier to share configs between
different modules.
"""
def __init__(self):
self.project = None
def init(self, project):
self.project = project
self._initialized = True
@property
def initialized(self) -> bool:
return self._initialized
@property
@require_initialize
def cache_dir(self) -> Path:
return Path(self.project.config.get("cache_dir"))
@require_initialize
def cache(self, name: str) -> Path:
path = self.cache_dir / name
path.mkdir(exist_ok=True)
return path
@require_initialize
def make_wheel_cache(self) -> shims.WheelCache:
return shims.WheelCache(
self.cache_dir.as_posix(), shims.FormatControl(set(), set()),
)
context = Context()
......@@ -8,3 +8,11 @@ class RequirementError(PdmException, ValueError):
class InvalidPyVersion(PdmException, ValueError):
pass
class WheelBuildError(PdmException):
pass
class ProjectNotInitialized(PdmException):
pass
from typing import Optional, TYPE_CHECKING
import os
from typing import Optional, Any, Dict, List
from distlib.database import EggInfoDistribution
from distlib.metadata import Metadata
from pip_shims import InstallRequirement
from pip._internal.exceptions import InstallationError
from distlib.wheel import Wheel
from pip_shims import shims
from pkg_resources import safe_extra
if TYPE_CHECKING:
from pdm.models.requirements import Requirement
from pdm.models.repositories import BaseRepository
from pdm.context import context
from pdm.exceptions import WheelBuildError, RequirementError
from pdm.utils import cached_property, create_tracked_tempdir
from pdm.models.markers import split_marker_element, Marker
from pdm.models.requirements import Requirement
vcs = shims.VcsSupport()
def get_sdist(ireq: InstallRequirement) -> Optional[EggInfoDistribution]:
try:
egg_info = ireq.egg_info_path
except InstallationError:
ireq.run_egg_info()
egg_info = ireq.egg_info_path
def get_sdist(ireq: shims.InstallRequirement) -> Optional[EggInfoDistribution]:
egg_info = ireq.egg_info_path
return EggInfoDistribution(egg_info) if egg_info else None
def get_source_dir() -> str:
build_dir = context.project.packages_root
if build_dir:
src_dir = build_dir / "src"
src_dir.mkdir(exist_ok=True)
return src_dir.as_posix()
venv = os.environ.get("VIRTUAL_ENV", None)
if venv:
src_dir = os.path.join(venv, "src")
if os.path.exists(src_dir):
return src_dir
return create_tracked_tempdir("pdm-src")
class Candidate:
"""A concrete candidate that can be downloaded and installed."""
def __init__(self, req, repository=None):
# type: (Requirement, Optional[BaseRepository]) -> None
def __init__(self, req, repository, name=None, version=None, link=None):
self.req = req
self.repository = repository
self.name = name
self.version = version
if link is None:
link = self.ireq.link
self.link = link
self.wheel = None
self.build_dir = None
self.metadata = None
@cached_property
def ireq(self) -> shims.InstallRequirement:
return self.req.as_ireq()
@property
def is_wheel(self) -> bool:
return self.link.is_wheel
@cached_property
def revision(self) -> str:
if not self.req.is_vcs:
raise AttributeError("Non-VCS candidate doesn't have revision attribute")
return vcs.get_backend(self.req.vcs).get_revision(self.ireq.source_dir)
def get_metadata(self) -> Optional[Metadata]:
ireq = self.req.as_ireq()
if self.metadata is not None:
return self.metadata
ireq = self.ireq
if ireq.editable:
if not self.req.is_local_dir and not self.req.is_vcs:
raise RequirementError(
"Editable installation is only supported for "
"local directory and VCS location."
)
ireq.run_egg_info()
sdist = get_sdist(ireq)
return sdist.metadata if sdist else None
self.metadata = sdist.metadata if sdist else None
else:
if not self.wheel:
self._build_wheel()
return self.wheel.meta
def _build_wheel(self) -> None:
pass
class LocalCandidate(Candidate):
def __init__(self, req, repository=None):
# type: (Requirement, Optional[BaseRepository]) -> None
super().__init__(req, repository)
self.location = self.req.path.absolute()
def prepare_sources(self) -> None:
self.metadata = self.wheel.metadata
if not self.name:
self.name = self.metadata.name
self.req.name = self.name
if not self.version:
self.version = self.metadata.version
return self.metadata
def __repr__(self) -> str:
return f"<Candidate {self.name} {self.link.url}>"
def _make_pip_wheel_args(self) -> Dict[str, Any]:
src_dir = self.ireq.source_dir or get_source_dir()
if self.req.editable:
self.build_dir = src_dir
else:
self.build_dir = create_tracked_tempdir(prefix="pdm-build")
download_dir = context.cache("pkgs")
download_dir.mkdir(exist_ok=True)
wheel_download_dir = context.cache("wheels")
wheel_download_dir.mkdir(exist_ok=True)
return {
"build_dir": self.build_dir,
"src_dir": src_dir,
"download_dir": download_dir.as_posix(),
"wheel_download_dir": wheel_download_dir.as_posix(),
}
def prepare_source(self) -> None:
"""A local candidate has already everything in local, no need to download."""
pass
@property
def source_dir(self) -> str:
return self.location.as_posix()
kwargs = self._make_pip_wheel_args()
with self.repository.get_finder() as finder:
self.ireq.populate_link(finder, False, False)
if not self.req.editable and not self.req.name:
self.ireq.source_dir = kwargs["build_dir"]
else:
self.ireq.ensure_has_source_dir(kwargs["build_dir"])
if self.req.editable and self.req.is_local_dir:
return
download_dir = kwargs["download_dir"]
if self.is_wheel:
download_dir = kwargs["wheel_download_dir"]
shims.shim_unpack(
link=self.link,
download_dir=download_dir,
location=self.ireq.source_dir,
session=finder.session,
)
class RemoteCandidate(LocalCandidate):
pass
class VcsCandidate(LocalCandidate):
pass
def _build_wheel(self) -> None:
if self.is_wheel:
self.wheel = Wheel(
(context.cache("wheels") / self.link.filename).as_posix()
)
return
if not self.req.name:
# Name is not available for a tarball distribution. Get the package name
# from package's egg info.
# `run_egg_info()` won't work if there is a `req` attribute available.
self.ireq.req = None
self.ireq.run_egg_info()
self.req.name = self.ireq.metadata["Name"]
self.ireq.req = self.req
with self.repository.get_finder() as finder:
kwargs = self._make_pip_wheel_args()
with shims.make_preparer(
finder=finder, session=finder.session, **kwargs
) as preparer:
wheel_cache = context.make_wheel_cache()
builder = shims.WheelBuilder(
finder=finder, preparer=preparer, wheel_cache=wheel_cache
)
output_dir = create_tracked_tempdir(prefix="pdm-ephem")
wheel_path = builder._build_one(self.ireq, output_dir)
if not wheel_path or not os.path.exists(wheel_path):
raise WheelBuildError(str(self.ireq))
self.wheel = Wheel(wheel_path)
@classmethod
def from_installation_candidate(cls, candidate, req, repo):
inst = cls(
req,
repo,
name=candidate.project,
version=candidate.version,
link=candidate.link,
)
return inst
def get_dependencies_from_metadata(self) -> List[str]:
extras = self.req.extras or ()
metadata = self.get_metadata()
result = []
if self.req.editable:
if not metadata:
return result
dep_map = self.ireq.get_dist()._build_dep_map()
for extra, reqs in dep_map.items():
reqs = [Requirement.from_pkg_requirement(r) for r in reqs]
if not extra:
result.extend(r.as_line() for r in reqs)
else:
new_extra, _, marker = extra.partition(":")
if not new_extra.strip() or safe_extra(new_extra.strip()) in extras:
marker = Marker(marker) if marker else None
for r in reqs:
r.marker = marker
result.append(r.as_line())
else:
for req in metadata.run_requires:
_r = Requirement.from_line(req)
if not _r.marker:
result.append(req)
else:
elements, rest = split_marker_element(str(_r.marker), "extra")
_r.marker = rest
if not elements or any(
extra == e[1] for extra in extras for e in elements
):
result.append(_r.as_line())
return result
from packaging.markers import Marker as PackageMarker
from typing import Union, Optional
from typing import Union, Optional, Tuple, Iterable
class Marker(PackageMarker):
......@@ -35,3 +35,36 @@ class Marker(PackageMarker):
def get_marker(marker: Union[PackageMarker, Marker, None]) -> Optional[Marker]:
return Marker(str(marker)) if marker else None
def split_marker_element(
text: str, element: str
) -> Tuple[Iterable[Tuple[str, str]], Optional[str]]:
"""An element can be stripped from the marker only if all parts are connected
with `and` operater. The rest part are returned as a string or `None` if all are
stripped.
:param text: the input marker string
:param element: the element to be stripped
:returns: an iterable of (op, value) pairs together with the stripped part.
"""
if not text:
return [], text
marker = Marker(text)
if "or" in marker._markers:
return [], text
result = []
bare_markers = [m for m in marker._markers if m != "and"]
for m in bare_markers[:]:
if not isinstance(m, tuple):
continue
if m[0].value == element:
result.append(tuple(e.value for e in m[1:]))
bare_markers.remove(m)
if not bare_markers:
return result, None
new_markers = [bare_markers[0]]
for m in bare_markers[1:]:
new_markers.extend(["and", m])
marker._markers = new_markers
return result, marker
from typing import List, Tuple, Optional
from contextlib import contextmanager
from typing import List, Tuple
import pip_shims
from pdm.types import Source
from pdm.models.candidates import Candidate
from pdm.models.requirements import Requirement
from pdm.models.specifiers import PySpecSet
from pdm.utils import get_package_finder
from pdm.types import Source
from pdm.utils import get_finder
from pdm.context import context
class BaseRepository:
def __init__(self, source: Source, cache_dir: str) -> None:
self.source = source
self.cache_dir = cache_dir
def __init__(self, sources: List[Source]) -> None:
self.sources = sources
def match_index(self, requirement: Requirement) -> bool:
return requirement.index is None or requirement.index == self.source["name"]
def get_finder(
self, requires_python: Optional[PySpecSet]
) -> pip_shims.PackageFinder:
return get_package_finder([self.source], requires_python.as_py_versions())
@contextmanager
def get_finder(self) -> pip_shims.PackageFinder:
finder = get_finder(self.sources, context.cache_dir.as_posix())
yield finder
finder.session.close()
def get_dependencies(
self, candidate: Candidate
......@@ -32,6 +32,22 @@ class BaseRepository:
requires_python: PySpecSet,
allow_prereleases: bool = False,
) -> List[Candidate]:
if self.requirement.is_named:
return self._find_named_matches(
requirement, requires_python, allow_prereleases
)
else:
return [Candidate(requirement, self)]
def _find_named_matches(
self,
requirement: Requirement,
requires_python: PySpecSet,
allow_prereleases: bool = False,
) -> List[Candidate]:
"""Find candidates of the given NamedRequirement. Let it to be implemented in
subclasses.
"""
raise NotImplementedError
def _get_dependencies_from_cache(
......
from pathlib import Path
import re
import os
from typing import Any, Dict, Optional, Tuple
import urllib.parse as urlparse
......@@ -13,15 +14,10 @@ from packaging.specifiers import SpecifierSet
import pip_shims
from pdm.models.markers import get_marker
from pdm.models.candidates import (
Candidate,
RemoteCandidate,
LocalCandidate,
VcsCandidate,
)
from pdm.models.project_files import SetupReader
from pdm.models.readers import SetupReader
from pdm.types import RequirementDict
from pdm.exceptions import RequirementError
from pdm.utils import parse_name_version_from_wheel, url_without_fragments
VCS_SCHEMA = ("git", "hg", "svn", "bzr")
......@@ -50,7 +46,7 @@ class Requirement:
"""
VCS_REQ = re.compile(
rf"(?P<editable>-e[\t ]+)(?P<vcs>{'|'.join(VCS_SCHEMA)})\+"
rf"(?P<editable>-e[\t ]+)?(?P<vcs>{'|'.join(VCS_SCHEMA)})\+"
r"(?P<url>[^\s;]+)(?P<marker>[\t ]*;[^\n]+)?"
)
_PATH_START = r"(?:\.|/|[a-zA-Z]:[/\\])"
......@@ -140,16 +136,16 @@ class Requirement:
def as_req_dict(self) -> Tuple[str, RequirementDict]:
r = {}
if self.is_vcs:
r[self.vcs] = self.repo
elif self.url:
r["url"] = self.url
if self.editable:
r["editable"] = True
if self.extras:
r["extras"] = sorted(self.extras)
if self.path:
if self.is_vcs:
r[self.vcs] = self.repo
elif self.path and self.is_local_dir:
r["path"] = self.str_path
elif self.url:
r["url"] = self.url
if self.marker:
r["marker"] = str(self.marker)
if self.specs:
......@@ -191,6 +187,17 @@ class Requirement:
ireq.req = self
return ireq
@classmethod
def from_pkg_requirement(cls, req: PackageRequirement) -> "Requirement":
klass = FileRequirement if req.url else NamedRequirement
return klass(
name=req.name,
extras=req.extras,
url=req.url,
specifier=req.specifier,
marker=req.marker,
)
def _format_marker(self) -> str:
if self.marker:
return f"; {str(self.marker)}"
......@@ -207,15 +214,8 @@ class FileRequirement(Requirement):
self._parse_url()
if self.path and not self.path.exists():
raise RequirementError(f"The local path {self.path} does not exist.")
if self.is_local and not self.name:
self._parse_name_from_project_files()
@property
def normalized_url(self) -> Optional[str]:
if self.url:
return self.url
elif self.path:
return f"file:///{self.path.absolute().as_posix()}"
if not self.name and self.is_local_dir:
self._parse_name_from_local()
@classmethod
def from_line(cls, line: str, parsed: Dict[str, str]) -> "FileRequirement":
......@@ -228,9 +228,14 @@ class FileRequirement(Requirement):
return r
def _parse_url(self) -> None:
parsed = urlparse.urlparse(self.url)
if parsed.scheme == "file" and not parsed.netloc:
self.path = Path(parsed.path)
if not self.url:
if self.path:
self.url = f"file://{self.path.absolute().as_posix()}"
else:
parsed = urlparse.urlparse(self.url)
if parsed.scheme == "file" and not parsed.netloc:
self.path = Path(parsed.path)
self._parse_name_from_url()
@property
def is_local(self) -> bool:
......@@ -252,10 +257,10 @@ class FileRequirement(Requirement):
editable = "-e " if self.editable else ""
project_name = f"{self.project_name}" if self.project_name else ""
extras = f"[{','.join(sorted(self.extras))}]" if self.extras else ""
if self.path and not for_ireq and self.editable:
if self.path and not for_ireq and self.editable and not project_name:
location = self.str_path
else:
location = self.normalized_url
location = self.url
marker = self._format_marker()
if for_ireq and project_name:
return f"{editable}{location}#egg={project_name}{extras}{marker}"
......@@ -263,15 +268,24 @@ class FileRequirement(Requirement):
delimiter = " @ " if project_name else ""
return f"{editable}{project_name}{extras}{delimiter}{location}{marker}"
def as_candidate(self) -> Candidate:
if self.path:
return LocalCandidate(req=self)
else:
return RemoteCandidate(req=self)
def _parse_name_from_url(self) -> None:
parsed = urlparse.urlparse(self.url)
fragments = dict(urlparse.parse_qsl(parsed.fragment))
if "egg" in fragments:
egg_info = urlparse.unquote(fragments["egg"])
name, extras = _strip_extras(egg_info)
self.name = name
self.extras = extras
if not self.name:
filename = os.path.basename(url_without_fragments(self.url))
if filename.endswith(".whl"):
self.name, self.version = parse_name_version_from_wheel(filename)
def _parse_name_from_project_files(self) -> None:
def _parse_name_from_local(self) -> None:
result = SetupReader.read_from_directory(self.path.absolute().as_posix())
self.name = result["name"]
if not self.name:
raise RequirementError(f"The local path '{self.path}' is not installable.")
class NamedRequirement(Requirement, PackageRequirement):
......@@ -312,22 +326,14 @@ class VcsRequirement(FileRequirement):
editable = "-e " if self.editable else ""
return f"{editable}{self.vcs}+{self.url}{self._format_marker()}"
def as_candidate(self) -> Candidate:
return VcsCandidate(req=self)
def _parse_url(self) -> None:
if self.name and self.repo:
return
if self.url.startswith("git@"):
self.url = "ssh://" + self.url[4:].replace(":", "/")
parsed = urlparse.urlparse(self.url)
fragments = dict(urlparse.parse_qsl(parsed.fragment))
if "egg" in fragments:
egg_info = urlparse.unquote(fragments["egg"])
name, extras = _strip_extras(egg_info)
self.name = name
self.extras = extras
self.repo = urlparse.urlunparse(parsed._replace(fragment=""))
if not self.name:
self._parse_name_from_url()
if not self.name:
raise RequirementError("VCS requirement must provide a 'egg=' fragment.")
self.repo = url_without_fragments(self.url)
@staticmethod
def _build_url_from_req_dict(name: str, url: str, req_dict: RequirementDict) -> str:
......
......@@ -287,9 +287,9 @@ class PySpecSet(SpecifierSet):
for z in range(prev[2], current_max + 1):
yield (*prev[:2], z)
prev = (
cur
if cur[1] <= self.MAX_PY_VERSIONS[(cur[0],)]
else _bump_version(prev, 0)
_bump_version(prev, 0)
if cur[0] < 3 and cur[1] > self.MAX_PY_VERSIONS[(cur[0],)]
else cur
)
else: # X.Y.Z -> X.Y.W
while prev < upper:
......
from resolvelib.providers import AbstractProvider
from requirementslib import Requirement
from pdm.models.repositories import BaseRepository
from typing import List
class RepositoryProvider(AbstractProvider):
def __init__(self, repositories: List[BaseRepository]) -> None:
self.repositories = repositories
def __init__(self, repository: BaseRepository) -> None:
self.repository = repository
def identify(self, requirement: Requirement) -> str:
return requirement.key
"""
Compatibility code
"""
import os
import atexit
import shutil
import tempfile
from typing import List, Optional, Tuple
from urllib.parse import urlparse
import urllib.parse as parse
from distlib.wheel import Wheel
import pkg_resources
from contextlib import contextmanager
from pip_shims.shims import InstallCommand, PackageFinder, get_package_finder
from pdm.types import Source
try:
from functools import cached_property
except ImportError:
class cached_property:
def __init__(self, func):
self.func = func
self.attr_name = func.__name__
self.__doc__ = func.__doc__
def __get__(self, inst, cls=None):
if inst is None:
return self
if self.attr_name not in inst.__dict__:
inst.__dict__[self.attr_name] = self.func(inst)
return inst.__dict__[self.attr_name]
def prepare_pip_source_args(
sources: List[Source], pip_args: Optional[List[str]] = None
......@@ -20,7 +44,7 @@ def prepare_pip_source_args(
# Trust the host if it's not verified.
if not sources[0].get("verify_ssl", True):
pip_args.extend(
["--trusted-host", urlparse(sources[0]["url"]).hostname]
["--trusted-host", parse.urlparse(sources[0]["url"]).hostname]
) # type: ignore
# Add additional sources as extra indexes.
if len(sources) > 1:
......@@ -29,17 +53,53 @@ def prepare_pip_source_args(
# Trust the host if it's not verified.
if not source.get("verify_ssl", True):
pip_args.extend(
["--trusted-host", urlparse(source["url"]).hostname]
["--trusted-host", parse.urlparse(source["url"]).hostname]
) # type: ignore
return pip_args
def get_finder(
sources: List[Source], python_versions: Optional[Tuple[str, ...]] = None
) -> PackageFinder:
def get_finder(sources: List[Source], cache_dir: Optional[str] = None) -> PackageFinder:
install_cmd = InstallCommand()
pip_args = prepare_pip_source_args(sources)
options, _ = install_cmd.parser.parse_args(pip_args)
return get_package_finder(
install_cmd=install_cmd, options=options, python_versions=python_versions
)
if cache_dir:
options.cache_dir = cache_dir
return get_package_finder(install_cmd=install_cmd, options=options)
def create_tracked_tempdir(
suffix: Optional[str] = None, prefix: Optional[str] = "", dir: Optional[str] = None
) -> str:
name = tempfile.mkdtemp(suffix, prefix, dir)
os.makedirs(name, mode=0o777, exist_ok=True)
def clean_up():
shutil.rmtree(name, ignore_errors=True)
atexit.register(clean_up)
return name
def parse_name_version_from_wheel(filename: str) -> Tuple[str, str]:
w = Wheel(filename)
return w.name, w.version
def url_without_fragments(url: str) -> str:
return parse.urlunparse(parse.urlparse(url)._replace(fragment=""))
@contextmanager
def allow_all_markers():
"""This is a monkey patch function that temporarily disables marker evaluation."""
from pip._vendor import pkg_resources as vendor_pkg
def evaluate_marker(text, extra=None):
return True
old_evaluate = pkg_resources.evaluate_marker
pkg_resources.evaluate_marker = evaluate_marker
vendor_pkg.evaluate_marker = evaluate_marker
yield
pkg_resources.evaluate_marker = old_evaluate
vendor_pkg = old_evaluate
......@@ -6,6 +6,10 @@ setup(
version="0.0.1",
description="test demo",
py_modules=["demo"],
install_requires=["idna", "chardet"],
extras_require={'tests': ['pytest'], 'security': ['requests']},
python_requires=">=3.3",
install_requires=["idna", "chardet; os_name=='nt'"],
extras_require={
"tests": ["pytest"],
"security": ['requests; python_version>="3.6"'],
},
)
import pytest
from pdm.models.requirements import Requirement
from pdm.models.requirements import Requirement, RequirementError
from tests import FIXTURES
REQUIREMENTS = [
......@@ -33,6 +33,11 @@ REQUIREMENTS = [
("MyProject", {"editable": True, "git": "http://git.example.com/MyProject"}),
None,
),
(
"git+http://git.example.com/MyProject#egg=MyProject",
("MyProject", {"git": "http://git.example.com/MyProject"}),
None,
),
(
"https://github.com/pypa/pip/archive/1.3.1.zip",
(None, {"url": "https://github.com/pypa/pip/archive/1.3.1.zip"}),
......@@ -41,19 +46,18 @@ REQUIREMENTS = [
(
(FIXTURES / "projects/demo").as_posix(),
("demo", {"path": (FIXTURES / "projects/demo").as_posix()}),
"demo @ file:///" + (FIXTURES / "projects/demo").as_posix(),
"demo @ file://" + (FIXTURES / "projects/demo").as_posix(),
),
(
(FIXTURES / "artifacts/demo-0.0.1-py2.py3-none-any.whl").as_posix(),
(
None,
"demo",
{
"path": (
FIXTURES / "artifacts/demo-0.0.1-py2.py3-none-any.whl"
).as_posix()
"url": "file://"
+ (FIXTURES / "artifacts/demo-0.0.1-py2.py3-none-any.whl").as_posix()
},
),
"file:///"
"demo @ file://"
+ (FIXTURES / "artifacts/demo-0.0.1-py2.py3-none-any.whl").as_posix(),
),
]
......@@ -67,3 +71,20 @@ def test_convert_req_dict_to_req_line(req, req_dict, result):
r = Requirement.from_req_dict(*req_dict)
result = result or req
assert r.as_line() == result
@pytest.mark.parametrize(
"line,expected",
[
(
"-e https://github.com/pypa/pip/archive/1.3.1.zip",
"Editable requirement is only supported",
),
("requests; os_name=>'nt'", "Invalid requirement"),
("./nonexist", r"The local path (.+)? does not exist"),
("./tests", r"The local path (.+)? is not installable"),
],
)
def test_illegal_requirement_line(line, expected):
with pytest.raises(RequirementError, match=expected):
Requirement.from_line(line)
......@@ -4,18 +4,18 @@ from pdm.models.specifiers import PySpecSet
@pytest.mark.parametrize(
'original,normalized',
"original,normalized",
[
('>=3.6', '>=3.6'),
('<3.8', '<3.8'),
('~=2.7.0', '>=2.7,<2.8'),
('', ''),
('>=3.6,<3.8', '>=3.6,<3.8'),
('>3.6', '>=3.6.1'),
('<=3.7', '<3.7.1'),
('>=3.6,!=3.4.*', '>=3.6'),
('>=3.6,!=3.6.*', '>=3.7'),
('>=3.6,<3.8,!=3.8.*', '>=3.6,<3.8'),
(">=3.6", ">=3.6"),
("<3.8", "<3.8"),
("~=2.7.0", ">=2.7,<2.8"),
("", ""),
(">=3.6,<3.8", ">=3.6,<3.8"),
(">3.6", ">=3.6.1"),
("<=3.7", "<3.7.1"),
(">=3.6,!=3.4.*", ">=3.6"),
(">=3.6,!=3.6.*", ">=3.7"),
(">=3.6,<3.8,!=3.8.*", ">=3.6,<3.8"),
],
)
def test_normalize_pyspec(original, normalized):
......@@ -24,13 +24,13 @@ def test_normalize_pyspec(original, normalized):
@pytest.mark.parametrize(
'left,right,result',
"left,right,result",
[
('>=3.6', '>=3.0', '>=3.6'),
('>=3.6', '<3.8', '>=3.6,<3.8'),
('', '>=3.6', '>=3.6'),
('>=3.6', '<3.2', 'impossible'),
('>=2.7,!=3.0.*', '!=3.1.*', '>=2.7,!=3.0.*,!=3.1.*'),
(">=3.6", ">=3.0", ">=3.6"),
(">=3.6", "<3.8", ">=3.6,<3.8"),
("", ">=3.6", ">=3.6"),
(">=3.6", "<3.2", "impossible"),
(">=2.7,!=3.0.*", "!=3.1.*", ">=2.7,!=3.0.*,!=3.1.*"),
],
)
def test_pyspec_and_op(left, right, result):
......@@ -40,13 +40,14 @@ def test_pyspec_and_op(left, right, result):
@pytest.mark.parametrize(
'left,right,result',
"left,right,result",
[
('>=3.6', '>=3.0', '>=3.0'),
('', '>=3.6', ''),
('>=3.6', '<3.7', ''),
('>=3.6,<3.8', '>=3.4,<3.7', '>=3.4,<3.8'),
('~=2.7', '>=3.6', '>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*'),
(">=3.6", ">=3.0", ">=3.0"),
("", ">=3.6", ""),
(">=3.6", "<3.7", ""),
(">=3.6,<3.8", ">=3.4,<3.7", ">=3.4,<3.8"),
("~=2.7", ">=3.6", ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*"),
("<3.6.5", ">=3.7", "!=3.6.5,!=3.6.6,!=3.6.7,!=3.6.8,!=3.6.9,!=3.6.10"),
],
)
def test_pyspec_or_op(left, right, result):
......@@ -56,8 +57,8 @@ def test_pyspec_or_op(left, right, result):
def test_impossible_pyspec():
spec = PySpecSet('>=3.6,<3.4')
a = PySpecSet('>=2.7')
spec = PySpecSet(">=3.6,<3.4")
a = PySpecSet(">=2.7")
assert spec.is_impossible
assert (spec & a).is_impossible
assert spec | a == a
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册