Skip to content

Commit

Permalink
Python: Pass the env parameter into handlers
Browse files Browse the repository at this point in the history
This adds a relaxed_call function taken from
pyodide/pyodide#4392
and uses it to pass the env argument to the fetch/test handlers
  • Loading branch information
hoodmane committed Feb 2, 2024
1 parent 267d310 commit b3cf71a
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 16 deletions.
37 changes: 37 additions & 0 deletions samples/pyodide-secret/config.capnp
Original file line number Diff line number Diff line change
@@ -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/
);
11 changes: 11 additions & 0 deletions samples/pyodide-secret/worker.py
Original file line number Diff line number Diff line change
@@ -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")
88 changes: 88 additions & 0 deletions src/pyodide/internal/relaxed_call.py
Original file line number Diff line number Diff line change
@@ -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)
45 changes: 29 additions & 16 deletions src/pyodide/python-entrypoint-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { default as origMetadata } from "pyodide-internal:runtime-generated/curr
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;
Expand Down Expand Up @@ -137,6 +137,15 @@ async function setupPackages(pyodide) {

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 },
);
}

// Loop through globals that define Python modules in the metadata passed to our Worker. For
// each one, save it in Pyodide's file system.
const requirements = [];
Expand Down Expand Up @@ -190,47 +199,51 @@ 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 },
);
}

// The main module can have a `.py` extension, strip it if it exists.
const mainName = metadata.mainModule;
const mainModule = mainName.endsWith(".py") ? mainName.slice(0, -3) : mainName;
const mainModule = mainName.endsWith(".py")
? mainName.slice(0, -3)
: mainName;

return pyodide.pyimport(mainModule);
}

let mainModule;
let result;
async function getMainModule() {
if (mainModule) {
return mainModule;
if (result) {
return result;
}
// 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();
mainModule = await setupPackages(pyodide);
return mainModule;
const mainModule = await setupPackages(pyodide);
const relaxed_call = pyodide.pyimport("relaxed_call").relaxed_call;
result = { mainModule, relaxed_call };
return result;
}

export default {
async fetch(request, env) {
const mainModule = await getMainModule();
return await mainModule.fetch(request);
async fetch(ctx, env) {
const { relaxed_call, mainModule } = await getMainModule();
return await relaxed_call(mainModule.fetch, ctx, env);
},
async test() {
async test(ctx, env) {
try {
const mainModule = await getMainModule();
return await mainModule.test();
const { relaxed_call, mainModule } = await getMainModule();
return await relaxed_call(mainModule.test, ctx, env);
} catch (e) {
console.warn(e);
}
Expand Down

0 comments on commit b3cf71a

Please sign in to comment.