forked from pantsbuild/pants
-
Notifications
You must be signed in to change notification settings - Fork 1
/
poetry.py
125 lines (99 loc) · 4.27 KB
/
poetry.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
from textwrap import dedent
from typing import Any, Iterable, Sequence
import toml
from pkg_resources import Requirement
from pants.backend.python.subsystems.python_tool_base import PythonToolRequirementsBase
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
from pants.engine.fs import FileContent
# ----------------------------------------------------------------------------------------
# Subsystem
# ----------------------------------------------------------------------------------------
class PoetrySubsystem(PythonToolRequirementsBase):
options_scope = "poetry"
help = "Used to generate lockfiles for third-party Python dependencies."
default_version = "poetry==1.1.14"
register_interpreter_constraints = True
default_interpreter_constraints = ["CPython>=3.7,<4"]
# We must monkeypatch Poetry to include `setuptools` and `wheel` in the lockfile. This was fixed
# in Poetry 1.2. See https://github.com/python-poetry/poetry/issues/1584.
# WONTFIX(#12314): only use this custom launcher if using Poetry 1.1..
POETRY_LAUNCHER = FileContent(
"__pants_poetry_launcher.py",
dedent(
"""\
from poetry.console import main
from poetry.puzzle.provider import Provider
Provider.UNSAFE_PACKAGES = set()
main()
"""
).encode(),
)
# ----------------------------------------------------------------------------------------
# Parsing
# ----------------------------------------------------------------------------------------
_HEADER = {
"name": "pants-lockfile-generation",
"version": "0.1.0",
"description": "",
"authors": ["pantsbuild"],
}
def create_pyproject_toml(
requirements: Iterable[str], interpreter_constraints: InterpreterConstraints
) -> str:
return toml.dumps(create_pyproject_toml_as_dict(requirements, interpreter_constraints))
def create_pyproject_toml_as_dict(
raw_requirements: Iterable[str], interpreter_constraints: InterpreterConstraints
) -> dict:
python_constraint = {"python": interpreter_constraints.to_poetry_constraint()}
project_name_to_poetry_deps = defaultdict(list)
for raw_req in raw_requirements:
# WONTFIX(#12314): add error handling.
req = Requirement.parse(raw_req)
poetry_dep = PoetryDependency.from_requirement(req)
project_name_to_poetry_deps[req.project_name].append(poetry_dep)
deps = {
project_name: PoetryDependency.to_pyproject_toml_metadata(poetry_deps)
for project_name, poetry_deps in project_name_to_poetry_deps.items()
}
return {"tool": {"poetry": {**_HEADER, "dependencies": {**python_constraint, **deps}}}}
@dataclass(frozen=True)
class PoetryDependency:
name: str
version: str | None
extras: tuple[str, ...] = ()
markers: str | None = None
@classmethod
def from_requirement(cls, requirement: Requirement) -> PoetryDependency:
return PoetryDependency(
requirement.project_name,
version=str(requirement.specifier) or None, # type: ignore[attr-defined]
extras=tuple(sorted(requirement.extras)),
markers=str(requirement.marker) if requirement.marker else None,
)
@classmethod
def to_pyproject_toml_metadata(
cls, deps: Sequence[PoetryDependency]
) -> dict[str, Any] | list[dict[str, Any]]:
def convert_dep(dep: PoetryDependency) -> dict[str, Any]:
metadata: dict[str, Any] = {"version": dep.version or "*"}
if dep.extras:
metadata["extras"] = dep.extras
if dep.markers:
metadata["markers"] = dep.markers
return metadata
if not deps:
raise AssertionError("Must have at least one element!")
if len(deps) == 1:
return convert_dep(deps[0])
entries = []
name = deps[0].name
for dep in deps:
if dep.name != name:
raise AssertionError(f"All elements must have the same project name. Given: {deps}")
entries.append(convert_dep(dep))
return entries