Skip to content
File

Blob: src/pyodide/internal/util.ts

typescript143 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5// Callback used to report PythonWorkersInternalError construction to C++ metrics (via the
6// WorkerFatalReporter module). Registered by `python.ts` at module init in the main workerd
7// context.
8//
9// We can't `import` `pyodide-internal:fatal-reporter` (a C++ extension module) directly here
10// because `util.ts` is also esbuild-bundled into the pool context (`pool/emscriptenSetup.ts`
11// runs in a vanilla V8 isolate with no C++ extension module support). A static import would fail
12// esbuild's resolver; a dynamic import fails at runtime because workerd's dynamic module
13// resolver doesn't surface INTERNAL C++-backed modules. The callback indirection keeps
14// `util.ts` free of any reference to the C++ module while still letting the main context wire
15// in the real reporter.
16//
17// TODO: If we ever remove the Python pool, `util.ts` will no longer be pool-bundled and we can
18// drop this callback in favor of a direct static import of FatalReporter.
19let _reportInternalError: (() => void) | null = null;
20 
21export function setInternalErrorReporter(cb: () => void): void {
22 _reportInternalError = cb;
23}
24 
25/**
26 * This is an exception we should be throwing whenever there is something unexpected in our runtime
27 * that is **not** a result of the user doing something wrong, i.e. it's an internal error that is
28 * a result of a bug in our runtime.
29 */
30export class PythonWorkersInternalError extends Error {
31 constructor(message?: string) {
32 super(message);
33 try {
34 _reportInternalError?.();
35 } catch (_) {}
36 }
37 
38 override get name(): string {
39 return this.constructor.name;
40 }
41}
42 
43/**
44 * This is an exception we throw whenever there is an issue with the user's code, i.e. it's a result
45 * of the user doing something wrong.
46 */
47export class PythonUserError extends Error {
48 override get name(): string {
49 return this.constructor.name;
50 }
51}
52 
53// Split the stack into lines and print them individually.
54// We do this because edgeworker's test runner will put a multiline log all on one line. This is
55// very hard to read.
56export function reportError(e: Error): never {
57 e.stack?.split('\n').forEach((s: string) => {
58 console.warn(s);
59 });
60 throw e;
61}
62 
63/**
64 * Simple as possible runPython function which works with no foreign function
65 * interface. We need to use this rather than the normal easier to use
66 * interface because the normal interface doesn't work until after
67 * `API.finalizeBootstrap`, but `API.finalizeBootstrap` makes changes inside
68 * and outside the linear memory which have to stay in sync. It's hard to keep
69 * track of the invariants that `finalizeBootstrap` introduces between JS land
70 * and the linear memory so we do this.
71 *
72 * We wrap API.rawRun which does the following steps:
73 * 1. use textEncoder.encode to convert `code` into UTF8 bytes
74 * 2. malloc space for `code` in the wasm linear memory and copy the encoded
75 * `code` to this pointer
76 * 3. redirect standard error to a temporary buffer
77 * 4. call `PyRun_SimpleString`, which either works and returns 0 or formats a
78 * traceback to stderr and returns -1
79 * https://docs.python.org/3/c-api/veryhigh.html?highlight=simplestring#c.PyRun_SimpleString
80 * 5. frees the `code` pointer
81 * 6. Returns the return value from `PyRun_SimpleString` and whatever
82 * information went to stderr.
83 *
84 * PyRun_SimpleString executes the code at top level in the `__main__` module,
85 * so all variables defined get leaked into the global namespace unless we
86 * clean them up explicitly.
87 */
88export function simpleRunPython(
89 emscriptenModule: Module,
90 code: string
91): string {
92 const [status, cause] = emscriptenModule.API.rawRun(code);
93 // status 0: Ok
94 // status -1: Error
95 if (status === -1) {
96 // PyRun_SimpleString will have written a Python traceback to stderr.
97 console.warn('Command failed:', code);
98 console.warn(cause);
99 throw new PythonWorkersInternalError(
100 'Failed to run Python code:\n' + code + '\n\nError:\n' + cause
101 );
102 }
103 return cause;
104}
105 
106export function invalidateCaches(Module: Module): void {
107 simpleRunPython(
108 Module,
109 `from importlib import invalidate_caches; invalidate_caches(); del invalidate_caches`
110 );
111}
112 
113export function unreachable(obj: never, msg?: string): never {
114 if (msg === undefined) {
115 msg = obj;
116 }
117 throw new PythonWorkersInternalError(`Unreachable: ${msg}`);
118}
119 
120/**
121 * Loads a Python source file (bundled as a Uint8Array) into a fresh anonymous
122 * module and returns it. This is used for internal Python helpers that need to
123 * be invoked from JS but should not pollute the global namespace.
124 *
125 * `moduleName` is the bare name of the module (e.g. "introspection"); typically
126 * the source is the default export from `pyodide-internal:<moduleName>.py`.
127 */
128export function loadPythonMod(
129 pyodide: Pyodide,
130 moduleName: string,
131 source: Uint8Array
132): { __dict__: PyDict } {
133 const mod = pyodide.runPython(
134 `from types import ModuleType; ModuleType('${moduleName}')`
135 ) as { __dict__: PyDict };
136 const decoder = new TextDecoder();
137 pyodide.runPython(decoder.decode(source), {
138 globals: mod.__dict__,
139 filename: `${moduleName}.py`,
140 });
141 return mod;
142}