@dbx-tools/projen 0.6.76 → 0.6.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/index.ts +8 -21
- package/package.json +4 -4
- package/src/project-js.ts +1237 -0
- package/src/project-predicate.ts +1 -1
- package/src/project-py.ts +329 -0
- package/src/project.ts +46 -1292
- package/src/vscode.ts +3 -3
package/src/project-predicate.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { IConstruct } from "constructs";
|
|
|
11
11
|
import { Project } from "projen";
|
|
12
12
|
import { project } from "..";
|
|
13
13
|
import { toPosix } from "./packages.ts";
|
|
14
|
-
import { DBXToolsProject, DBXToolsNodeProject, DBXToolsTypeScriptProject } from "./project.ts";
|
|
14
|
+
import { DBXToolsProject, DBXToolsNodeProject, DBXToolsTypeScriptProject } from "./project-js.ts";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Guard: the construct is a projen {@link Project} - the base every builder here
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/** Reusable uv workspace generation for Python packages hosted in a projen tree. */
|
|
2
|
+
import { string } from "@dbx-tools/shared-core";
|
|
3
|
+
import { Component, TextFile, type Project, javascript, vscode } from "projen";
|
|
4
|
+
import { GithubWorkflow } from "projen/lib/github";
|
|
5
|
+
import { JobPermission } from "projen/lib/github/workflows-model";
|
|
6
|
+
|
|
7
|
+
/** Git location used by direct `#subdirectory=` package dependencies. */
|
|
8
|
+
export interface PythonRepositoryOptions {
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly ref?: string;
|
|
11
|
+
readonly root?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** One independently installable Python package in the uv workspace. */
|
|
15
|
+
export interface PythonPackageOptions {
|
|
16
|
+
readonly directory: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly module: string;
|
|
19
|
+
readonly description: string;
|
|
20
|
+
readonly dependencies?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Python release workflow configuration. */
|
|
24
|
+
export interface PythonReleaseOptions {
|
|
25
|
+
readonly workflowName?: string;
|
|
26
|
+
readonly environment?: string;
|
|
27
|
+
readonly environmentUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Options for {@link DBXToolsPythonWorkspace}. */
|
|
31
|
+
export interface DBXToolsPythonWorkspaceOptions {
|
|
32
|
+
readonly packages: readonly PythonPackageOptions[];
|
|
33
|
+
readonly repository: PythonRepositoryOptions;
|
|
34
|
+
readonly requiresPython?: string;
|
|
35
|
+
readonly ruffTarget?: string;
|
|
36
|
+
readonly workspaceName?: string;
|
|
37
|
+
readonly devDependencies?: readonly string[];
|
|
38
|
+
readonly testPaths?: readonly string[];
|
|
39
|
+
readonly lintPaths?: readonly string[];
|
|
40
|
+
readonly ruffPerFileIgnores?: Readonly<Record<string, readonly string[]>>;
|
|
41
|
+
readonly interpreterPath?: string | false;
|
|
42
|
+
readonly release?: boolean | PythonReleaseOptions;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_DEV_DEPENDENCIES = [
|
|
46
|
+
"pytest>=8.4,<9",
|
|
47
|
+
"pytest-asyncio>=1.1,<2",
|
|
48
|
+
"pyyaml>=6.0,<7",
|
|
49
|
+
"ruff>=0.12,<1",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
const quote = (value: string): string => JSON.stringify(value);
|
|
53
|
+
|
|
54
|
+
/** Repository-relative path for a Python package directory. */
|
|
55
|
+
export function pythonPackagePath(repository: PythonRepositoryOptions, directory: string): string {
|
|
56
|
+
return `${repository.root ?? "packages/py"}/${directory}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** PEP 508 dependency pointing at a sibling package in a Git repository. */
|
|
60
|
+
export function pythonGitDependency(
|
|
61
|
+
repository: PythonRepositoryOptions,
|
|
62
|
+
name: string,
|
|
63
|
+
directory: string,
|
|
64
|
+
): string {
|
|
65
|
+
return `${name} @ git+${repository.url}@${repository.ref ?? "main"}#subdirectory=${pythonPackagePath(repository, directory)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function projectVscode(project: Project): vscode.VsCode | undefined {
|
|
69
|
+
return (project as Project & { readonly vscode?: vscode.VsCode }).vscode;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Generates a root uv workspace, member package metadata, Python tasks, editor
|
|
74
|
+
* interpreter selection, and an optional trusted-publishing workflow.
|
|
75
|
+
*/
|
|
76
|
+
export class DBXToolsPythonWorkspace extends Component {
|
|
77
|
+
readonly packages: readonly PythonPackageOptions[];
|
|
78
|
+
readonly repository: Required<PythonRepositoryOptions>;
|
|
79
|
+
readonly requiresPython: string;
|
|
80
|
+
|
|
81
|
+
constructor(project: javascript.NodeProject, options: DBXToolsPythonWorkspaceOptions) {
|
|
82
|
+
super(project);
|
|
83
|
+
this.packages = options.packages;
|
|
84
|
+
this.repository = {
|
|
85
|
+
url: options.repository.url,
|
|
86
|
+
ref: options.repository.ref ?? "main",
|
|
87
|
+
root: options.repository.root ?? "packages/py",
|
|
88
|
+
};
|
|
89
|
+
this.requiresPython = options.requiresPython ?? ">=3.10";
|
|
90
|
+
|
|
91
|
+
this.emitWorkspace(project, options);
|
|
92
|
+
this.emitPackages(project);
|
|
93
|
+
this.addTasks(project, options);
|
|
94
|
+
|
|
95
|
+
const interpreterPath = options.interpreterPath ?? "${workspaceFolder}/.venv/bin/python";
|
|
96
|
+
if (interpreterPath !== false) {
|
|
97
|
+
projectVscode(project)?.settings.addSetting("python.defaultInterpreterPath", interpreterPath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (options.release) {
|
|
101
|
+
this.addReleaseWorkflow(project, options.release === true ? {} : options.release);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Repository-relative package directory. */
|
|
106
|
+
packagePath(directory: string): string {
|
|
107
|
+
return pythonPackagePath(this.repository, directory);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** PEP 508 dependency pointing at a sibling package in the configured repository. */
|
|
111
|
+
gitDependency(name: string, directory: string): string {
|
|
112
|
+
return pythonGitDependency(this.repository, name, directory);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private emitWorkspace(
|
|
116
|
+
project: javascript.NodeProject,
|
|
117
|
+
options: DBXToolsPythonWorkspaceOptions,
|
|
118
|
+
): void {
|
|
119
|
+
const devDependencies = options.devDependencies ?? DEFAULT_DEV_DEPENDENCIES;
|
|
120
|
+
const testPaths = options.testPaths ?? [this.repository.root];
|
|
121
|
+
const ruffTarget = options.ruffTarget ?? "py310";
|
|
122
|
+
const perFileIgnores = options.ruffPerFileIgnores ?? {};
|
|
123
|
+
new TextFile(project, "pyproject.toml", {
|
|
124
|
+
marker: false,
|
|
125
|
+
readonly: true,
|
|
126
|
+
lines: [
|
|
127
|
+
'# ~~ Generated by projen. To modify, edit .projenrc.ts and run "bunx projen".',
|
|
128
|
+
"[project]",
|
|
129
|
+
`name = ${quote(options.workspaceName ?? `${string.toSlug(project.name)}-python-workspace`)}`,
|
|
130
|
+
'version = "0.0.0"',
|
|
131
|
+
`requires-python = ${quote(this.requiresPython)}`,
|
|
132
|
+
"dependencies = []",
|
|
133
|
+
"",
|
|
134
|
+
"[dependency-groups]",
|
|
135
|
+
`dev = [${devDependencies.map(quote).join(", ")}]`,
|
|
136
|
+
"",
|
|
137
|
+
"[tool.uv]",
|
|
138
|
+
"package = false",
|
|
139
|
+
"",
|
|
140
|
+
"[tool.uv.workspace]",
|
|
141
|
+
`members = [${quote(`${this.repository.root}/*`)}]`,
|
|
142
|
+
"",
|
|
143
|
+
"[tool.uv.sources]",
|
|
144
|
+
...this.packages.map((pkg) => `${pkg.name} = { workspace = true }`),
|
|
145
|
+
"",
|
|
146
|
+
"[tool.pytest.ini_options]",
|
|
147
|
+
'asyncio_mode = "auto"',
|
|
148
|
+
`testpaths = [${testPaths.map(quote).join(", ")}]`,
|
|
149
|
+
"",
|
|
150
|
+
"[tool.ruff]",
|
|
151
|
+
`target-version = ${quote(ruffTarget)}`,
|
|
152
|
+
"line-length = 100",
|
|
153
|
+
"",
|
|
154
|
+
"[tool.ruff.lint.per-file-ignores]",
|
|
155
|
+
...Object.entries(perFileIgnores).map(
|
|
156
|
+
([path, rules]) => `${quote(path)} = [${rules.map(quote).join(", ")}]`,
|
|
157
|
+
),
|
|
158
|
+
"",
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private emitPackages(project: javascript.NodeProject): void {
|
|
164
|
+
for (const pkg of this.packages) {
|
|
165
|
+
new TextFile(project, `${this.packagePath(pkg.directory)}/pyproject.toml`, {
|
|
166
|
+
marker: false,
|
|
167
|
+
readonly: true,
|
|
168
|
+
lines: [
|
|
169
|
+
'# ~~ Generated by projen. To modify, edit .projenrc.ts and run "bunx projen".',
|
|
170
|
+
"[project]",
|
|
171
|
+
`name = ${quote(pkg.name)}`,
|
|
172
|
+
'version = "0.0.0"',
|
|
173
|
+
`description = ${quote(pkg.description)}`,
|
|
174
|
+
'readme = "README.md"',
|
|
175
|
+
`requires-python = ${quote(this.requiresPython)}`,
|
|
176
|
+
`dependencies = [${(pkg.dependencies ?? []).map(quote).join(", ")}]`,
|
|
177
|
+
"",
|
|
178
|
+
"[project.urls]",
|
|
179
|
+
`Source = ${quote(`${this.repository.url.replace(/\.git$/, "")}/tree/${this.repository.ref}/${this.packagePath(pkg.directory)}`)}`,
|
|
180
|
+
"",
|
|
181
|
+
"[build-system]",
|
|
182
|
+
'requires = ["uv_build>=0.11.28,<0.12.0"]',
|
|
183
|
+
'build-backend = "uv_build"',
|
|
184
|
+
"",
|
|
185
|
+
"[tool.uv.build-backend]",
|
|
186
|
+
`module-name = ${quote(pkg.module)}`,
|
|
187
|
+
'module-root = "src"',
|
|
188
|
+
"namespace = true",
|
|
189
|
+
"",
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private addTasks(project: javascript.NodeProject, options: DBXToolsPythonWorkspaceOptions): void {
|
|
196
|
+
const lintPaths = options.lintPaths ?? [this.repository.root];
|
|
197
|
+
project.addTask("py:sync", {
|
|
198
|
+
exec: "uv sync --all-packages",
|
|
199
|
+
description: "Resolve and install every Python workspace package",
|
|
200
|
+
});
|
|
201
|
+
project.addTask("py:test", {
|
|
202
|
+
exec: "uv run pytest",
|
|
203
|
+
description: "Run Python workspace tests",
|
|
204
|
+
});
|
|
205
|
+
project.addTask("py:lint", {
|
|
206
|
+
exec: `uv run ruff check ${lintPaths.join(" ")}`,
|
|
207
|
+
description: "Lint Python workspace packages",
|
|
208
|
+
});
|
|
209
|
+
project.addTask("py:format", {
|
|
210
|
+
exec: `uv run ruff format ${lintPaths.join(" ")}`,
|
|
211
|
+
description: "Format Python workspace packages",
|
|
212
|
+
});
|
|
213
|
+
project.addTask("py:build", {
|
|
214
|
+
exec: "uv build --all-packages",
|
|
215
|
+
description: "Build every Python workspace package",
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private addReleaseWorkflow(project: javascript.NodeProject, options: PythonReleaseOptions): void {
|
|
220
|
+
if (!project.github) return;
|
|
221
|
+
const workflow = new GithubWorkflow(project.github, options.workflowName ?? "python-release");
|
|
222
|
+
workflow.file?.addOverride("permissions", { contents: "read" });
|
|
223
|
+
workflow.on({ workflowDispatch: {} });
|
|
224
|
+
workflow.file?.addOverride("on.workflow_dispatch", {
|
|
225
|
+
inputs: {
|
|
226
|
+
version: {
|
|
227
|
+
description: "Python package version to build",
|
|
228
|
+
type: "string",
|
|
229
|
+
required: true,
|
|
230
|
+
},
|
|
231
|
+
publish: {
|
|
232
|
+
description: "Upload the validated distributions to PyPI",
|
|
233
|
+
type: "boolean",
|
|
234
|
+
default: false,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
workflow.addJob("build", {
|
|
239
|
+
runsOn: ["ubuntu-latest"],
|
|
240
|
+
permissions: { contents: JobPermission.READ },
|
|
241
|
+
timeoutMinutes: 20,
|
|
242
|
+
steps: [
|
|
243
|
+
{ name: "Checkout", uses: "actions/checkout@v6" },
|
|
244
|
+
{ name: "Setup uv", uses: "astral-sh/setup-uv@v7" },
|
|
245
|
+
{
|
|
246
|
+
name: "Stamp workspace versions",
|
|
247
|
+
env: { VERSION: "${{ inputs.version }}" },
|
|
248
|
+
run: this.renderVersionStampScript(),
|
|
249
|
+
},
|
|
250
|
+
{ name: "Build distributions", run: "uv build --all-packages" },
|
|
251
|
+
{
|
|
252
|
+
name: "Validate distributions",
|
|
253
|
+
run: [
|
|
254
|
+
`test "$(find dist -maxdepth 1 -type f | wc -l | tr -d ' ')" -eq ${this.packages.length * 2}`,
|
|
255
|
+
"uvx twine check dist/*",
|
|
256
|
+
].join("\n"),
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "Upload distributions",
|
|
260
|
+
uses: "actions/upload-artifact@v7",
|
|
261
|
+
with: { name: "python-distributions", path: "dist" },
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
workflow.addJob("publish", {
|
|
266
|
+
if: "${{ inputs.publish }}",
|
|
267
|
+
needs: ["build"],
|
|
268
|
+
environment: {
|
|
269
|
+
name: options.environment ?? "pypi",
|
|
270
|
+
url: options.environmentUrl ?? "https://pypi.org/",
|
|
271
|
+
},
|
|
272
|
+
runsOn: ["ubuntu-latest"],
|
|
273
|
+
permissions: { idToken: JobPermission.WRITE },
|
|
274
|
+
timeoutMinutes: 10,
|
|
275
|
+
steps: [
|
|
276
|
+
{
|
|
277
|
+
name: "Download distributions",
|
|
278
|
+
uses: "actions/download-artifact@v8",
|
|
279
|
+
with: { name: "python-distributions", path: "dist" },
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "Publish to PyPI",
|
|
283
|
+
uses: "pypa/gh-action-pypi-publish@release/v1",
|
|
284
|
+
with: { "packages-dir": "dist" },
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private renderVersionStampScript(): string {
|
|
291
|
+
return [
|
|
292
|
+
`chmod -R u+w ${this.repository.root}`,
|
|
293
|
+
"python - <<'PY'",
|
|
294
|
+
"from pathlib import Path",
|
|
295
|
+
"import os",
|
|
296
|
+
"import re",
|
|
297
|
+
"",
|
|
298
|
+
'version = os.environ["VERSION"]',
|
|
299
|
+
`package_files = sorted(Path(${quote(this.repository.root)}).glob("*/pyproject.toml"))`,
|
|
300
|
+
"packages = {}",
|
|
301
|
+
"for path in package_files:",
|
|
302
|
+
' source = path.read_text(encoding="utf-8")',
|
|
303
|
+
' name = re.search(r\'^name = "([^"]+)"$\', source, re.MULTILINE)',
|
|
304
|
+
" if name is None:",
|
|
305
|
+
' raise ValueError(f"Missing project name in {path}")',
|
|
306
|
+
" packages[name.group(1)] = path.parent.name",
|
|
307
|
+
"",
|
|
308
|
+
"for path in package_files:",
|
|
309
|
+
' source = path.read_text(encoding="utf-8")',
|
|
310
|
+
" source, count = re.subn(",
|
|
311
|
+
' r\'^version = "[^"]+"$\',',
|
|
312
|
+
" f'version = \"{version}\"',",
|
|
313
|
+
" source,",
|
|
314
|
+
" count=1,",
|
|
315
|
+
" flags=re.MULTILINE,",
|
|
316
|
+
" )",
|
|
317
|
+
" if count != 1:",
|
|
318
|
+
' raise ValueError(f"Expected one project version in {path}")',
|
|
319
|
+
" for name, directory in packages.items():",
|
|
320
|
+
" source = re.sub(",
|
|
321
|
+
` rf'{re.escape(name)} @ git\\+[^" ]+#subdirectory=${this.repository.root}/{re.escape(directory)}',`,
|
|
322
|
+
' f"{name}=={version}",',
|
|
323
|
+
" source,",
|
|
324
|
+
" )",
|
|
325
|
+
' path.write_text(source, encoding="utf-8")',
|
|
326
|
+
"PY",
|
|
327
|
+
].join("\n");
|
|
328
|
+
}
|
|
329
|
+
}
|