Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ world, you can generate them using the `bindings` subcommand:
componentize-py -d hello.wit -w hello bindings hello_guest
```

Then, use the `hello` module produced by the command above to write your app:
Then, use the bindings produced by the command above (a `wit` package inside
`hello_guest`) to write your app:

```shell
cat >app.py <<EOF
import wit_world
class WitWorld(wit_world.WitWorld):
import wit

@wit.guest
class Hello(wit.WorldExports):
def hello(self) -> str:
return "Hello, World!"
EOF
Expand Down
40 changes: 40 additions & 0 deletions bundled/componentize_py_exports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Registry of the implementations an app provides for its WIT exports.

The bindings generated by `componentize-py` include a decorator for each
exported interface, resource, and world, and applying one of those decorators
registers the decorated class here. `componentize-py` reads this registry when
pre-initializing the component in order to connect each WIT export to the code
implementing it.

Application code should not use this module directly; use the generated
decorators instead, e.g.::

from wit.exports.wasi.http_v0_2 import incoming_handler

@incoming_handler.guest
class MyHandler:
...
"""

from typing import Any, Callable, Dict, TypeVar

T = TypeVar("T")

# Export key (see `scope` in `init.wit`) -> implementing class.
EXPORTS: Dict[str, Any] = {}


def register(key: str) -> Callable[[T], T]:
"""Return a decorator which registers the class it's applied to as the
implementation of the WIT export named by `key`."""

def decorate(value: T) -> T:
if key in EXPORTS:
raise RuntimeError(
f"multiple implementations registered for `{key}`; "
f"exactly one is expected"
)
EXPORTS[key] = value
return value

return decorate
13 changes: 9 additions & 4 deletions bundled/poll_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@
import subprocess

from componentize_py_types import Ok, Err
from wit_world.imports import types, streams, poll, outgoing_handler
from wit_world.imports.types import (

# Placeholders replaced at build time; see `HELPER_INTERFACES` in summary.rs.
import WASI_HTTP_TYPES_MODULE as types
import WASI_HTTP_OUTGOING_HANDLER_MODULE as outgoing_handler
import WASI_IO_STREAMS_MODULE as streams
import WASI_IO_POLL_MODULE as poll
from WASI_HTTP_TYPES_MODULE import (
IncomingBody,
OutgoingBody,
OutgoingRequest,
IncomingResponse,
)
from wit_world.imports.streams import StreamError_Closed, InputStream
from wit_world.imports.poll import Pollable
from WASI_IO_STREAMS_MODULE import StreamError_Closed, InputStream
from WASI_IO_POLL_MODULE import Pollable
from typing import Optional, cast

# Maximum number of bytes to read at a time
Expand Down
5 changes: 3 additions & 2 deletions examples/cli-p3/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from wit_world import exports
from wit.exports.wasi.cli_v0_3 import run, Run

class Run(exports.Run):
@run.guest
class Cli(Run):
async def run(self) -> None:
print("Hello, world!")
5 changes: 3 additions & 2 deletions examples/cli/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from wit_world import exports
from wit.exports.wasi.cli_v0_2 import run, Run

class Run(exports.Run):
@run.guest
class Cli(Run):
def run(self) -> None:
print("Hello, world!")
17 changes: 9 additions & 8 deletions examples/http-p3/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
import asyncio
import hashlib
import componentize_py_async_support
import wit_world
import wit

from typing import Optional
from componentize_py_types import Ok, Result
from componentize_py_async_support.streams import ByteStreamWriter
from componentize_py_async_support.futures import FutureReader
from wit_world import exports
from wit_world.imports import client
from wit_world.imports.wasi_http_types import (
from wit.exports.wasi.http_v0_3 import handler, Handler
from wit.imports.wasi.http_v0_3 import client
from wit.imports.wasi.http_v0_3.types import (
Method_Get,
Method_Post,
Scheme,
Expand All @@ -31,7 +31,8 @@
from urllib import parse


class Handler(exports.Handler):
@handler.guest
class HttpHandler(Handler):
"""Implements the `export`ed portion of the `wasi-http` `proxy` world."""

async def handle(self, request: Request) -> Response:
Expand All @@ -52,7 +53,7 @@ async def handle(self, request: Request) -> Response:
filter(lambda pair: pair[0] == "url", headers),
))

tx, rx = wit_world.byte_stream()
tx, rx = wit.byte_stream()
componentize_py_async_support.spawn(hash_all(urls, tx))

return Response.new(
Expand Down Expand Up @@ -127,8 +128,8 @@ async def sha256(url: str) -> tuple[str, str]:


def trailers_future() -> FutureReader[Result[Optional[Fields], ErrorCode]]:
return wit_world.result_option_wasi_http_types_fields_wasi_http_types_error_code_future(lambda: Ok(None))[1]
return wit.result_option_wasi_http_v0_3_types_fields_wasi_http_v0_3_types_error_code_future(lambda: Ok(None))[1]


def unit_future() -> FutureReader[Result[None, ErrorCode]]:
return wit_world.result_unit_wasi_http_types_error_code_future(lambda: Ok(None))[1]
return wit.result_unit_wasi_http_v0_3_types_error_code_future(lambda: Ok(None))[1]
9 changes: 5 additions & 4 deletions examples/http/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
import poll_loop

from componentize_py_types import Ok
from wit_world import exports
from wit_world.imports import types
from wit_world.imports.types import (
from wit.exports.wasi.http_v0_2 import incoming_handler, IncomingHandler
from wit.imports.wasi.http_v0_2 import types
from wit.imports.wasi.http_v0_2.types import (
Method_Get,
Method_Post,
Scheme,
Expand All @@ -31,7 +31,8 @@
from urllib import parse


class IncomingHandler(exports.IncomingHandler):
@incoming_handler.guest
class Handler(IncomingHandler):
"""Implements the `export`ed portion of the `wasi-http` `proxy` world."""

def handle(self, request: IncomingRequest, response_out: ResponseOutparam) -> None:
Expand Down
12 changes: 7 additions & 5 deletions examples/matrix-math/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,24 @@

import sys
import numpy
import wit_world
from wit_world import exports
import wit
from wit.exports.wasi.cli_v0_2 import run, Run
from componentize_py_types import Err


class WitWorld(wit_world.WitWorld):
@wit.guest
class MatrixMath(wit.WorldExports):
def multiply(self, a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
print(f"matrix_multiply received arguments {a} and {b}")
return numpy.matmul(a, b).tolist() # type: ignore


class Run(exports.Run):
@run.guest
class Cli(Run):
def run(self) -> None:
args = sys.argv[1:]
if len(args) != 2:
print("usage: matrix-math <matrix> <matrix>", file=sys.stderr)
exit(-1)

print(WitWorld().multiply(eval(args[0]), eval(args[1])))
print(MatrixMath().multiply(eval(args[0]), eval(args[1])))
5 changes: 3 additions & 2 deletions examples/sandbox/guest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import wit_world
import wit
from componentize_py_types import Err
import json

Expand All @@ -11,7 +11,8 @@ def handle(e: Exception) -> Err[str]:
return Err(f"{type(e).__name__}: {message}")


class WitWorld(wit_world.WitWorld):
@wit.guest
class Sandbox(wit.WorldExports):
def eval(self, expression: str) -> str:
try:
return json.dumps(eval(expression))
Expand Down
11 changes: 6 additions & 5 deletions examples/tcp-p3/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
import asyncio
import ipaddress
from ipaddress import IPv4Address, IPv6Address
import wit_world
from wit_world import exports
from wit_world.imports.wasi_sockets_types import (
import wit
from wit.exports.wasi.cli_v0_3 import run, Run
from wit.imports.wasi.sockets_v0_3.types import (
TcpSocket,
IpSocketAddress_Ipv4,
IpSocketAddress_Ipv6,
Expand All @@ -17,7 +17,8 @@

IPAddress = IPv4Address | IPv6Address

class Run(exports.Run):
@run.guest
class Tcp(Run):
async def run(self) -> None:
args = sys.argv[1:]
if len(args) != 1:
Expand Down Expand Up @@ -67,7 +68,7 @@ async def send_and_receive(address: IPAddress, port: int) -> None:

await sock.connect(make_socket_address(address, port))

send_tx, send_rx = wit_world.byte_stream()
send_tx, send_rx = wit.byte_stream()
async def write() -> None:
await send_tx.write_all(b"hello, world!")

Expand Down
5 changes: 3 additions & 2 deletions examples/tcp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
import asyncio
import ipaddress
from ipaddress import IPv4Address, IPv6Address
from wit_world import exports
from wit.exports.wasi.cli_v0_2 import run, Run
from typing import Tuple


class Run(exports.Run):
@run.guest
class Tcp(Run):
def run(self) -> None:
args = sys.argv[1:]
if len(args) != 1:
Expand Down
71 changes: 44 additions & 27 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -908,10 +908,30 @@ fn do_init(app_name: String, symbols: Symbols, stub_wasi: bool) -> Result<(), St
Python::initialize();

let init = |py: Python| {
let app = py.import(app_name.as_str())?;
// Importing the app runs its decorators, filling `componentize_py_exports.EXPORTS`.
py.import(app_name.as_str())?;

let registry = py.import("componentize_py_exports")?.getattr("EXPORTS")?;

let implementation = |scope: &str| {
registry.get_item(scope).map_err(|error| {
if error.is_instance_of::<pyo3::exceptions::PyKeyError>(py) {
PyAssertionError::new_err(format!(
"no implementation registered for `{scope}`; please apply the \
corresponding decorator from the generated bindings to the class \
implementing it"
))
} else {
error
}
})
};

STUB_WASI.set(stub_wasi).unwrap();

// One implementation instance per interface, shared by its functions.
let mut instances = std::collections::HashMap::<String, Py<PyAny>>::new();

EXPORTS
.set(
symbols
Expand All @@ -920,35 +940,32 @@ fn do_init(app_name: String, symbols: Symbols, stub_wasi: bool) -> Result<(), St
.map(|export| {
Ok(Export {
kind: match &export.kind {
FunctionExportKind::Freestanding(exp::Function {
protocol,
name,
}) => ExportKind::Freestanding {
name: PyString::intern(py, name).into(),
instance: app.getattr(protocol.as_str())?.call0()?.into(),
},
FunctionExportKind::Constructor(Constructor {
module,
protocol,
}) => ExportKind::Constructor(
py.import(module.as_str())?
.getattr(protocol.as_str())?
.into(),
),
FunctionExportKind::Freestanding(exp::Function { scope, name }) => {
let instance = if let Some(instance) = instances.get(scope) {
instance.clone_ref(py)
} else {
let instance: Py<PyAny> =
implementation(scope)?.call0()?.into();
instances.insert(scope.clone(), instance.clone_ref(py));
instance
};
ExportKind::Freestanding {
name: PyString::intern(py, name).into(),
instance,
}
}
FunctionExportKind::Constructor(Constructor { scope }) => {
ExportKind::Constructor(implementation(scope)?.into())
}
FunctionExportKind::Method(name) => {
ExportKind::Method(PyString::intern(py, name).into())
}
FunctionExportKind::Static(Static {
module,
protocol,
name,
}) => ExportKind::Static {
name: PyString::intern(py, name).into(),
class: py
.import(module.as_str())?
.getattr(protocol.as_str())?
.into(),
},
FunctionExportKind::Static(Static { scope, name }) => {
ExportKind::Static {
name: PyString::intern(py, name).into(),
class: implementation(scope)?.into(),
}
}
},
return_style: export.return_style,
})
Expand Down
Loading
Loading