未验证 提交 5b106c48 编写于 作者: F frostming

support poetry converting

上级 18579641
import abc
import collections
def convert_from(field=None, name=None):
def wrapper(func):
func._convert_from = field
func._convert_to = name
return func
return wrapper
class _MetaConverterMeta(abc.ABCMeta):
def __init__(cls, name, bases, ns):
super().__init__(name, bases, ns)
cls._converters = {}
_default = object()
for key, value in ns.items():
if getattr(value, "_convert_from", _default) is not _default:
name = value._convert_to or key
cls._converters[name] = value
class MetaConverter(collections.abc.Mapping, metaclass=_MetaConverterMeta):
"""Convert a metadata dictionary to PDM's format"""
def __init__(self, source):
self._data = {}
self._convert(dict(source))
def __getitem__(self, k):
return self._data[k]
def __len__(self):
return len(self._data)
def __iter__(self):
return iter(self._data)
def _convert(self, source):
for key, func in self._converters.items():
if func._convert_from and func._convert_from not in source:
continue
if func._convert_from is None:
value = source
else:
value = source[func._convert_from]
self._data[key] = func(self, value)
# Delete all used fields
for key, func in self._converters.items():
if func._convert_from is None:
continue
try:
del source[func._convert_from]
except KeyError:
pass
# Add remaining items to the data
self._data.update(source)
import functools
import operator
import re
import tomlkit
from pdm.formats.base import MetaConverter, convert_from
from pdm.models.markers import Marker
from pdm.models.specifiers import PySpecSet
def check_fingerprint(filename):
with open(filename, encoding="utf-8") as fp:
data = tomlkit.parse(fp.read())
return "tool" in data and "poetry" in data["tool"]
VERSION_RE = re.compile(r"(\S+?)\s*(\d.*?)\s*(?=,|$)")
def _convert_specifier(version):
parts = []
for op, version in VERSION_RE.findall(version):
if op == "~":
op += "="
elif op == "^":
major, *vparts = version.split(".")
next_major = ".".join([str(int(major) + 1)] + ["0"] * len(vparts))
parts.append(f">={version},<{next_major}")
continue
parts.append(f"{op}{version}")
return ",".join(parts)
def _convert_python(python):
if not python:
return ""
parts = [PySpecSet(_convert_specifier(s)) for s in python.split("||")]
return functools.reduce(operator.or_, parts)
def _convert_req(req_dict):
if not getattr(req_dict, "items", None):
return _convert_specifier(req_dict)
req_dict = dict(req_dict)
if "version" in req_dict:
req_dict["version"] = _convert_specifier(req_dict["version"])
markers = []
if "markers" in req_dict:
markers.append(Marker(req_dict.pop("markers")))
if "python" in req_dict:
markers.append(
Marker(_convert_python(req_dict.pop("python")).as_marker_string())
)
if markers:
req_dict["marker"] = str(functools.reduce(operator.and_, markers))
if "rev" in req_dict or "branch" in req_dict or "tag" in req_dict:
req_dict["ref"] = req_dict.pop(
"rev", req_dict.pop("tag", req_dict.pop("branch", None))
)
return req_dict
class PoetryMetaConverter(MetaConverter):
@convert_from("authors")
def author(self, value):
return value[0]
@convert_from("maintainers")
def maintainer(self, value):
return value[0]
@convert_from()
def python_requires(self, source):
python = source.get("dependencies", {}).pop("python", None)
return str(_convert_python(python))
@convert_from()
def project_urls(self, source):
rv = source.pop("urls", {})
if "repository" in source:
rv["Repository"] = source.pop("repository")
if "documentation" in source:
rv["Documentation"] = source.pop("documentation")
return rv
@convert_from("scripts")
def cli(self, value):
return value
@convert_from("plugins")
def entry_points(self, value):
return value
@convert_from()
def dependencies(self, source):
rv = {}
value, extras = dict(source["dependencies"]), source.pop("extras", {})
for key, req_dict in value.items():
optional = getattr(req_dict, "items", None) and req_dict.pop(
"optional", False
)
req_dict = _convert_req(req_dict)
if optional:
extra = next((k for k, v in extras.items() if key in v), None)
if extra:
self._data.setdefault(f"{extra}-dependencies", {})[key] = req_dict
else:
rv[key] = req_dict
if extras:
self._data["extras"] = list(extras)
del source["dependencies"]
return rv
@convert_from("dev-dependencies", name="dev-dependencies")
def dev_dependencies(self, value):
return {key: _convert_req(req) for key, req in value.items()}
def convert(filename):
with open(filename, encoding="utf-8") as fp:
return dict(PoetryMetaConverter(tomlkit.parse(fp.read())["tool"]["poetry"]))
import hashlib
from pip._internal.req.req_file import parse_requirements
from pdm.models.requirements import Requirement
from pdm.models.markers import Marker
from pdm.models.requirements import parse_requirement
from pdm.utils import get_finder
def _requirement_to_str_lowercase_name(requirement):
"""Formats a packaging.requirements.Requirement with a lowercase name."""
parts = [requirement.name.lower()]
if requirement.extras:
parts.append("[{0}]".format(",".join(sorted(requirement.extras))))
if requirement.specifier:
parts.append(str(requirement.specifier))
if requirement.url:
parts.append("@ {0}".format(requirement.url))
if requirement.marker:
parts.append("; {0}".format(requirement.marker))
return "".join(parts)
def requirement_from_ireq(ireq):
"""Formats an `InstallRequirement` instance as a
`pdm.models.requirement.Requirement`.
Generic formatter for pretty printing InstallRequirements to the terminal
in a less verbose way than using its `__str__` method.
:param :class:`InstallRequirement` ireq: A pip **InstallRequirement** instance.
:return: A formatted string for prettyprinting
:rtype: str
"""
if ireq.editable:
line = "{}".format(ireq.link)
else:
line = _requirement_to_str_lowercase_name(ireq.req)
if str(ireq.req.marker) != str(ireq.markers):
if not ireq.req.marker:
line = "{}; {}".format(line, ireq.markers)
else:
name, markers = line.split(";", 1)
markers = Marker(markers) & ireq.markers
line = "{}; {}".format(name, markers)
return parse_requirement(line, ireq.editable)
def parse_requirement_file(filename):
finder = get_finder([])
ireqs = list(parse_requirements(filename, finder.session, finder))
......@@ -26,19 +74,15 @@ def check_fingerprint(filename):
def convert_url_to_source(url, name=None):
if not name:
name = hashlib.sha1(url.encode("utf-8")).hexdigest[:6]
name = hashlib.sha1(url.encode("utf-8")).hexdigest()[:6]
return {"name": name, "url": url, "verify_ssl": url.startswith("https://")}
def convert(filename):
ireqs, finder = parse_requirement_file(filename)
reqs = []
for ireq in ireqs:
req = ireq.req
req.marker = ireq.markers
reqs.append(Requirement.from_pkg_requirement(req))
data = {"dependencies": dict(req.to_req_dict() for req in reqs)}
ireqs, finder = parse_requirement_file(str(filename))
reqs = [requirement_from_ireq(ireq) for ireq in ireqs]
data = {"dependencies": dict(req.as_req_dict() for req in reqs)}
if finder.index_urls:
sources = [convert_url_to_source(finder.index_urls[0], "pypi")]
sources.extend(convert_url_to_source(url) for url in finder.index_urls[1:])
......
......@@ -65,6 +65,7 @@ class Requirement:
"project_name",
"url",
"path",
"ref",
"index",
"version",
"allow_prereleases",
......@@ -145,7 +146,7 @@ class Requirement:
r["version"] = "*"
if len(r) == 1 and next(iter(r), None) == "version":
r = r["version"]
for attr in ["index", "allow_prereleases"]:
for attr in ["index", "allow_prereleases", "ref"]:
if getattr(self, attr) is not None:
r[attr] = getattr(self, attr)
return self.project_name, r
......@@ -344,7 +345,13 @@ class VcsRequirement(FileRequirement):
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)
repo = url_without_fragments(self.url)
ref = None
parsed = urlparse.urlparse(repo)
if "@" in parsed.path:
path, ref = parsed.path.split("@", 1)
repo = urlparse.urlunparse(parsed._replace(path=path))
self.repo, self.ref = repo, ref
@staticmethod
def _build_url_from_req_dict(name: str, url: str, req_dict: RequirementDict) -> str:
......
......@@ -33,13 +33,8 @@ REQUIREMENTS = [
None,
),
(
"git+http://git.example.com/MyProject#egg=MyProject",
("MyProject", {"git": "http://git.example.com/MyProject"}),
None,
),
(
"git+http://git.example.com/MyProject#egg=MyProject",
("MyProject", {"git": "http://git.example.com/MyProject"}),
"git+http://git.example.com/MyProject.git@master#egg=MyProject",
("MyProject", {"git": "http://git.example.com/MyProject.git", "ref": "master"}),
None,
),
(
......
from pdm.formats import pipfile, requirements
from pdm.formats import pipfile, poetry, requirements
from tests import FIXTURES
......@@ -17,3 +17,40 @@ def test_convert_pipfile():
assert result["dependencies"]["pywinusb"]["marker"] == 'sys_platform == "win32"'
assert result["source"][0]["url"] == "https://pypi.python.org/simple"
def test_convert_requirements_file():
golden_file = FIXTURES / "requirements.txt"
assert requirements.check_fingerprint(golden_file)
result = requirements.convert(golden_file)
assert len(result["source"]) == 2
assert result["dependencies"]["webassets"] == "==2.0"
assert result["dependencies"]["whoosh"]["marker"] == 'sys_platform == "win32"'
assert result["dependencies"]["pip"]["editable"]
assert result["dependencies"]["pip"]["git"] == "https://github.com/pypa/pip.git"
def test_convert_poetry():
golden_file = FIXTURES / "pyproject-poetry.toml"
assert poetry.check_fingerprint(golden_file)
result = poetry.convert(golden_file)
assert result["author"] == "Sébastien Eustace <sebastien@eustace.io>"
assert result["name"] == "poetry"
assert result["version"] == "1.0.0"
assert "Repository" in result["project_urls"]
assert result["python_requires"] == ">=2.7,<4.0,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
assert result["dependencies"]["cleo"]["marker"] == 'python_version ~= "2.7"'
assert result["dependencies"]["cachecontrol"]["marker"] == (
'python_version >= "3.4" and python_version < "4.0"'
)
assert "psycopg2" not in result["dependencies"]
assert "psycopg2" in result["pgsql-dependencies"]
assert sorted(result["extras"]) == ["mysql", "pgsql"]
assert len(result["dev-dependencies"]) == 2
assert result["cli"] == {"poetry": "poetry.console:run"}
assert result["entry_points"]["blogtool.parsers"] == {
".rst": "some_module:SomeClass"
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册