Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: Pass the env parameter into handlers #1610

Merged
merged 15 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
50 changes: 38 additions & 12 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,37 +199,54 @@ 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.includes("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;
hoodmane marked this conversation as resolved.
Show resolved Hide resolved

return pyodide.pyimport(mainModule);
}

export default {
async fetch(request, env) {
let mainModulePromise;
function getMainModule() {
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();
const mainModule = await setupPackages(pyodide);
return await mainModule.fetch(request);
const relaxed_call = pyodide.pyimport("relaxed_call").relaxed_call;
result = { mainModule, relaxed_call };
return result;
})();
return mainModulePromise;
}

export default {
async fetch(request, env, ctx) {
const { relaxed_call, mainModule } = await getMainModule();
hoodmane marked this conversation as resolved.
Show resolved Hide resolved
return await relaxed_call(mainModule.fetch, request, env, ctx);
},
async test() {
async test(ctrl, env, ctx) {
try {
const pyodide = await loadPyodide();
const mainModule = await setupPackages(pyodide);
return await mainModule.test();
const { relaxed_call, mainModule } = await getMainModule();
return await relaxed_call(mainModule.test, ctrl, env, ctx);
} catch (e) {
console.warn(e);
}
Expand Down
Loading