Skip to content

Commit

Permalink
Python: Pass the env parameter into handlers (#1610)
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 authored Feb 6, 2024
1 parent c437cf0 commit 6b63c70
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 12 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)
35 changes: 23 additions & 12 deletions src/pyodide/python-entrypoint-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));

Expand All @@ -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();
Expand All @@ -142,28 +151,30 @@ async function setupPackages(pyodide) {
}

let mainModulePromise;
function getMainModule() {
function getPyodide() {
if (mainModulePromise !== undefined) {
return mainModulePromise;
}
mainModulePromise = (async function() {
// 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);
}
Expand Down

0 comments on commit 6b63c70

Please sign in to comment.