Newer
Older
# Copyright 2023-2024 Dom Sekotill <dom.sekotill@kodo.org.uk>
"""
Script for generating stub packages from konnect.curl source code
"""
import sys
from collections.abc import Iterator
from copy import deepcopy
from pathlib import Path
from shutil import copyfile
from shutil import rmtree
from subprocess import run
from typing import Self
from typing import TypeAlias
import toml
CWD = Path(".")
POETRY_CONFIG = {
"name": "types-konnect.curl",
"version": "",
"description": "static type stubs for konnect.curl",
"packages": [
{"include": "konnect-stubs"},
],
"authors": [],
"dependencies": {},
"include": [
"LICENCE.txt",
],
"license": "MPL-2.0",
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
}
STUB_PKG_CONFIG = {
"build-system": {
"build-backend": "poetry.core.masonry.api",
"requires": ["poetry_core>=1.0.0"],
},
"tool": {"poetry": POETRY_CONFIG},
}
Config: TypeAlias = dict[str, object]
def get_object(config: Config, *path: str|int) -> object:
"""
Return an object from the configuration object accessed by path keys/indexes
"""
obj: object = config
for step in path:
obj = obj[step] # type: ignore
return obj
def get_config(config: Config, *path: str|int) -> Config:
"""
Get a sub-config mapping from the configuration object accessed by path keys/indexes
"""
value = get_object(config, *path)
if not isinstance(value, dict):
raise TypeError(f"Not a mapping: {value!r}")
return value
def get_array(config: Config, *path: str|int) -> list[object]:
"""
Return a list from the configuration object accessed by path keys/indexes
"""
sequence = get_object(config, *path)
if not isinstance(sequence, list):
raise TypeError(f"Not a sequence: {sequence!r}")
return sequence
def get_str(config: Config, *path: str|int) -> str:
"""
Get a string from the configuration object accessed by path keys/indexes
"""
value = get_object(config, *path)
if not isinstance(value, str):
raise TypeError(f"Not a string: {value!r}")
return value
class Environment:
"""
A simple virtual environment (venv) interface
"""
def __init__(self, path: Path):
self.path = path
self.bin = path / "bin"
self.python = self.bin / "python"
def __enter__(self) -> Self:
if self.python.is_file():
return self
run([sys.executable, "-mvenv", self.path], check=True)
return self
def __exit__(self, *exc_info: object) -> None:
pass
def install(self, package: str) -> None:
"""
Install the named package in the environment
"""
self.exec_module("pip", "install", package)
def exec_module(self, module: str, *args: str|Path) -> None:
"""
Execute the named module from the environment with the given arguments
"""
run([self.python, "-m", module, *args], check=True)
def run(self, name: str, *args: str|Path) -> None:
"""
Execute a binary or script installed in the environment
"""
run([self.bin / name, *args], check=True)
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
class Project:
"""
Information about the root project
"""
def __init__(self, root: Path = CWD):
self.root = root
self._config: Config|None = None
@property
def config(self) -> Config:
"""
The project configuration (pyproject.toml) as a configuration mapping
"""
if not self._config:
with open("pyproject.toml") as config:
self._config = toml.load(config)
return self._config
def get_version(self) -> str:
"""
Return the project's version
"""
return get_str(self.config, "project", "version")
def get_authors(self) -> Iterator[str]:
"""
Return an iterator over the project authors objects
"""
for author in get_array(self.config, "project", "authors"):
if not isinstance(author, dict):
raise TypeError(f"Not an author mapping: {author!r}")
yield f"{author['name']} <{author['email']}>"
def python_dependency(self) -> str:
"""
Return the python dependency specification for the project
"""
return get_str(self.config, "project", "requires-python")
class Package:
"""
Methods for preparing and building the stubs packages
"""
def __init__(self, project: Project):
self.project = project
def complete_config(self) -> Config:
"""
Return the package's configuration (pyproject.toml) as a mapping
"""
config = deepcopy(STUB_PKG_CONFIG)
poetry = get_config(config, "tool", "poetry")
assert isinstance(poetry["authors"], list)
assert isinstance(poetry["dependencies"], dict)
poetry["version"] = self.project.get_version()
poetry["authors"].extend(self.project.get_authors())
poetry["dependencies"].update(python=self.project.python_dependency())
return config
def build(self, build_dir: Path) -> None:
"""
Prepare and build the stubs package in build_dir
"""
if build_dir.exists():
rmtree(build_dir)
build_dir.mkdir(parents=True)
with build_dir.joinpath("pyproject.toml").open("w") as config:
toml.dump(self.complete_config(), config)
with Environment(build_dir / "stub.venv") as env:
self.make_stubs(build_dir, env)
self.build_package(build_dir, env)
def copy_docs(self, build_dir: Path) -> None:
"""
Copy minimal documentation for the types stubs package
"""
copyfile(self.project.root / "LICENCE.txt", build_dir / "LICENCE.txt")
def make_stubs(self, build_dir: Path, env: Environment) -> None:
"""
Generate type stub package
"""
env.install("mypy")
"--verbose",
"--output", build_dir / "konnect-stubs",
self.project.root / "konnect/curl",
)
copyfile(
self.project.root / "konnect/curl/__init__.py",
build_dir / "konnect-stubs/curl/__init__.pyi",
)
copyfile(
self.project.root / "konnect/curl/_enums.py",
build_dir / "konnect-stubs/curl/_enums.pyi",
)
def build_package(self, build_dir: Path, env: Environment) -> None:
"""
Build distribution packages from generated sources
"""
env.install("build")
env.exec_module(
"build",
"--outdir", self.project.root / "dist",
build_dir,
)
if __name__ == "__main__":
project = Project()
package = Package(project)
package.build(Path("build/package"))