Skip to content

Commit e46f4d8

Browse files
authored
Merge pull request #5713 from cloudflare/python-compat-flag
Python: Allow importing python compat flag from python source code
2 parents 1a4a798 + aa88098 commit e46f4d8

File tree

7 files changed

+77
-35
lines changed

7 files changed

+77
-35
lines changed

src/pyodide/internal/metadata.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@ export const TRANSITIVE_REQUIREMENTS =
4747
export const MAIN_MODULE_NAME = MetadataReader.getMainModule();
4848

4949
export type CompatibilityFlags = MetadataReader.CompatibilityFlags;
50-
export const COMPATIBILITY_FLAGS: MetadataReader.CompatibilityFlags =
51-
MetadataReader.getCompatibilityFlags();
50+
export const COMPATIBILITY_FLAGS: MetadataReader.CompatibilityFlags = {
51+
// Compat flags returned from getCompatibilityFlags is immutable,
52+
// but in Pyodide 0.26, we modify the JS object that is exposed to the Python through
53+
// registerJsModule so we create a new object here by copying the values.
54+
...MetadataReader.getCompatibilityFlags(),
55+
};
5256
export const WORKFLOWS_ENABLED: boolean =
5357
!!COMPATIBILITY_FLAGS.python_workflows;
5458
const NO_GLOBAL_HANDLERS: boolean =

src/pyodide/internal/python.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
maybeRestoreSnapshot,
99
finalizeBootstrap,
1010
isRestoringSnapshot,
11+
type CustomSerializedObjects,
1112
} from 'pyodide-internal:snapshot';
1213
import {
1314
entropyMountFiles,
@@ -20,7 +21,6 @@ import {
2021
LEGACY_VENDOR_PATH,
2122
setCpuLimitNearlyExceededCallback,
2223
} from 'pyodide-internal:metadata';
23-
import type { PyodideEntrypointHelper } from 'pyodide:python-entrypoint-helper';
2424

2525
/**
2626
* SetupEmscripten is an internal module defined in setup-emscripten.h the module instantiates
@@ -47,7 +47,7 @@ import { TRANSITIVE_REQUIREMENTS } from 'pyodide-internal:metadata';
4747
*/
4848
function prepareWasmLinearMemory(
4949
Module: Module,
50-
pyodide_entrypoint_helper: PyodideEntrypointHelper
50+
customSerializedObjects: CustomSerializedObjects
5151
): void {
5252
maybeRestoreSnapshot(Module);
5353
// entropyAfterRuntimeInit adjusts JS state ==> always needs to be called.
@@ -61,7 +61,7 @@ function prepareWasmLinearMemory(
6161
adjustSysPath(Module);
6262
}
6363
if (Module.API.version !== '0.26.0a2') {
64-
finalizeBootstrap(Module, pyodide_entrypoint_helper);
64+
finalizeBootstrap(Module, customSerializedObjects);
6565
}
6666
}
6767

@@ -210,7 +210,7 @@ export function loadPyodide(
210210
isWorkerd: boolean,
211211
lockfile: PackageLock,
212212
indexURL: string,
213-
pyodide_entrypoint_helper: PyodideEntrypointHelper
213+
customSerializedObjects: CustomSerializedObjects
214214
): Pyodide {
215215
try {
216216
const Module = enterJaegerSpan('instantiate_emscripten', () =>
@@ -238,18 +238,18 @@ export function loadPyodide(
238238
});
239239

240240
enterJaegerSpan('prepare_wasm_linear_memory', () => {
241-
prepareWasmLinearMemory(Module, pyodide_entrypoint_helper);
241+
prepareWasmLinearMemory(Module, customSerializedObjects);
242242
});
243243

244-
maybeCollectSnapshot(Module, pyodide_entrypoint_helper);
244+
maybeCollectSnapshot(Module, customSerializedObjects);
245245
// Mount worker files after doing snapshot upload so we ensure that data from the files is never
246246
// present in snapshot memory.
247247
mountWorkerFiles(Module);
248248

249249
if (Module.API.version === '0.26.0a2') {
250250
// Finish setting up Pyodide's ffi so we can use the nice Python interface
251251
// In newer versions we already did this in prepareWasmLinearMemory.
252-
finalizeBootstrap(Module, pyodide_entrypoint_helper);
252+
finalizeBootstrap(Module, customSerializedObjects);
253253
}
254254
const pyodide = Module.API.public_api;
255255

src/pyodide/internal/snapshot.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -582,15 +582,25 @@ async function importJsModulesFromSnapshot(
582582

583583
type CustomSerialized =
584584
| { pyodide_entrypoint_helper: true }
585+
| { cloudflare_compat_flags: true }
585586
| SerializedJsModule;
587+
/**
588+
* Global objects that need a custom serializer
589+
*/
590+
export type CustomSerializedObjects = {
591+
pyodide_entrypoint_helper: PyodideEntrypointHelper;
592+
cloudflare_compat_flags: CompatibilityFlags;
593+
};
586594

587595
function getHiwireSerializer(
588-
pyodide_entrypoint_helper: PyodideEntrypointHelper,
596+
globalObj: CustomSerializedObjects,
589597
modules: Set<string>
590598
): (obj: any) => CustomSerialized {
591599
return function serializer(obj: any): CustomSerialized {
592-
if (obj === pyodide_entrypoint_helper) {
600+
if (obj === globalObj.pyodide_entrypoint_helper) {
593601
return { pyodide_entrypoint_helper: true };
602+
} else if (obj === globalObj.cloudflare_compat_flags) {
603+
return { cloudflare_compat_flags: true };
594604
}
595605
const serializedModule = maybeSerializeJsModule(obj, modules);
596606
if (serializedModule) {
@@ -601,11 +611,13 @@ function getHiwireSerializer(
601611
}
602612

603613
function getHiwireDeserializer(
604-
pyodide_entrypoint_helper: PyodideEntrypointHelper
614+
globalObj: CustomSerializedObjects
605615
): (obj: CustomSerialized) => any {
606616
return function deserializer(obj) {
607617
if ('pyodide_entrypoint_helper' in obj) {
608-
return pyodide_entrypoint_helper;
618+
return globalObj.pyodide_entrypoint_helper;
619+
} else if ('cloudflare_compat_flags' in obj) {
620+
return globalObj.cloudflare_compat_flags;
609621
}
610622
if ('jsModule' in obj) {
611623
return deserializeJsModule(obj, JS_MODULES);
@@ -622,15 +634,15 @@ function getHiwireDeserializer(
622634
function makeLinearMemorySnapshot(
623635
Module: Module,
624636
importedModulesList: string[],
625-
pyodide_entrypoint_helper: PyodideEntrypointHelper,
637+
customSerializedObjects: CustomSerializedObjects,
626638
snapshotType: ArtifactBundler.SnapshotType
627639
): Uint8Array {
628640
const dsoHandles = recordDsoHandles(Module);
629641
let hiwire: SnapshotConfig | undefined;
630642
const jsModuleNames: Set<string> = new Set();
631643
if (Module.API.version !== '0.26.0a2') {
632644
hiwire = Module.API.serializeHiwireState(
633-
getHiwireSerializer(pyodide_entrypoint_helper, jsModuleNames)
645+
getHiwireSerializer(customSerializedObjects, jsModuleNames)
634646
);
635647
}
636648
const settings: SnapshotSettings = {
@@ -807,7 +819,7 @@ export function maybeRestoreSnapshot(Module: Module): void {
807819
function collectSnapshot(
808820
Module: Module,
809821
importedModulesList: string[],
810-
pyodide_entrypoint_helper: PyodideEntrypointHelper,
822+
customSerializedObjects: CustomSerializedObjects,
811823
snapshotType: ArtifactBundler.SnapshotType
812824
): void {
813825
if (!IS_EW_VALIDATING && !SHOULD_SNAPSHOT_TO_DISK) {
@@ -818,7 +830,7 @@ function collectSnapshot(
818830
const snapshot = makeLinearMemorySnapshot(
819831
Module,
820832
importedModulesList,
821-
pyodide_entrypoint_helper,
833+
customSerializedObjects,
822834
snapshotType
823835
);
824836
entropyAfterSnapshot(Module);
@@ -841,7 +853,7 @@ function collectSnapshot(
841853
*/
842854
export function maybeCollectDedicatedSnapshot(
843855
Module: Module,
844-
pyodide_entrypoint_helper: PyodideEntrypointHelper | null
856+
customSerializedObjects: CustomSerializedObjects | null
845857
): void {
846858
if (!IS_CREATING_SNAPSHOT) {
847859
return;
@@ -859,12 +871,12 @@ export function maybeCollectDedicatedSnapshot(
859871
);
860872
}
861873

862-
if (!pyodide_entrypoint_helper) {
874+
if (!customSerializedObjects) {
863875
throw new PythonWorkersInternalError(
864-
'pyodide_entrypoint_helper is required for dedicated snapshot'
876+
'customSerializedObjects is required for dedicated snapshot'
865877
);
866878
}
867-
collectSnapshot(Module, [], pyodide_entrypoint_helper, 'dedicated');
879+
collectSnapshot(Module, [], customSerializedObjects, 'dedicated');
868880
}
869881

870882
/**
@@ -875,7 +887,7 @@ export function maybeCollectDedicatedSnapshot(
875887
*/
876888
export function maybeCollectSnapshot(
877889
Module: Module,
878-
pyodide_entrypoint_helper: PyodideEntrypointHelper
890+
customSerializedObjects: CustomSerializedObjects
879891
): void {
880892
// In order to surface any problems that occur in `memorySnapshotDoImports` to
881893
// users in local development, always call it even if we aren't actually
@@ -893,21 +905,21 @@ export function maybeCollectSnapshot(
893905
collectSnapshot(
894906
Module,
895907
importedModulesList,
896-
pyodide_entrypoint_helper,
908+
customSerializedObjects,
897909
IS_CREATING_BASELINE_SNAPSHOT ? 'baseline' : 'package'
898910
);
899911
}
900912

901913
export function finalizeBootstrap(
902914
Module: Module,
903-
pyodide_entrypoint_helper: PyodideEntrypointHelper
915+
customSerializedObjects: CustomSerializedObjects
904916
): void {
905917
Module.API.config._makeSnapshot =
906918
IS_CREATING_SNAPSHOT && Module.API.version !== '0.26.0a2';
907919
enterJaegerSpan('finalize_bootstrap', () => {
908920
Module.API.finalizeBootstrap(
909921
LOADED_SNAPSHOT_META?.hiwire,
910-
getHiwireDeserializer(pyodide_entrypoint_helper)
922+
getHiwireDeserializer(customSerializedObjects)
911923
);
912924
});
913925
// finalizeBootstrap overrides LD_LIBRARY_PATH. Restore it.

src/pyodide/python-entrypoint-helper.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
WORKFLOWS_ENABLED,
2020
LEGACY_GLOBAL_HANDLERS,
2121
LEGACY_INCLUDE_SDK,
22+
COMPATIBILITY_FLAGS,
2223
} from 'pyodide-internal:metadata';
2324
import { default as Limiter } from 'pyodide-internal:limiter';
2425
import {
@@ -145,12 +146,10 @@ async function getPyodide(): Promise<Pyodide> {
145146
return pyodidePromise;
146147
}
147148
pyodidePromise = (async function (): Promise<Pyodide> {
148-
const pyodide = loadPyodide(
149-
IS_WORKERD,
150-
LOCKFILE,
151-
WORKERD_INDEX_URL,
152-
get_pyodide_entrypoint_helper()
153-
);
149+
const pyodide = loadPyodide(IS_WORKERD, LOCKFILE, WORKERD_INDEX_URL, {
150+
pyodide_entrypoint_helper: get_pyodide_entrypoint_helper(),
151+
cloudflare_compat_flags: COMPATIBILITY_FLAGS,
152+
});
154153
await setupPatches(pyodide);
155154
return pyodide;
156155
})();
@@ -238,6 +237,8 @@ async function setupPatches(pyodide: Pyodide): Promise<void> {
238237
get_pyodide_entrypoint_helper()
239238
);
240239

240+
pyodide.registerJsModule('_cloudflare_compat_flags', COMPATIBILITY_FLAGS);
241+
241242
// Inject modules that enable JS features to be used idiomatically from Python.
242243
if (LEGACY_INCLUDE_SDK) {
243244
await injectWorkersApi(pyodide);
@@ -610,10 +611,11 @@ export async function initPython(): Promise<PythonInitResult> {
610611

611612
// Collect a dedicated snapshot at the very end.
612613
const pyodide = await getPyodide();
613-
maybeCollectDedicatedSnapshot(
614-
pyodide._module,
615-
get_pyodide_entrypoint_helper()
616-
);
614+
const customSerializedObjects = {
615+
pyodide_entrypoint_helper: get_pyodide_entrypoint_helper(),
616+
cloudflare_compat_flags: COMPATIBILITY_FLAGS,
617+
};
618+
maybeCollectDedicatedSnapshot(pyodide._module, customSerializedObjects);
617619

618620
return { handlers, pythonEntrypointClasses, makeEntrypointClass };
619621
}

src/workerd/server/tests/python/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,5 @@ py_wd_test(
7575
make_snapshot = False,
7676
use_snapshot = "numpy",
7777
)
78+
79+
py_wd_test("python-compat-flag")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Workerd = import "/workerd/workerd.capnp";
2+
3+
const unitTests :Workerd.Config = (
4+
services = [
5+
( name = "python-compat-flag",
6+
worker = (
7+
modules = [
8+
(name = "worker.py", pythonModule = embed "worker.py"),
9+
],
10+
compatibilityFlags = [%PYTHON_FEATURE_FLAGS, "python_workflows"],
11+
)
12+
),
13+
],
14+
);
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import _cloudflare_compat_flags
2+
from workers import WorkerEntrypoint
3+
4+
5+
class Default(WorkerEntrypoint):
6+
def test(self):
7+
assert _cloudflare_compat_flags.python_workflows
8+
assert not _cloudflare_compat_flags.python_no_global_handlers

0 commit comments

Comments
 (0)