Skip to content

Commit cb2aaf8

Browse files
committed
Add patch_env to Python workers sdk
A context manager, to be used like: ``` with patch_env(key1=value1, key2=value2): # env is patched # env is restored ```
1 parent 0771b31 commit cb2aaf8

File tree

14 files changed

+164
-4
lines changed

14 files changed

+164
-4
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ coverage
4040
perf.data
4141

4242
.claude
43+
44+
# python
45+
__pycache__

src/pyodide/internal/envHelpers.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import innerEnv from 'cloudflare-internal:env';
2+
3+
// A generator that keeps the environment patched until it is resolved.
4+
// Used to define the patch_env context manager, see
5+
// src/pyodide/internal/workers-api/src/workers/_workers.py
6+
export function* patch_env_helper(patch: unknown): Generator<void> {
7+
using _ = innerEnv.pythonPatchEnv(patch);
8+
yield;
9+
}

src/pyodide/internal/util.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ export function invalidateCaches(Module: Module): void {
7979
);
8080
}
8181

82-
export function unreachable(msg: never): never {
82+
export function unreachable(
83+
obj: never,
84+
msg: string | undefined = undefined
85+
): never {
86+
if (msg === undefined) {
87+
msg = obj;
88+
}
8389
throw new PythonWorkersInternalError(`Unreachable: ${msg}`);
8490
}

src/pyodide/internal/workers-api/src/workers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
fetch,
2121
handler,
2222
import_from_javascript,
23+
patch_env,
2324
python_from_rpc,
2425
python_to_rpc,
2526
)
@@ -47,6 +48,7 @@
4748
"fetch",
4849
"handler",
4950
"import_from_javascript",
51+
"patch_env",
5052
"python_from_rpc",
5153
"python_to_rpc",
5254
]

src/pyodide/internal/workers-api/src/workers/_workers.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@
66
import inspect
77
import json
88
from asyncio import create_task, gather
9-
from collections.abc import Awaitable, Generator, Iterable, MutableMapping
9+
from collections.abc import (
10+
Awaitable,
11+
Generator,
12+
Iterable,
13+
Iterator,
14+
MutableMapping,
15+
Sequence,
16+
)
1017
from contextlib import ExitStack, contextmanager
1118
from enum import StrEnum
1219
from http import HTTPMethod, HTTPStatus
@@ -96,6 +103,15 @@ def import_from_javascript(module_name: str) -> Any:
96103
raise
97104

98105

106+
@contextmanager
107+
def patch_env(
108+
d: dict[str, Any] | Sequence[tuple[str, Any]] | None = None, **kwds: dict[str, Any]
109+
) -> Iterator[None]:
110+
if d:
111+
kwds = dict(d) | kwds
112+
yield from _pyodide_entrypoint_helper.patch_env_helper(to_js(kwds))
113+
114+
99115
type JSBody = (
100116
"js.Blob | JsBuffer | js.FormData | js.ReadableStream | js.URLSearchParams"
101117
)

src/pyodide/python-entrypoint-helper.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
reportError,
2828
} from 'pyodide-internal:util';
2929
import { LOADED_SNAPSHOT_TYPE } from 'pyodide-internal:snapshot';
30+
import { patch_env_helper } from 'pyodide-internal:envHelpers';
3031

3132
type PyFuture<T> = Promise<T> & { copy(): PyFuture<T>; destroy(): void };
3233

@@ -65,10 +66,11 @@ function patchWaitUntil(ctx: {
6566

6667
export type PyodideEntrypointHelper = {
6768
doAnImport: (mod: string) => Promise<any>;
68-
cloudflareWorkersModule: any;
69+
cloudflareWorkersModule: { env: any };
6970
cloudflareSocketsModule: any;
7071
workerEntrypoint: any;
7172
patchWaitUntil: typeof patchWaitUntil;
73+
patch_env_helper: (patch: unknown) => Generator<void>;
7274
};
7375
import { maybeCollectDedicatedSnapshot } from 'pyodide-internal:snapshot';
7476

@@ -96,6 +98,7 @@ export function setDoAnImport(
9698
cloudflareSocketsModule,
9799
workerEntrypoint,
98100
patchWaitUntil,
101+
patch_env_helper,
99102
};
100103
}
101104

src/pyodide/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"pyodide-internal:generated/emscriptenSetup": [
1111
"./internal/pool/emscriptenSetup.ts"
1212
],
13-
"internal:*": ["./types/*"]
13+
"internal:*": ["./types/*"],
14+
"cloudflare-internal:*": ["./types/cloudflare-internal/*"]
1415
},
1516
"typeRoots": ["./types"],
1617
"noEmit": false,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const innerEnv: {
2+
pythonPatchEnv(patch: unknown): {
3+
[Symbol.dispose](): void;
4+
};
5+
};
6+
export default innerEnv;

src/workerd/api/modules.c++

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ jsg::JsRef<jsg::JsValue> EnvModule::withEnv(
3737
});
3838
}
3939

40+
jsg::Ref<PythonPatchedEnv> EnvModule::pythonPatchEnv(jsg::Lock& js, jsg::Value newEnv) {
41+
auto& key = jsg::IsolateBase::from(js.v8Isolate).getEnvAsyncContextKey();
42+
return jsg::alloc<PythonPatchedEnv>(js, key, kj::mv(newEnv));
43+
}
44+
4045
kj::Maybe<jsg::JsObject> EnvModule::getCurrentExports(jsg::Lock& js) {
4146
auto& key = jsg::IsolateBase::from(js.v8Isolate).getExportsAsyncContextKey();
4247
// Check async context first - withExports() overrides take precedence over the disable flags.

src/workerd/api/modules.h

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,27 @@
1919

2020
namespace workerd::api {
2121

22+
// An object with a [Symbol.dispose]() method to remove patch to environment. Not exposed
23+
// publically, just used to implement Python's `patch_env()` context manager.
24+
// See src/pyodide/internal/envHelpers.ts
25+
class PythonPatchedEnv: public jsg::Object {
26+
public:
27+
PythonPatchedEnv(jsg::Lock& js, jsg::AsyncContextFrame::StorageKey& key, jsg::Value store) {
28+
scope.emplace(js, key, kj::mv(store));
29+
}
30+
31+
void dispose() {
32+
scope = kj::none;
33+
}
34+
35+
JSG_RESOURCE_TYPE(PythonPatchedEnv) {
36+
JSG_DISPOSE(dispose);
37+
}
38+
39+
private:
40+
kj::Maybe<jsg::AsyncContextFrame::StorageScope> scope;
41+
};
42+
2243
class EnvModule final: public jsg::Object {
2344
public:
2445
EnvModule() = default;
@@ -39,12 +60,18 @@ class EnvModule final: public jsg::Object {
3960
jsg::Value newExports,
4061
jsg::Function<jsg::JsRef<jsg::JsValue>()> fn);
4162

63+
// Patch environment and return an object with a [Symbol.dispose]() method to restore it.
64+
// Not exposed publically, just used to implement Python's `patch_env()` context manager.
65+
// See src/pyodide/internal/envHelpers.ts
66+
jsg::Ref<PythonPatchedEnv> pythonPatchEnv(jsg::Lock& js, jsg::Value newEnv);
67+
4268
JSG_RESOURCE_TYPE(EnvModule) {
4369
JSG_METHOD(getCurrentEnv);
4470
JSG_METHOD(getCurrentExports);
4571
JSG_METHOD(withEnv);
4672
JSG_METHOD(withExports);
4773
JSG_METHOD(withEnvAndExports);
74+
JSG_METHOD(pythonPatchEnv);
4875
}
4976
};
5077

0 commit comments

Comments
 (0)