@dbx-tools/projen 0.9.51 → 0.9.54
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 +56 -16
- package/index.ts +9 -6
- package/package.json +5 -6
- package/src/dbx-tools-config.ts +8 -0
- package/src/generated.ts +6 -0
- package/src/packages.ts +6 -0
- package/src/project-py.ts +89 -18
- package/src/project.ts +2 -1
- package/src/python-node-bindings-component.ts +71 -0
- package/src/python-node-bindings.ts +38 -14
- package/src/python-node-runtime.ts +56 -0
- package/src/workspace-lock.ts +31 -13
- package/tasks/publish-python.ts +14 -6
- package/tasks/python-localize-imports.py +188 -0
- package/tasks/python-node-bindings-watch.ts +96 -0
- package/tasks/python-node-bindings.ts +39 -129
- package/tasks/python-node-runtime-watch.ts +40 -0
- package/tasks/python-sync.ts +85 -3
- package/tasks/sync.ts +15 -3
- package/shims/python-node/bootstrap.ts +0 -129
- package/shims/python-node/child-process.ts +0 -96
- package/shims/python-node/crypto.ts +0 -60
- package/shims/python-node/fs-promises.ts +0 -179
- package/shims/python-node/fs.ts +0 -173
- package/shims/python-node/headers.ts +0 -75
- package/shims/python-node/host.ts +0 -341
- package/shims/python-node/os.ts +0 -20
- package/shims/python-node/process.ts +0 -13
- package/shims/python-node/readline.ts +0 -37
- package/shims/python-node/tsconfig.json +0 -24
- package/src/python-node-bundle.ts +0 -67
package/src/workspace-lock.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as fileLock from "@dbx-tools/core/file-lock";
|
|
|
4
4
|
import { log } from "@dbx-tools/shared-core";
|
|
5
5
|
|
|
6
6
|
const MUTATION_LOCK_SCOPE = "dbx-tools-workspace-mutation";
|
|
7
|
+
const WAIT_LOG_INTERVAL_MS = 5_000;
|
|
7
8
|
const logger = log.logger("projen:workspace-lock");
|
|
8
9
|
|
|
9
10
|
/** Optional preflight used before and after acquiring the workspace mutation lock. */
|
|
@@ -33,17 +34,34 @@ export async function withWorkspaceMutationLock<T>(
|
|
|
33
34
|
if (check && !(await check())) {
|
|
34
35
|
return undefined;
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
let waitLogTimer: ReturnType<typeof setInterval> | undefined;
|
|
38
|
+
try {
|
|
39
|
+
return await fileLock.withFileLock(
|
|
40
|
+
[MUTATION_LOCK_SCOPE, repository],
|
|
41
|
+
async () => {
|
|
42
|
+
if (check && !(await check())) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
return callback();
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
backends: process.platform === "win32" ? ["file"] : ["flock", "file"],
|
|
49
|
+
onWait: () => {
|
|
50
|
+
if (waitLogTimer) return;
|
|
51
|
+
const startedAt = Date.now();
|
|
52
|
+
waitLogTimer = setInterval(
|
|
53
|
+
() =>
|
|
54
|
+
logger.info("waiting for workspace mutation lock", {
|
|
55
|
+
repository,
|
|
56
|
+
elapsedMs: Date.now() - startedAt,
|
|
57
|
+
}),
|
|
58
|
+
WAIT_LOG_INTERVAL_MS,
|
|
59
|
+
);
|
|
60
|
+
waitLogTimer.unref?.();
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
} finally {
|
|
65
|
+
if (waitLogTimer) clearInterval(waitLogTimer);
|
|
66
|
+
}
|
|
49
67
|
}
|
package/tasks/publish-python.ts
CHANGED
|
@@ -79,11 +79,9 @@ export function publishPythonProjects(options: {
|
|
|
79
79
|
readonly root: string;
|
|
80
80
|
readonly version: string;
|
|
81
81
|
}): void {
|
|
82
|
-
const
|
|
83
|
-
const workspace = projectUtils.root(root) ?? dirname(root);
|
|
84
|
-
const output = mkdtempSync(join(workspace, ".projen-python-publish-"));
|
|
82
|
+
const output = mkdtempSync(join(tmpdir(), "projen-python-publish-"));
|
|
85
83
|
try {
|
|
86
|
-
|
|
84
|
+
buildPythonProjectsInternal({ ...options, output }, true);
|
|
87
85
|
runTaskCommand(
|
|
88
86
|
process.cwd(),
|
|
89
87
|
"uvx",
|
|
@@ -101,7 +99,7 @@ export function publishPythonProjects(options: {
|
|
|
101
99
|
{ env: { ...process.env, UV_DEFAULT_INDEX: options.indexUrl } },
|
|
102
100
|
);
|
|
103
101
|
} finally {
|
|
104
|
-
|
|
102
|
+
removeTemporaryTree(output);
|
|
105
103
|
}
|
|
106
104
|
}
|
|
107
105
|
|
|
@@ -115,11 +113,21 @@ export function buildPythonProjects(options: {
|
|
|
115
113
|
readonly root: string;
|
|
116
114
|
readonly version: string;
|
|
117
115
|
}): void {
|
|
116
|
+
buildPythonProjectsInternal(options, false);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildPythonProjectsInternal(
|
|
120
|
+
options: Parameters<typeof buildPythonProjects>[0],
|
|
121
|
+
allowOutputOutsideWorkspace: boolean,
|
|
122
|
+
): void {
|
|
118
123
|
const root = resolve(options.root);
|
|
119
124
|
const output = resolve(options.output);
|
|
120
125
|
const workspace = projectUtils.root(root) ?? dirname(root);
|
|
121
126
|
const outputPath = relative(workspace, output);
|
|
122
|
-
if (
|
|
127
|
+
if (
|
|
128
|
+
!allowOutputOutsideWorkspace &&
|
|
129
|
+
(!outputPath || outputPath.startsWith("..") || isAbsolute(outputPath))
|
|
130
|
+
) {
|
|
123
131
|
throw new Error("Python release output must be a non-root directory inside the workspace");
|
|
124
132
|
}
|
|
125
133
|
rmSync(output, { recursive: true, force: true });
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""Rewrite absolute imports of synchronized upstream modules to their generated package.
|
|
2
|
+
|
|
3
|
+
Usage: python-localize-imports.py ROOT TARGET SOURCE [MODULE ...]
|
|
4
|
+
|
|
5
|
+
ROOT holds the synchronized ``.py`` tree and is importable as the dotted package
|
|
6
|
+
TARGET. SOURCE is the dotted path ROOT had upstream (empty when it has none), so
|
|
7
|
+
upstream may import a synchronized module either bare (``config.schema``) or under
|
|
8
|
+
SOURCE (``graphiti_core.driver.postgraph``). MODULE names the upstream modules to
|
|
9
|
+
rewrite; without any, every top-level module in ROOT is rewritten in both forms.
|
|
10
|
+
|
|
11
|
+
Covers ``import``, ``from ... import``, and constant-string ``__import__`` /
|
|
12
|
+
``importlib.import_module`` calls. Prints a JSON object mapping each MODULE to the
|
|
13
|
+
number of references rewritten.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
FROM_MODULE = re.compile(r"from\s+([^\W\d]\w*(?:\s*\.\s*[^\W\d]\w*)*)")
|
|
25
|
+
DYNAMIC_IMPORTS = {"__import__", "import_module"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Localizer:
|
|
29
|
+
def __init__(self, root: Path, target: str, source: str, modules: list[str]) -> None:
|
|
30
|
+
self.target = target
|
|
31
|
+
self.source = source
|
|
32
|
+
local = local_modules(root)
|
|
33
|
+
if modules:
|
|
34
|
+
self.modules = modules
|
|
35
|
+
for module in modules:
|
|
36
|
+
if self.local_name(module).split(".", 1)[0] not in local:
|
|
37
|
+
raise SystemExit(f"localize_imports module {module!r} is not synchronized")
|
|
38
|
+
else:
|
|
39
|
+
shadowed = sorted(local & set(sys.stdlib_module_names))
|
|
40
|
+
if shadowed:
|
|
41
|
+
raise SystemExit(
|
|
42
|
+
f"synchronized modules shadow the standard library: {', '.join(shadowed)}; "
|
|
43
|
+
"list localize_imports modules explicitly"
|
|
44
|
+
)
|
|
45
|
+
self.modules = sorted(local) + (
|
|
46
|
+
[f"{source}.{name}" for name in sorted(local)] if source else []
|
|
47
|
+
)
|
|
48
|
+
# Longest first, so a nested entry owns its imports over its parent.
|
|
49
|
+
self.ordered = sorted(self.modules, key=len, reverse=True)
|
|
50
|
+
self.counts = dict.fromkeys(self.modules, 0)
|
|
51
|
+
|
|
52
|
+
def local_name(self, name: str) -> str:
|
|
53
|
+
if self.source and name.startswith(f"{self.source}."):
|
|
54
|
+
return name[len(self.source) + 1 :]
|
|
55
|
+
return name
|
|
56
|
+
|
|
57
|
+
def owner(self, name: str) -> str | None:
|
|
58
|
+
return next(
|
|
59
|
+
(module for module in self.ordered if name == module or name.startswith(f"{module}.")),
|
|
60
|
+
None,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def rewrite(self, name: str) -> str | None:
|
|
64
|
+
module = self.owner(name)
|
|
65
|
+
if module is None:
|
|
66
|
+
return None
|
|
67
|
+
self.counts[module] += 1
|
|
68
|
+
return f"{self.target}.{self.local_name(name)}"
|
|
69
|
+
|
|
70
|
+
def localize(self, path: Path) -> None:
|
|
71
|
+
data = path.read_bytes()
|
|
72
|
+
tree = ast.parse(data, filename=str(path))
|
|
73
|
+
starts = [0]
|
|
74
|
+
for line in data.splitlines(keepends=True):
|
|
75
|
+
starts.append(starts[-1] + len(line))
|
|
76
|
+
|
|
77
|
+
def span(node: ast.AST) -> tuple[int, int]:
|
|
78
|
+
# AST columns are UTF-8 byte offsets within their line.
|
|
79
|
+
return (
|
|
80
|
+
starts[node.lineno - 1] + node.col_offset,
|
|
81
|
+
starts[node.end_lineno - 1] + node.end_col_offset,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
edits: list[tuple[int, int, bytes]] = []
|
|
85
|
+
for node in ast.walk(tree):
|
|
86
|
+
if isinstance(node, ast.ImportFrom):
|
|
87
|
+
if node.level or not node.module:
|
|
88
|
+
continue
|
|
89
|
+
local = self.rewrite(node.module)
|
|
90
|
+
if local is None:
|
|
91
|
+
continue
|
|
92
|
+
start, end = span(node)
|
|
93
|
+
text = data[start:end].decode()
|
|
94
|
+
found = FROM_MODULE.match(text)
|
|
95
|
+
if not found or re.sub(r"\s", "", found[1]) != node.module:
|
|
96
|
+
raise SystemExit(f"{path}:{node.lineno}: cannot locate module in {text!r}")
|
|
97
|
+
edits.append(
|
|
98
|
+
(
|
|
99
|
+
start + len(text[: found.start(1)].encode()),
|
|
100
|
+
start + len(text[: found.end(1)].encode()),
|
|
101
|
+
local.encode(),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
elif isinstance(node, ast.Import):
|
|
105
|
+
if not any(self.owner(alias.name) for alias in node.names):
|
|
106
|
+
continue
|
|
107
|
+
statements = [self.import_statement(path, node, alias) for alias in node.names]
|
|
108
|
+
start, end = span(node)
|
|
109
|
+
edits.append((start, end, "; ".join(statements).encode()))
|
|
110
|
+
elif isinstance(node, ast.Call) and self.is_dynamic_import(node):
|
|
111
|
+
argument = node.args[0]
|
|
112
|
+
local = self.rewrite(argument.value)
|
|
113
|
+
if local is None:
|
|
114
|
+
continue
|
|
115
|
+
if self.returns_top_level(node):
|
|
116
|
+
# Without a fromlist, `__import__` returns the top-level package.
|
|
117
|
+
raise SystemExit(
|
|
118
|
+
f"{path}:{node.lineno}: pass a fromlist to localize __import__"
|
|
119
|
+
)
|
|
120
|
+
start, end = span(argument)
|
|
121
|
+
text = data[start:end].decode()
|
|
122
|
+
if argument.value not in text:
|
|
123
|
+
raise SystemExit(f"{path}:{node.lineno}: cannot rewrite escaped {text!r}")
|
|
124
|
+
edits.append((start, end, text.replace(argument.value, local, 1).encode()))
|
|
125
|
+
if not edits:
|
|
126
|
+
return
|
|
127
|
+
for start, end, replacement in sorted(edits, reverse=True):
|
|
128
|
+
data = data[:start] + replacement + data[end:]
|
|
129
|
+
ast.parse(data, filename=str(path))
|
|
130
|
+
path.write_bytes(data)
|
|
131
|
+
|
|
132
|
+
def import_statement(self, path: Path, node: ast.Import, alias: ast.alias) -> str:
|
|
133
|
+
local = self.rewrite(alias.name)
|
|
134
|
+
if local is None:
|
|
135
|
+
return f"import {alias.name}" + (f" as {alias.asname}" if alias.asname else "")
|
|
136
|
+
if alias.asname:
|
|
137
|
+
return f"import {local} as {alias.asname}"
|
|
138
|
+
binding, _, submodule = self.local_name(alias.name).partition(".")
|
|
139
|
+
if binding != alias.name.split(".", 1)[0]:
|
|
140
|
+
# `import pkg.sub` binds `pkg`, which no longer names the synchronized tree.
|
|
141
|
+
raise SystemExit(f"{path}:{node.lineno}: alias `import {alias.name}` to localize it")
|
|
142
|
+
if not submodule:
|
|
143
|
+
return f"from {self.target} import {binding}"
|
|
144
|
+
return f"import {local}; from {self.target} import {binding}"
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def is_dynamic_import(node: ast.Call) -> bool:
|
|
148
|
+
function = node.func
|
|
149
|
+
name = function.id if isinstance(function, ast.Name) else getattr(function, "attr", None)
|
|
150
|
+
return (
|
|
151
|
+
name in DYNAMIC_IMPORTS
|
|
152
|
+
and bool(node.args)
|
|
153
|
+
and isinstance(node.args[0], ast.Constant)
|
|
154
|
+
and isinstance(node.args[0].value, str)
|
|
155
|
+
and not node.args[0].value.startswith(".")
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def returns_top_level(node: ast.Call) -> bool:
|
|
160
|
+
if not isinstance(node.func, ast.Name) or node.func.id != "__import__":
|
|
161
|
+
return False
|
|
162
|
+
return len(node.args) < 4 and not any(
|
|
163
|
+
keyword.arg == "fromlist" for keyword in node.keywords
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def local_modules(root: Path) -> set[str]:
|
|
168
|
+
names = set()
|
|
169
|
+
for path in root.rglob("*.py"):
|
|
170
|
+
parts = path.relative_to(root).parts
|
|
171
|
+
name = parts[0] if len(parts) > 1 else path.stem
|
|
172
|
+
if name.isidentifier() and name != "__init__":
|
|
173
|
+
names.add(name)
|
|
174
|
+
return names
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main(argv: list[str]) -> None:
|
|
178
|
+
if len(argv) < 3:
|
|
179
|
+
raise SystemExit(__doc__)
|
|
180
|
+
root, target, source, *modules = argv
|
|
181
|
+
localizer = Localizer(Path(root), target, source, modules)
|
|
182
|
+
for path in sorted(Path(root).rglob("*.py")):
|
|
183
|
+
localizer.localize(path)
|
|
184
|
+
print(json.dumps(localizer.counts))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
main(sys.argv[1:])
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Keep every configured Python Node bridge current with one repository watcher. */
|
|
3
|
+
import { dirname, resolve, sep } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { log } from "@dbx-tools/shared-core";
|
|
6
|
+
import { runTaskCommand } from "../src/_task-command.ts";
|
|
7
|
+
import { repoRoot } from "../src/packages.ts";
|
|
8
|
+
import {
|
|
9
|
+
affectedPythonNodeBindingProjects,
|
|
10
|
+
generatePythonNodeBindings,
|
|
11
|
+
pythonNodeBindingWatchInputs,
|
|
12
|
+
resolveAllPythonNodeBindings,
|
|
13
|
+
type ResolvedPythonNodeBindings,
|
|
14
|
+
} from "../src/python-node-bindings.ts";
|
|
15
|
+
import { watchLoop, watchRoots } from "../src/watch.ts";
|
|
16
|
+
import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
|
|
17
|
+
|
|
18
|
+
const logger = log.logger("projen:python-node-bindings-watch");
|
|
19
|
+
const generator = fileURLToPath(new URL("./python-node-bindings.ts", import.meta.url));
|
|
20
|
+
|
|
21
|
+
function generate(project: string): void {
|
|
22
|
+
runTaskCommand(repoRoot, "bun", [generator, "--root", repoRoot, "--project", project]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function containsPath(parent: string, candidate: string): boolean {
|
|
26
|
+
const resolvedParent = resolve(parent);
|
|
27
|
+
const resolvedCandidate = resolve(candidate);
|
|
28
|
+
return (
|
|
29
|
+
resolvedCandidate === resolvedParent || resolvedCandidate.startsWith(`${resolvedParent}${sep}`)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Watch broad package roots, each configured Python workspace root, and only
|
|
35
|
+
* exceptional inputs such as function overrides that live outside those roots.
|
|
36
|
+
*/
|
|
37
|
+
function watchPaths(configs: readonly ResolvedPythonNodeBindings[]): string[] {
|
|
38
|
+
const roots = watchRoots();
|
|
39
|
+
const pythonRoots = configs.map(({ projectDirectory }) => dirname(projectDirectory));
|
|
40
|
+
const covered = [...roots, ...pythonRoots];
|
|
41
|
+
const explicitInputs = configs
|
|
42
|
+
.flatMap(pythonNodeBindingWatchInputs)
|
|
43
|
+
.filter((input) => !covered.some((root) => containsPath(root, input)));
|
|
44
|
+
return [...new Set([...covered, ...explicitInputs])].sort();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function selection(
|
|
48
|
+
previous: readonly ResolvedPythonNodeBindings[],
|
|
49
|
+
changed: readonly string[],
|
|
50
|
+
): {
|
|
51
|
+
readonly all: boolean;
|
|
52
|
+
readonly configs: ResolvedPythonNodeBindings[];
|
|
53
|
+
readonly projects: string[];
|
|
54
|
+
} {
|
|
55
|
+
const configs = resolveAllPythonNodeBindings(repoRoot);
|
|
56
|
+
const manifests = new Set([...previous, ...configs].map(({ pyproject }) => resolve(pyproject)));
|
|
57
|
+
const all = changed.some((path) => manifests.has(resolve(path)));
|
|
58
|
+
return {
|
|
59
|
+
all,
|
|
60
|
+
configs,
|
|
61
|
+
projects: all ? [] : affectedPythonNodeBindingProjects(repoRoot, configs, changed),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Generate once, then rebuild only bridges affected by each debounced change batch. */
|
|
66
|
+
export async function main(): Promise<void> {
|
|
67
|
+
let configs = resolveAllPythonNodeBindings(repoRoot);
|
|
68
|
+
await withWorkspaceMutationLock(repoRoot, () => generatePythonNodeBindings(repoRoot));
|
|
69
|
+
logger.success("generated all Python Node bindings");
|
|
70
|
+
|
|
71
|
+
watchLoop(
|
|
72
|
+
"python-node-bindings",
|
|
73
|
+
watchPaths(configs),
|
|
74
|
+
(changed) => {
|
|
75
|
+
const next = selection(configs, changed);
|
|
76
|
+
configs = next.configs;
|
|
77
|
+
if (next.all) {
|
|
78
|
+
generatePythonNodeBindings(repoRoot);
|
|
79
|
+
logger.success("regenerated all Python Node bindings");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const project of next.projects) {
|
|
83
|
+
generate(project);
|
|
84
|
+
logger.success(`regenerated ${project} Node bindings`);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
check: (changed) => {
|
|
89
|
+
const next = selection(configs, changed);
|
|
90
|
+
return next.all || next.projects.length > 0;
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (import.meta.main) await main();
|
|
@@ -13,12 +13,14 @@ import stdLibBrowser from "node-stdlib-browser";
|
|
|
13
13
|
import ts from "typescript";
|
|
14
14
|
import { header, makeReadonly, makeWritable } from "../src/generated.ts";
|
|
15
15
|
import { publicFunctionExports, publicNamespaceExports } from "../src/module-exports.ts";
|
|
16
|
+
import { resolveRepoRoot } from "../src/packages.ts";
|
|
16
17
|
import {
|
|
17
|
-
|
|
18
|
+
generatePythonNodeBindings,
|
|
18
19
|
resolvePythonNodeBindings,
|
|
19
20
|
type ResolvedPythonNodeBindings,
|
|
20
21
|
type ResolvedPythonNodeFunctionOverride,
|
|
21
22
|
} from "../src/python-node-bindings.ts";
|
|
23
|
+
import { PYTHON_NODE_RUNTIME_MODULE } from "../src/python-node-runtime.ts";
|
|
22
24
|
|
|
23
25
|
const logger = log.logger("projen:python-node-bindings");
|
|
24
26
|
|
|
@@ -81,11 +83,15 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
81
83
|
root: { type: "string" },
|
|
82
84
|
},
|
|
83
85
|
});
|
|
84
|
-
|
|
86
|
+
const root = values.root ? resolve(values.root) : resolveRepoRoot();
|
|
87
|
+
if (!values.project) {
|
|
88
|
+
if (values.binding !== undefined || values.module !== undefined) {
|
|
89
|
+
throw new Error("--binding and --module require --project");
|
|
90
|
+
}
|
|
91
|
+
generatePythonNodeBindings(root, { check: values.check });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
85
94
|
|
|
86
|
-
const root = values.root
|
|
87
|
-
? resolve(values.root)
|
|
88
|
-
: resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
89
95
|
const configs = resolvePythonNodeBindings(root, values.project).map(discoverBindingModules);
|
|
90
96
|
if (configs.length > 1 && configs.some(({ modules }) => modules.length === 0)) {
|
|
91
97
|
throw new Error(
|
|
@@ -161,21 +167,21 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
161
167
|
functionsByPythonName.set(functionName.pythonName, functionName.javascriptName);
|
|
162
168
|
}
|
|
163
169
|
|
|
164
|
-
const shimRoot = PYTHON_NODE_SHIM_ROOT;
|
|
165
170
|
const overridesByModule = groupOverrides(
|
|
166
171
|
configs.flatMap(({ functionOverrides }) => functionOverrides),
|
|
167
172
|
);
|
|
168
173
|
const shimEntry = "dbx-tools:python-entry";
|
|
169
174
|
const functionOverrideNamespace = "dbx-tools-function-override";
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
const runtimeBuiltins = new Set([
|
|
176
|
+
"child_process",
|
|
177
|
+
"crypto",
|
|
178
|
+
"fs",
|
|
179
|
+
"fs/promises",
|
|
180
|
+
"os",
|
|
181
|
+
"process",
|
|
182
|
+
"readline",
|
|
178
183
|
]);
|
|
184
|
+
const runtimeBuiltinNamespace = "dbx-tools-node-runtime-builtin";
|
|
179
185
|
const bridgeSource = [
|
|
180
186
|
"export const __pythonGet = (target, name) => target[name];",
|
|
181
187
|
"export const __pythonInvokePositioned = (fn, entries) => {",
|
|
@@ -240,12 +246,18 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
240
246
|
namespace: "dbx-tools-python",
|
|
241
247
|
}));
|
|
242
248
|
build.onLoad({ filter: /.*/, namespace: "dbx-tools-python" }, () => ({
|
|
249
|
+
contents: [bridgeSource, ...runtimeExports].join("\n"),
|
|
250
|
+
loader: "ts",
|
|
251
|
+
}));
|
|
252
|
+
build.onLoad({ filter: /.*/, namespace: runtimeBuiltinNamespace }, ({ path }) => ({
|
|
243
253
|
contents: [
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
254
|
+
'const runtime = globalThis[Symbol.for("@dbx-tools/node-runtime/runtime")];',
|
|
255
|
+
"if (!runtime) throw new Error('dbx-tools-node-runtime was not loaded before its generated bundle');",
|
|
256
|
+
`const builtin = runtime.modules[${JSON.stringify(`node:${path}`)}] ?? runtime.modules[${JSON.stringify(path)}];`,
|
|
257
|
+
`if (!builtin) throw new Error(${JSON.stringify(`dbx-tools-node-runtime does not provide ${path}`)});`,
|
|
258
|
+
"module.exports = builtin;",
|
|
247
259
|
].join("\n"),
|
|
248
|
-
loader: "
|
|
260
|
+
loader: "js",
|
|
249
261
|
}));
|
|
250
262
|
build.onLoad({ filter: /.*/, namespace: functionOverrideNamespace }, ({ path }) => {
|
|
251
263
|
const overrides = overridesByModule.get(path);
|
|
@@ -272,8 +284,9 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
|
|
|
272
284
|
return { path, namespace: functionOverrideNamespace };
|
|
273
285
|
}
|
|
274
286
|
const moduleName = path.replace(/^node:/, "");
|
|
275
|
-
|
|
276
|
-
|
|
287
|
+
if (runtimeBuiltins.has(moduleName)) {
|
|
288
|
+
return { path: moduleName, namespace: runtimeBuiltinNamespace };
|
|
289
|
+
}
|
|
277
290
|
const standard = standardAliases[moduleName];
|
|
278
291
|
return standard
|
|
279
292
|
? { path: Bun.resolveSync(standard, dirname(fileURLToPath(import.meta.url))) }
|
|
@@ -1172,125 +1185,27 @@ function pythonRuntimeLoader(source: string): string {
|
|
|
1172
1185
|
"",
|
|
1173
1186
|
"from __future__ import annotations",
|
|
1174
1187
|
"",
|
|
1175
|
-
"import asyncio",
|
|
1176
|
-
"import inspect",
|
|
1177
|
-
"from dataclasses import fields, is_dataclass",
|
|
1178
1188
|
"from pathlib import Path",
|
|
1179
|
-
"from threading import Lock",
|
|
1180
1189
|
"from typing import Any",
|
|
1181
1190
|
"",
|
|
1182
|
-
|
|
1183
|
-
"import pythonmonkey.require",
|
|
1191
|
+
`from ${PYTHON_NODE_RUNTIME_MODULE} import MISSING, RuntimeBundle, load_bundle`,
|
|
1184
1192
|
"",
|
|
1185
|
-
"_RUNTIME:
|
|
1186
|
-
"_LOCK = Lock()",
|
|
1187
|
-
"MISSING = object()",
|
|
1193
|
+
"_RUNTIME: RuntimeBundle | None = None",
|
|
1188
1194
|
"",
|
|
1189
1195
|
"",
|
|
1190
|
-
"def
|
|
1191
|
-
" try:",
|
|
1192
|
-
" asyncio.get_running_loop()",
|
|
1193
|
-
" except RuntimeError:",
|
|
1194
|
-
" async def load() -> Any:",
|
|
1195
|
-
' return pm.require(str(Path(__file__).with_name("_runtime.js")))',
|
|
1196
|
-
"",
|
|
1197
|
-
" return asyncio.run(load())",
|
|
1198
|
-
' return pm.require(str(Path(__file__).with_name("_runtime.js")))',
|
|
1199
|
-
"",
|
|
1200
|
-
"",
|
|
1201
|
-
"def get_runtime() -> Any:",
|
|
1196
|
+
"def get_runtime() -> RuntimeBundle:",
|
|
1202
1197
|
" global _RUNTIME",
|
|
1203
1198
|
" if _RUNTIME is None:",
|
|
1204
|
-
|
|
1205
|
-
" if _RUNTIME is None:",
|
|
1206
|
-
" _RUNTIME = _load_runtime()",
|
|
1199
|
+
' _RUNTIME = load_bundle(Path(__file__).with_name("_runtime.js"), bundle_id=__name__)',
|
|
1207
1200
|
" return _RUNTIME",
|
|
1208
1201
|
"",
|
|
1209
1202
|
"",
|
|
1210
|
-
"def _module(name: str) -> Any:",
|
|
1211
|
-
' return get_runtime()["__pythonModule"](name)',
|
|
1212
|
-
"",
|
|
1213
|
-
"",
|
|
1214
|
-
"def _snake_to_camel(name: str) -> str:",
|
|
1215
|
-
' head, *tail = name.split("_")',
|
|
1216
|
-
' return head + "".join(part[:1].upper() + part[1:] for part in tail)',
|
|
1217
|
-
"",
|
|
1218
|
-
"",
|
|
1219
|
-
"def _to_javascript(value: Any) -> Any:",
|
|
1220
|
-
" if isinstance(value, _NodeObject):",
|
|
1221
|
-
" return value._target",
|
|
1222
|
-
" if is_dataclass(value) and not isinstance(value, type):",
|
|
1223
|
-
" return {",
|
|
1224
|
-
' item.metadata.get("javascript_name", item.name): _to_javascript(field_value)',
|
|
1225
|
-
" for item in fields(value)",
|
|
1226
|
-
" if (field_value := getattr(value, item.name)) is not None",
|
|
1227
|
-
" }",
|
|
1228
|
-
" if isinstance(value, dict):",
|
|
1229
|
-
" return {key: _to_javascript(item) for key, item in value.items()}",
|
|
1230
|
-
" if isinstance(value, (list, tuple)):",
|
|
1231
|
-
" return [_to_javascript(item) for item in value]",
|
|
1232
|
-
" return value",
|
|
1233
|
-
"",
|
|
1234
|
-
"",
|
|
1235
|
-
"def _from_javascript(value: Any) -> Any:",
|
|
1236
|
-
" if isinstance(value, str):",
|
|
1237
|
-
' return value.encode("utf-8").decode("utf-8")',
|
|
1238
|
-
" if value is None or isinstance(value, (int, float, bool)):",
|
|
1239
|
-
" return value",
|
|
1240
|
-
' kind = get_runtime()["__pythonKind"](value)',
|
|
1241
|
-
' if kind == "instance":',
|
|
1242
|
-
" return _NodeObject(value)",
|
|
1243
|
-
' if kind == "array":',
|
|
1244
|
-
" return [_from_javascript(item) for item in value]",
|
|
1245
|
-
' if kind == "record":',
|
|
1246
|
-
" return {str(key): _from_javascript(item) for key, item in value.items()}",
|
|
1247
|
-
" return value",
|
|
1248
|
-
"",
|
|
1249
|
-
"",
|
|
1250
|
-
"async def _resolve(value: Any) -> Any:",
|
|
1251
|
-
" if inspect.isawaitable(value):",
|
|
1252
|
-
" value = await value",
|
|
1253
|
-
" return _from_javascript(value)",
|
|
1254
|
-
"",
|
|
1255
|
-
"",
|
|
1256
|
-
"class _NodeObject:",
|
|
1257
|
-
" def __init__(self, target: Any) -> None:",
|
|
1258
|
-
" self._target = target",
|
|
1259
|
-
"",
|
|
1260
|
-
" def __getattr__(self, name: str) -> Any:",
|
|
1261
|
-
" javascript_name = _snake_to_camel(name)",
|
|
1262
|
-
' value = get_runtime()["__pythonGet"](self._target, javascript_name)',
|
|
1263
|
-
" if not callable(value):",
|
|
1264
|
-
" return _from_javascript(value)",
|
|
1265
|
-
"",
|
|
1266
|
-
" async def invoke(*args: Any) -> Any:",
|
|
1267
|
-
' result = await get_runtime()["__pythonInvokeMethod"](',
|
|
1268
|
-
" self._target,",
|
|
1269
|
-
" javascript_name,",
|
|
1270
|
-
" [_to_javascript(arg) for arg in args],",
|
|
1271
|
-
" )",
|
|
1272
|
-
' if result["ok"]:',
|
|
1273
|
-
' return _from_javascript(result["value"])',
|
|
1274
|
-
' error = result["error"]',
|
|
1275
|
-
" message = f\"{error['name']}: {error['message']}\"",
|
|
1276
|
-
' if error.get("stack"):',
|
|
1277
|
-
" message = f\"{message}\\n{error['stack']}\"",
|
|
1278
|
-
" raise RuntimeError(message)",
|
|
1279
|
-
"",
|
|
1280
|
-
" return invoke",
|
|
1281
|
-
"",
|
|
1282
|
-
"",
|
|
1283
1203
|
"async def invoke_positioned(",
|
|
1284
1204
|
" module: str,",
|
|
1285
1205
|
" name: str,",
|
|
1286
1206
|
" arguments: list[tuple[int, Any]],",
|
|
1287
1207
|
") -> Any:",
|
|
1288
|
-
" return await
|
|
1289
|
-
' get_runtime()["__pythonInvokePositioned"](',
|
|
1290
|
-
" _module(module)[name],",
|
|
1291
|
-
" [[index, _to_javascript(value)] for index, value in arguments],",
|
|
1292
|
-
" ),",
|
|
1293
|
-
" )",
|
|
1208
|
+
" return await get_runtime().invoke_positioned(module, name, arguments)",
|
|
1294
1209
|
"",
|
|
1295
1210
|
"",
|
|
1296
1211
|
"def invoke_positioned_sync(",
|
|
@@ -1298,12 +1213,7 @@ function pythonRuntimeLoader(source: string): string {
|
|
|
1298
1213
|
" name: str,",
|
|
1299
1214
|
" arguments: list[tuple[int, Any]],",
|
|
1300
1215
|
") -> Any:",
|
|
1301
|
-
" return
|
|
1302
|
-
' get_runtime()["__pythonInvokePositioned"](',
|
|
1303
|
-
" _module(module)[name],",
|
|
1304
|
-
" [[index, _to_javascript(value)] for index, value in arguments],",
|
|
1305
|
-
" ),",
|
|
1306
|
-
" )",
|
|
1216
|
+
" return get_runtime().invoke_positioned_sync(module, name, arguments)",
|
|
1307
1217
|
"",
|
|
1308
1218
|
].join("\n");
|
|
1309
1219
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env -S bun
|
|
2
|
+
/** Keep the one package-owned PythonMonkey shim runtime current. */
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { log } from "@dbx-tools/shared-core";
|
|
7
|
+
import { runTaskCommand } from "../src/_task-command.ts";
|
|
8
|
+
import { repoRoot } from "../src/packages.ts";
|
|
9
|
+
import { watchLoop } from "../src/watch.ts";
|
|
10
|
+
import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
|
|
11
|
+
|
|
12
|
+
const logger = log.logger("projen:python-node-runtime-watch");
|
|
13
|
+
|
|
14
|
+
export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
|
|
15
|
+
const { values } = parseArgs({
|
|
16
|
+
args,
|
|
17
|
+
options: { project: { type: "string" } },
|
|
18
|
+
strict: true,
|
|
19
|
+
});
|
|
20
|
+
const project = values.project?.trim();
|
|
21
|
+
if (!project) throw new Error("--project is required");
|
|
22
|
+
|
|
23
|
+
const projectDirectory = resolve(repoRoot, project);
|
|
24
|
+
const buildScript = resolve(projectDirectory, "build-runtime.ts");
|
|
25
|
+
const shimDirectory = resolve(projectDirectory, "shims");
|
|
26
|
+
for (const path of [buildScript, shimDirectory]) {
|
|
27
|
+
if (!existsSync(path)) throw new Error(`Node runtime input does not exist: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const build = (): void => runTaskCommand(repoRoot, "bun", [buildScript]);
|
|
31
|
+
await withWorkspaceMutationLock(repoRoot, build);
|
|
32
|
+
logger.success("built shared Python Node runtime");
|
|
33
|
+
|
|
34
|
+
watchLoop("python-node-runtime", [buildScript, shimDirectory], () => {
|
|
35
|
+
build();
|
|
36
|
+
logger.success("rebuilt shared Python Node runtime");
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (import.meta.main) await main();
|