diff --git a/samples/pyodide-secret/config.capnp b/samples/pyodide-secret/config.capnp new file mode 100644 index 00000000000..db00c1567e8 --- /dev/null +++ b/samples/pyodide-secret/config.capnp @@ -0,0 +1,37 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const config :Workerd.Config = ( + services = [ + (name = "main", worker = .mainWorker), + ], + + sockets = [ + # Serve HTTP on port 8080. + ( name = "http", + address = "*:8080", + http = (), + service = "main" + ), + ], + autogates = [ + # Pyodide is included as a builtin wasm module so it requires the + # corresponding autogate flag. + "workerd-autogate-builtin-wasm-modules", + ] +); + +const mainWorker :Workerd.Worker = ( + modules = [ + (name = "worker.py", pythonModule = embed "./worker.py"), + ], + compatibilityDate = "2023-12-18", + compatibilityFlags = ["experimental"], + bindings = [ + ( + name = "secret", + text = "thisisasecret" + ), + ], + # Learn more about compatibility dates at: + # https://developers.cloudflare.com/workers/platform/compatibility-dates/ +); diff --git a/samples/pyodide-secret/worker.py b/samples/pyodide-secret/worker.py new file mode 100644 index 00000000000..ebc705bfc7e --- /dev/null +++ b/samples/pyodide-secret/worker.py @@ -0,0 +1,11 @@ +from js import Response + + +def fetch(request, env): + print(env.secret) + return Response.new("hello world") + + +def test(ctx, env): + print(env.secret) + print("Hi there, this is a test") diff --git a/src/pyodide/internal/relaxed_call.py b/src/pyodide/internal/relaxed_call.py new file mode 100644 index 00000000000..7a0e673d83b --- /dev/null +++ b/src/pyodide/internal/relaxed_call.py @@ -0,0 +1,88 @@ +""" +Allow calling functions with more arguments than they want + +Copied from +https://github.com/pyodide/pyodide/pull/4392/ +""" + +from collections.abc import Callable +from functools import lru_cache, wraps +from inspect import Parameter, Signature, signature +from typing import Any, ParamSpec, TypeVar + + +def _relaxed_call_sig(func: Callable[..., Any]) -> Signature | None: + try: + sig = signature(func) + except (TypeError, ValueError): + return None + new_params = list(sig.parameters.values()) + idx: int | None = -1 + for idx, param in enumerate(new_params): + if param.kind in (Parameter.KEYWORD_ONLY, Parameter.VAR_KEYWORD): + break + if param.kind == Parameter.VAR_POSITIONAL: + idx = None + break + else: + idx += 1 + if idx is not None: + new_params.insert(idx, Parameter("__var_positional", Parameter.VAR_POSITIONAL)) + + for param in new_params: + if param.kind == Parameter.VAR_KEYWORD: + break + else: + new_params.append(Parameter("__var_keyword", Parameter.VAR_KEYWORD)) + new_sig = sig.replace(parameters=new_params) + return new_sig + + +@lru_cache +def _relaxed_call_sig_cached(func: Callable[..., Any]) -> Signature | None: + return _relaxed_call_sig(func) + + +def _do_call( + func: Callable[..., Any], sig: Signature, args: Any, kwargs: dict[str, Any] +) -> Any: + bound = sig.bind(*args, **kwargs) + bound.arguments.pop("__var_positional", None) + bound.arguments.pop("__var_keyword", None) + return func(*bound.args, **bound.kwargs) + + +Param = ParamSpec("Param") +Param2 = ParamSpec("Param2") +RetType = TypeVar("RetType") + + +def relaxed_wrap(func: Callable[Param, RetType]) -> Callable[..., RetType]: + """Decorator which creates a function that ignores extra arguments + + If extra positional or keyword arguments are provided they will be + discarded. + """ + sig = _relaxed_call_sig(func) + if sig is None: + raise TypeError("Cannot wrap function") + else: + sig2 = sig + + @wraps(func) + def wrapper(*args: Param.args, **kwargs: Param.kwargs) -> RetType: + return _do_call(func, sig2, args, kwargs) + + return wrapper + + +def relaxed_call(func: Callable[..., RetType], *args: Any, **kwargs: Any) -> RetType: + """Call the function ignoring extra arguments + + If extra positional or keyword arguments are provided they will be + discarded. + """ + sig = _relaxed_call_sig_cached(func) + if sig is None: + return func(*args, **kwargs) + return _do_call(func, sig, args, kwargs) diff --git a/src/pyodide/python-entrypoint-helper.js b/src/pyodide/python-entrypoint-helper.js index 4fd996f5393..1f3ff5d525a 100644 --- a/src/pyodide/python-entrypoint-helper.js +++ b/src/pyodide/python-entrypoint-helper.js @@ -9,7 +9,7 @@ import { default as MetadataReader } from "pyodide-internal:runtime-generated/me function initializePackageIndex(pyodide) { if (!lockfile.packages) { throw new Error( - "Loaded pyodide lock file does not contain the expected key 'packages'." + "Loaded pyodide lock file does not contain the expected key 'packages'.", ); } const API = pyodide._api; @@ -101,6 +101,15 @@ async function setupPackages(pyodide) { const isWorkerd = MetadataReader.isWorkerd(); initializePackageIndex(pyodide); + { + const mod = await import("pyodide-internal:relaxed_call.py"); + pyodide.FS.writeFile( + "/lib/python3.11/site-packages/relaxed_call.py", + new Uint8Array(mod.default), + { canOwn: true }, + ); + } + const requirements = MetadataReader.getRequirements(); const pythonRequirements = isWorkerd ? requirements : requirements.filter(req => !EMBEDDED_PYTHON_PACKAGES.has(req)); @@ -122,16 +131,16 @@ async function setupPackages(pyodide) { pyodide.FS.writeFile( "/lib/python3.11/site-packages/aiohttp_fetch_patch.py", new Uint8Array(mod.default), - { canOwn: true } + { canOwn: true }, ); pyodide.pyimport("aiohttp_fetch_patch"); } - if (requirements.some(req => req.startsWith("fastapi"))) { + if (requirements.some((req) => req.startsWith("fastapi"))) { const mod = await import("pyodide-internal:asgi.py"); pyodide.FS.writeFile( "/lib/python3.11/site-packages/asgi.py", new Uint8Array(mod.default), - { canOwn: true } + { canOwn: true }, ); } let mainModuleName = MetadataReader.getMainModule(); @@ -142,7 +151,7 @@ async function setupPackages(pyodide) { } let mainModulePromise; -function getMainModule() { +function getPyodide() { if (mainModulePromise !== undefined) { return mainModulePromise; } @@ -150,20 +159,22 @@ function getMainModule() { // TODO: investigate whether it is possible to run part of loadPyodide in top level scope // When we do it in top level scope we seem to get a broken file system. const pyodide = await loadPyodide(); - return await setupPackages(pyodide); + const mainModule = await setupPackages(pyodide); + const relaxed_call = pyodide.pyimport("relaxed_call").relaxed_call; + return { mainModule, relaxed_call }; })(); return mainModulePromise; } export default { - async fetch(request, env) { - const mainModule = await getMainModule(); - return await mainModule.fetch(request); + async fetch(request, env, ctx) { + const { relaxed_call, mainModule } = await getPyodide(); + return await relaxed_call(mainModule.fetch, request, env, ctx); }, - async test() { + async test(ctrl, env, ctx) { try { - const mainModule = await getMainModule(); - return await mainModule.test(); + const { relaxed_call, mainModule } = await getPyodide(); + return await relaxed_call(mainModule.test, ctrl, env, ctx); } catch (e) { console.warn(e); }