@dbx-tools/projen 0.9.50 → 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.
@@ -20,8 +20,13 @@ import {
20
20
  } from "node:fs";
21
21
  import { tmpdir } from "node:os";
22
22
  import { basename, dirname, join, resolve } from "node:path";
23
+ import * as exec from "@dbx-tools/core/exec";
23
24
  import { withFileLock } from "@dbx-tools/core/file-lock";
24
25
  import { parse } from "smol-toml";
26
+ import { PYTHON_GENERATED_PACKAGE, PYTHON_SYNC_PACKAGE } from "../src/generated.ts";
27
+
28
+ /** AST rewriter that points absolute imports of synchronized modules at their generated package. */
29
+ const LOCALIZER = join(import.meta.dirname, "python-localize-imports.py");
25
30
 
26
31
  interface SyncConfig {
27
32
  readonly name?: string;
@@ -29,6 +34,7 @@ interface SyncConfig {
29
34
  readonly include?: readonly string[];
30
35
  readonly exclude?: readonly string[];
31
36
  readonly replace?: Readonly<Record<string, string>>;
37
+ readonly localize_imports?: boolean | readonly string[];
32
38
  }
33
39
 
34
40
  interface SyncManifest {
@@ -63,21 +69,33 @@ export async function main(args: readonly string[] = process.argv.slice(2)): Pro
63
69
  const moduleName = build?.["module-name"];
64
70
  if (!moduleName) throw new Error(`${project}/pyproject.toml has no uv module-name`);
65
71
  const moduleRoot = resolve(project, build?.["module-root"] ?? "src");
66
- const generatedRoot = join(moduleRoot, ...moduleName.split("."), "_generated", "sync");
67
- for (const config of configs) await synchronize(config, generatedRoot, { check, force });
72
+ // The import package and the output directory derive from the same segments.
73
+ const generatedPackage = [moduleName, PYTHON_GENERATED_PACKAGE, PYTHON_SYNC_PACKAGE].join(".");
74
+ const generatedRoot = join(moduleRoot, ...generatedPackage.split("."));
75
+ for (const config of configs) {
76
+ await synchronize(config, generatedRoot, generatedPackage, { check, force });
77
+ }
68
78
  }
69
79
 
70
80
  async function synchronize(
71
81
  config: SyncConfig,
72
82
  generatedRoot: string,
83
+ generatedPackage: string,
73
84
  options: { readonly check: boolean; readonly force: boolean },
74
85
  ): Promise<void> {
75
86
  const source = parseSource(config.source);
76
87
  const name = config.name ?? defaultName(source);
77
88
  const target = join(generatedRoot, name);
89
+ const localize = localizedModules(config, name);
90
+ const importPackage = `${generatedPackage}.${name}`;
78
91
  const optionsHash = hash({
79
92
  exclude: [...(config.exclude ?? [])].sort(),
80
93
  include: [...(config.include ?? ["**/*"])].sort(),
94
+ localize: localize && {
95
+ localizer: hash(readFileSync(LOCALIZER, "utf8")),
96
+ modules: [...localize].sort(),
97
+ package: importPackage,
98
+ },
81
99
  name,
82
100
  replace: config.replace ?? {},
83
101
  source: config.source,
@@ -110,7 +128,13 @@ async function synchronize(
110
128
  return;
111
129
  }
112
130
  if (options.check) throw new Error(`Generated Python sync is stale: ${target}`);
113
- replaceTarget(config, source, target, { optionsHash, resolvedCommit });
131
+ replaceTarget(
132
+ config,
133
+ source,
134
+ target,
135
+ localize && { modules: localize, package: importPackage },
136
+ { optionsHash, resolvedCommit },
137
+ );
114
138
  });
115
139
  }
116
140
 
@@ -118,6 +142,7 @@ function replaceTarget(
118
142
  config: SyncConfig,
119
143
  source: ParsedSource,
120
144
  target: string,
145
+ localize: { readonly modules: readonly string[]; readonly package: string } | undefined,
121
146
  manifest: Omit<SyncManifest, "source">,
122
147
  ): void {
123
148
  const temporary = mkdtempSync(join(tmpdir(), "dbx-tools-python-sync-"));
@@ -168,6 +193,7 @@ function replaceTarget(
168
193
  `Python sync replacements did not match ${source.repository}: ${missing.join(", ")}`,
169
194
  );
170
195
  }
196
+ if (localize) localizeImports(staging, source, localize.package, localize.modules);
171
197
  const license = join(checkout, "LICENSE");
172
198
  if (existsSync(license)) cpSync(license, join(staging, "LICENSE.upstream"));
173
199
  writeFileSync(
@@ -184,6 +210,62 @@ function replaceTarget(
184
210
  }
185
211
  }
186
212
 
213
+ /**
214
+ * Upstream modules whose imports are rewritten: empty to detect every synchronized
215
+ * module, or `undefined` when localization is off. A sync whose name is not a Python
216
+ * identifier is not importable as a package, so it only localizes when asked to.
217
+ */
218
+ function localizedModules(config: SyncConfig, name: string): readonly string[] | undefined {
219
+ const option = config.localize_imports;
220
+ if (option === false) return undefined;
221
+ if (Array.isArray(option) && option.length === 0) {
222
+ throw new Error(`Python sync ${name} lists no localize_imports modules; use false instead`);
223
+ }
224
+ if (!PYTHON_IDENTIFIER.test(name)) {
225
+ if (option === undefined) return undefined;
226
+ throw new Error(`Python sync name ${name} must be a Python identifier to localize imports`);
227
+ }
228
+ return Array.isArray(option) ? option : [];
229
+ }
230
+
231
+ const PYTHON_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
232
+
233
+ function localizeImports(
234
+ staging: string,
235
+ source: ParsedSource,
236
+ importPackage: string,
237
+ modules: readonly string[],
238
+ ): void {
239
+ // Upstream code may import its modules under the subdirectory's dotted path.
240
+ const segments = (source.subdirectory ?? "").split("/").filter(Boolean);
241
+ const sourcePackage = segments.every((segment) => PYTHON_IDENTIFIER.test(segment))
242
+ ? segments.join(".")
243
+ : "";
244
+ // Captured stderr carries the localizer's reason into the thrown error.
245
+ const { stdout } = exec.spawnSync(
246
+ "uv",
247
+ [
248
+ "run",
249
+ "--no-project",
250
+ "--quiet",
251
+ "python",
252
+ LOCALIZER,
253
+ staging,
254
+ importPackage,
255
+ sourcePackage,
256
+ ...modules,
257
+ ],
258
+ { cwd: staging, stdout: "capture", stderr: "capture", stdin: "ignore", check: true },
259
+ );
260
+ const counts = JSON.parse(stdout ?? "{}") as Record<string, number>;
261
+ const missing = modules.filter((module) => !counts[module]);
262
+ if (missing.length > 0) {
263
+ throw new Error(
264
+ `Python sync localize_imports did not match ${source.repository}: ${missing.join(", ")}`,
265
+ );
266
+ }
267
+ }
268
+
187
269
  function parseSource(value: string): ParsedSource {
188
270
  const match = /^(?:(.*?)\s+@\s+)?git\+(.+?)@([^#]+)(?:#subdirectory=(.+))?$/.exec(value);
189
271
  if (!match) throw new Error(`Invalid pip Git source: ${value}`);
package/tasks/sync.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env -S bun
2
- /** Synthesize once or supervise the focused Projen and barrel watch loops. */
2
+ /** Synthesize once or supervise Projen, barrels, and registered focused watchers. */
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { log } from "@dbx-tools/shared-core";
5
5
  import concurrently from "concurrently";
6
- import { repoRoot } from "../src/packages.ts";
6
+ import { repoRoot, syncWatchTasks } from "../src/packages.ts";
7
7
  import { runSynth } from "../src/scaffold.ts";
8
8
  import { withWorkspaceMutationLock } from "../src/workspace-lock.ts";
9
9
 
@@ -36,6 +36,15 @@ function watcher(script: string, name: string, prefixColor: string, ...args: str
36
36
  };
37
37
  }
38
38
 
39
+ /** One Projen task registered by a component as a focused sync watcher. */
40
+ function taskWatcher(task: string) {
41
+ return {
42
+ command: ["bun", "run", JSON.stringify(task)].join(" "),
43
+ name: task.replace(/:watch$/, ""),
44
+ prefixColor: "yellow",
45
+ };
46
+ }
47
+
39
48
  export async function main(args: string[] = process.argv.slice(2)): Promise<void> {
40
49
  if (!args.includes("--watch")) {
41
50
  // One-shot: full synth (+install + barrels via the post-synth component). This is
@@ -48,7 +57,9 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
48
57
  // watchers under `concurrently`. The projenrc watcher is the intelligent stand-in
49
58
  // for stock `projen --watch` - it re-synths (+install) ONLY when `.projenrc.ts` or
50
59
  // a configured `syncResynthPaths` entry changes, while the barrel watcher keeps
51
- // generated package exports fresh on source edits with no full synth.
60
+ // generated package exports fresh on source edits with no full synth. Components
61
+ // can register other repository-wide watchers, such as Python Node bindings and
62
+ // the shared Python Node runtime, through the generated dbxToolsConfig record.
52
63
  //
53
64
  // VS Code auto-runs this on folder open, so from here down nothing is allowed to be
54
65
  // fatal: errors are logged and retried, and only a stop signal ends the task.
@@ -68,6 +79,7 @@ export async function main(args: string[] = process.argv.slice(2)): Promise<void
68
79
  const watchers = [
69
80
  watcher("projenrc.ts", "projenrc", "magenta"),
70
81
  watcher("barrels.ts", "barrels", "cyan", "--watch"),
82
+ ...syncWatchTasks().map(taskWatcher),
71
83
  ];
72
84
  const { result } = concurrently(watchers, {
73
85
  prefix: "name",
@@ -1,129 +0,0 @@
1
- import { Buffer } from "node:buffer";
2
-
3
- import { PythonHeaders } from "./headers.ts";
4
- import { installPythonGlobals, pythonHost } from "./host.ts";
5
-
6
- installPythonGlobals();
7
-
8
- (globalThis as typeof globalThis & { Buffer?: typeof Buffer }).Buffer = Buffer;
9
-
10
- const globals = globalThis as typeof globalThis & {
11
- AbortController?: typeof AbortController;
12
- Headers?: typeof Headers;
13
- Request?: typeof Request;
14
- Response?: typeof Response;
15
- structuredClone?: typeof structuredClone;
16
- TextDecoder?: typeof TextDecoder;
17
- TextEncoder?: typeof TextEncoder;
18
- fetch?: typeof fetch;
19
- };
20
- globals.AbortController ??= class AbortController {
21
- readonly signal: AbortSignal;
22
- readonly #listeners = new Set<() => void>();
23
-
24
- constructor() {
25
- this.signal = {
26
- aborted: false,
27
- addEventListener: (name: string, listener: EventListenerOrEventListenerObject) => {
28
- if (name === "abort") {
29
- this.#listeners.add(
30
- typeof listener === "function"
31
- ? () => listener(new Event("abort"))
32
- : () => listener.handleEvent(new Event("abort")),
33
- );
34
- }
35
- },
36
- removeEventListener: () => {},
37
- } as AbortSignal;
38
- }
39
-
40
- abort(): void {
41
- if (this.signal.aborted) return;
42
- Object.defineProperty(this.signal, "aborted", { value: true });
43
- for (const listener of this.#listeners) listener();
44
- this.#listeners.clear();
45
- }
46
- } as typeof AbortController;
47
-
48
- globals.Headers ??= PythonHeaders as unknown as typeof Headers;
49
-
50
- class PythonResponse {
51
- readonly headers: Headers;
52
- readonly ok: boolean;
53
- readonly status: number;
54
- readonly #body: Uint8Array;
55
-
56
- constructor(body: Uint8Array, init: ResponseInit = {}) {
57
- this.#body = body;
58
- this.status = init.status ?? 200;
59
- this.ok = this.status >= 200 && this.status < 300;
60
- this.headers = new Headers(init.headers);
61
- }
62
-
63
- async arrayBuffer(): Promise<ArrayBuffer> {
64
- return this.#body.slice().buffer;
65
- }
66
-
67
- async text(): Promise<string> {
68
- return new TextDecoder().decode(this.#body);
69
- }
70
-
71
- async json(): Promise<unknown> {
72
- return JSON.parse(await this.text());
73
- }
74
- }
75
-
76
- globals.Response ??= PythonResponse as unknown as typeof Response;
77
- globals.structuredClone ??= ((value: unknown) => cloneStructured(value)) as typeof structuredClone;
78
- globals.TextEncoder ??= class TextEncoder {
79
- encode(value: string): Uint8Array {
80
- const encoded = unescape(encodeURIComponent(String(value)));
81
- return Uint8Array.from(encoded, (character) => character.charCodeAt(0));
82
- }
83
- } as typeof TextEncoder;
84
- globals.TextDecoder ??= class TextDecoder {
85
- decode(value?: ArrayBuffer | ArrayBufferView): string {
86
- const bytes =
87
- value instanceof ArrayBuffer
88
- ? new Uint8Array(value)
89
- : value
90
- ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
91
- : new Uint8Array();
92
- const encoded = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
93
- return decodeURIComponent(escape(encoded));
94
- }
95
- } as typeof TextDecoder;
96
- globals.fetch ??= (async (input: string | URL | Request, init: RequestInit = {}) => {
97
- const request = typeof input === "object" && "url" in input ? input : undefined;
98
- const headers = init.headers ?? request?.headers;
99
- const response = await pythonHost().http.fetch(
100
- request?.url ?? String(input),
101
- init.method ?? request?.method,
102
- headers ? Object.fromEntries(new Headers(headers).entries()) : undefined,
103
- typeof init.body === "string" ? init.body : undefined,
104
- );
105
- return new Response(Uint8Array.from(response.body), {
106
- status: response.status,
107
- headers: response.headers,
108
- });
109
- }) as typeof fetch;
110
-
111
- function cloneStructured(value: unknown, seen = new Map<object, unknown>()): unknown {
112
- if (value === null || typeof value !== "object") return value;
113
- const cached = seen.get(value);
114
- if (cached !== undefined) return cached;
115
- if (value instanceof Date) return new Date(value.getTime());
116
- if (Array.isArray(value)) {
117
- const clone: unknown[] = [];
118
- seen.set(value, clone);
119
- for (const item of value) clone.push(cloneStructured(item, seen));
120
- return clone;
121
- }
122
- if (Object.getPrototypeOf(value) === Object.prototype) {
123
- const clone: Record<string, unknown> = {};
124
- seen.set(value, clone);
125
- for (const [key, item] of Object.entries(value)) clone[key] = cloneStructured(item, seen);
126
- return clone;
127
- }
128
- throw new TypeError(`PythonMonkey structuredClone does not support ${value.constructor.name}`);
129
- }
@@ -1,96 +0,0 @@
1
- import { EventEmitter } from "node:events";
2
- import { Readable, Writable } from "node:stream";
3
-
4
- import { pythonHost, type PythonProcessHandle } from "./host.ts";
5
-
6
- interface SpawnOptions {
7
- env?: Record<string, string>;
8
- signal?: AbortSignal;
9
- }
10
-
11
- class PythonChildProcess extends EventEmitter {
12
- readonly pid = undefined;
13
- readonly stdin = new Writable({ write: (_chunk, _encoding, callback) => callback() });
14
- readonly stdout = new Readable({ read() {} });
15
- readonly stderr = new Readable({ read() {} });
16
- private active = true;
17
- private readonly handle: PythonProcessHandle;
18
-
19
- constructor(command: string, args: string[], options: SpawnOptions) {
20
- super();
21
- this.handle = pythonHost().process.start(command, args, options.env, undefined);
22
- options.signal?.addEventListener("abort", () => {
23
- if (!this.active) return;
24
- this.active = false;
25
- // Actually terminate the Python child so an aborted call leaks nothing.
26
- this.handle.kill();
27
- this.stdout.push(null);
28
- this.stderr.push(null);
29
- this.emit(
30
- "error",
31
- Object.assign(new Error("Process execution was aborted"), { code: "ABORT_ERR" }),
32
- );
33
- });
34
- void this.handle.wait().then(
35
- (result) => {
36
- if (!this.active) return;
37
- this.active = false;
38
- if (result.stdout) this.stdout.push(result.stdout);
39
- if (result.stderr) this.stderr.push(result.stderr);
40
- this.stdout.push(null);
41
- this.stderr.push(null);
42
- this.emit("close", result.exitCode);
43
- this.emit("exit", result.exitCode);
44
- },
45
- (error) => {
46
- if (!this.active) return;
47
- this.active = false;
48
- this.stdout.push(null);
49
- this.stderr.push(null);
50
- this.emit("error", error);
51
- },
52
- );
53
- }
54
-
55
- kill(): boolean {
56
- this.handle.kill();
57
- return true;
58
- }
59
- }
60
-
61
- export function spawn(command: string, args: string[] = [], options: SpawnOptions = {}) {
62
- return new PythonChildProcess(command, args, options);
63
- }
64
-
65
- export function execFile(
66
- command: string,
67
- args: string[],
68
- _options: object,
69
- callback: (error: Error | null, result?: { stdout: string; stderr: string }) => void,
70
- ): void {
71
- void pythonHost()
72
- .process.start(command, args, undefined, undefined)
73
- .wait()
74
- .then(
75
- (result) => {
76
- if (result.exitCode === 0) {
77
- callback(null, { stdout: result.stdout ?? "", stderr: result.stderr ?? "" });
78
- } else {
79
- callback(
80
- Object.assign(new Error(result.stderr || `Command exited ${result.exitCode}`), {
81
- code: result.exitCode,
82
- }),
83
- );
84
- }
85
- },
86
- (error) => callback(error instanceof Error ? error : new Error(String(error))),
87
- );
88
- }
89
-
90
- export function spawnSync(): never {
91
- throw Object.assign(new Error("Synchronous subprocesses are unavailable in PythonMonkey"), {
92
- code: "ENOSYS",
93
- });
94
- }
95
-
96
- export default { execFile, spawn, spawnSync };
@@ -1,60 +0,0 @@
1
- import { pythonHost } from "./host.ts";
2
-
3
- class PythonHash {
4
- private readonly content: number[] = [];
5
-
6
- constructor(private readonly algorithm: string) {}
7
-
8
- update(value: string | ArrayBuffer | ArrayBufferView): this {
9
- const bytes =
10
- typeof value === "string"
11
- ? new TextEncoder().encode(value)
12
- : value instanceof ArrayBuffer
13
- ? new Uint8Array(value)
14
- : new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
15
- this.content.push(...bytes);
16
- return this;
17
- }
18
-
19
- digest(encoding?: "hex" | "base64"): string | Uint8Array {
20
- const digest = pythonHost().crypto.digest(this.algorithm, this.content, encoding);
21
- if (typeof digest === "string") return digest;
22
- const bytes = Uint8Array.from(digest) as Uint8Array & {
23
- readBigInt64BE(offset: number): bigint;
24
- };
25
- bytes.readBigInt64BE = (offset) => {
26
- let value = 0n;
27
- for (const byte of bytes.slice(offset, offset + 8)) {
28
- value = (value << 8n) | BigInt(byte);
29
- }
30
- return BigInt.asIntN(64, value);
31
- };
32
- return bytes;
33
- }
34
- }
35
-
36
- export function createHash(algorithm: string): PythonHash {
37
- const normalized = algorithm.toLowerCase();
38
- if (!getHashes().includes(normalized)) {
39
- throw new Error(`Unsupported hash algorithm: ${algorithm}`);
40
- }
41
- return new PythonHash(normalized);
42
- }
43
-
44
- export function getHashes(): string[] {
45
- return pythonHost().crypto.hashes();
46
- }
47
-
48
- export function randomBytes(length: number): Uint8Array {
49
- return Uint8Array.from(pythonHost().crypto.randomBytes(length));
50
- }
51
-
52
- export function randomUUID(): string {
53
- const bytes = randomBytes(16);
54
- bytes[6] = (bytes[6]! & 0x0f) | 0x40;
55
- bytes[8] = (bytes[8]! & 0x3f) | 0x80;
56
- const hex = Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
57
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
58
- }
59
-
60
- export default { createHash, getHashes, randomBytes, randomUUID };
@@ -1,179 +0,0 @@
1
- import { Buffer } from "node:buffer";
2
-
3
- import { pythonHost } from "./host.ts";
4
-
5
- function nodeError(error: unknown, code: string, path: string): Error {
6
- return Object.assign(error instanceof Error ? error : new Error(String(error)), { code, path });
7
- }
8
-
9
- function translatePythonError(cause: unknown, path: string): never {
10
- const message = String(cause);
11
- if (/FileNotFoundError|Errno 2/.test(message)) throw nodeError(cause, "ENOENT", path);
12
- if (/FileExistsError|Errno 17/.test(message)) throw nodeError(cause, "EEXIST", path);
13
- if (/PermissionError|Errno 13/.test(message)) throw nodeError(cause, "EACCES", path);
14
- throw cause;
15
- }
16
-
17
- export async function access(path: string): Promise<void> {
18
- try {
19
- await pythonHost().file.stat(String(path));
20
- } catch (cause) {
21
- translatePythonError(cause, path);
22
- }
23
- }
24
-
25
- export async function chmod(path: string, mode: number): Promise<void> {
26
- await pythonHost().file.chmod(String(path), mode);
27
- }
28
-
29
- export async function appendFile(
30
- path: string,
31
- content: string | ArrayBuffer | ArrayBufferView,
32
- ): Promise<void> {
33
- const bytes =
34
- typeof content === "string"
35
- ? new TextEncoder().encode(content)
36
- : content instanceof ArrayBuffer
37
- ? new Uint8Array(content)
38
- : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
39
- await pythonHost().file.appendBytes(String(path), Array.from(bytes));
40
- }
41
-
42
- export async function copyFile(source: string, destination: string): Promise<void> {
43
- await pythonHost().file.copy(String(source), String(destination));
44
- }
45
-
46
- export async function mkdir(
47
- path: string,
48
- options: { recursive?: boolean } = {},
49
- ): Promise<string | undefined> {
50
- let created: boolean;
51
- try {
52
- created = await pythonHost().file.mkdir(String(path), options.recursive === true);
53
- } catch (cause) {
54
- if (/FileExistsError|Errno 17/.test(String(cause))) {
55
- if (options.recursive) return undefined;
56
- throw nodeError(cause, "EEXIST", path);
57
- }
58
- throw cause;
59
- }
60
- if (!created && !options.recursive) throw nodeError(new Error(`EEXIST: ${path}`), "EEXIST", path);
61
- return created ? String(path) : undefined;
62
- }
63
-
64
- export async function mkdtemp(prefix: string): Promise<string> {
65
- return pythonHost().file.mkdtemp(String(prefix));
66
- }
67
-
68
- export async function readdir(
69
- path: string,
70
- options: { withFileTypes?: boolean } = {},
71
- ): Promise<unknown[]> {
72
- const entries = await pythonHost().file.readDirectory(String(path));
73
- if (!options.withFileTypes) return entries.map((entry) => entry.name);
74
- return entries.map((entry) => ({
75
- name: entry.name,
76
- isDirectory: () => entry.directory,
77
- isFile: () => entry.file,
78
- isSymbolicLink: () => false,
79
- }));
80
- }
81
-
82
- export async function readFile(
83
- path: string,
84
- options?: string | { encoding?: string | null },
85
- ): Promise<string | Uint8Array> {
86
- try {
87
- const bytes = Uint8Array.from(await pythonHost().file.readBytes(String(path)));
88
- const encoding = typeof options === "string" ? options : options?.encoding;
89
- return encoding ? new TextDecoder(encoding).decode(bytes) : Buffer.from(bytes);
90
- } catch (cause) {
91
- translatePythonError(cause, path);
92
- }
93
- }
94
-
95
- export async function realpath(path: string): Promise<string> {
96
- try {
97
- return await pythonHost().file.realpath(String(path));
98
- } catch (cause) {
99
- translatePythonError(cause, path);
100
- }
101
- }
102
-
103
- export async function rename(source: string, destination: string): Promise<void> {
104
- await pythonHost().file.rename(String(source), String(destination));
105
- }
106
-
107
- export async function rm(
108
- path: string,
109
- options: { recursive?: boolean; force?: boolean } = {},
110
- ): Promise<void> {
111
- await pythonHost().file.remove(String(path), options.recursive === true, options.force === true);
112
- }
113
-
114
- export async function unlink(path: string): Promise<void> {
115
- // No synchronous existence precheck: let the async remove run and treat a
116
- // missing file as success, so this never blocks on a sync Python call.
117
- try {
118
- await pythonHost().file.remove(String(path), false, true);
119
- } catch (cause) {
120
- if (/FileNotFoundError|Errno 2|ENOENT/.test(String(cause))) return;
121
- translatePythonError(cause, path);
122
- }
123
- }
124
-
125
- export async function stat(path: string): Promise<unknown> {
126
- let value: Awaited<ReturnType<ReturnType<typeof pythonHost>["file"]["stat"]>>;
127
- try {
128
- value = await pythonHost().file.stat(String(path));
129
- } catch (cause) {
130
- translatePythonError(cause, path);
131
- }
132
- return {
133
- mode: value.mode,
134
- mtime: new Date(value.mtimeMs),
135
- mtimeMs: value.mtimeMs,
136
- size: value.size,
137
- isDirectory: () => value.directory,
138
- isFile: () => value.file,
139
- isSymbolicLink: () => false,
140
- };
141
- }
142
-
143
- export async function writeFile(
144
- path: string,
145
- content: string | ArrayBuffer | ArrayBufferView,
146
- options: { mode?: number } = {},
147
- ): Promise<void> {
148
- const bytes =
149
- typeof content === "string"
150
- ? new TextEncoder().encode(content)
151
- : content instanceof ArrayBuffer
152
- ? new Uint8Array(content)
153
- : new Uint8Array(content.buffer, content.byteOffset, content.byteLength);
154
- await pythonHost().file.writeBytes(String(path), Array.from(bytes), options.mode);
155
- }
156
-
157
- export async function open(): Promise<never> {
158
- throw Object.assign(new Error("File descriptors are unavailable in PythonMonkey"), {
159
- code: "ENOSYS",
160
- });
161
- }
162
-
163
- export default {
164
- access,
165
- appendFile,
166
- chmod,
167
- copyFile,
168
- mkdir,
169
- mkdtemp,
170
- open,
171
- readFile,
172
- readdir,
173
- realpath,
174
- rename,
175
- rm,
176
- stat,
177
- unlink,
178
- writeFile,
179
- };