Skip to content
Merged
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
42 changes: 42 additions & 0 deletions .github/workflows/lint_examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/bash

set -euo pipefail

export COMPONENTIZE_PY_TEST_COUNT=0
export COMPONENTIZE_PY_TEST_SEED=bc6ad1950594f1fe477144ef5b3669dd5962e49de4f3b666e5cbf9072507749a
export WASMTIME_BACKTRACE_DETAILS=1

cargo build --release

# CLI
(cd examples/cli \
&& rm -rf command || true \
&& ../../target/release/componentize-py -d ../../wit -w wasi:cli/[email protected] bindings . \
&& mypy --strict .)

# HTTP
# poll_loop.py has many errors that might not be worth adjusting at the moment, so ignore for now
(cd examples/http \
&& rm -rf proxy || true \
&& ../../target/release/componentize-py -d ../../wit -w wasi:http/[email protected] bindings . \
&& mypy --strict --ignore-missing-imports -m app -p proxy)

# # Matrix Math
(cd examples/matrix-math \
&& rm -rf matrix_math || true \
&& curl -OL https://github.com/dicej/wasi-wheels/releases/download/v0.0.1/numpy-wasi.tar.gz \
&& tar xf numpy-wasi.tar.gz \
&& ../../target/release/componentize-py -d ../../wit -w matrix-math bindings . \
&& mypy --strict --follow-imports silent -m app -p matrix_math)

# Sandbox
(cd examples/sandbox \
&& rm -rf sandbox || true \
&& ../../target/release/componentize-py -d sandbox.wit bindings . \
&& mypy --strict -m guest -p sandbox)

# TCP
(cd examples/tcp \
&& rm -rf command || true \
&& ../../target/release/componentize-py -d ../../wit -w wasi:cli/[email protected] bindings . \
&& mypy --strict .)
14 changes: 14 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,17 @@ jobs:
- name: Test
shell: bash
run: COMPONENTIZE_PY_TEST_COUNT=20 PROPTEST_MAX_SHRINK_ITERS=0 cargo test --release

- uses: taiki-e/install-action@v2
with:
tool: wasmtime-cli
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install wasmtime mypy
- name: Test examples
shell: bash
run: bash .github/workflows/test_examples.sh
- name: Lint examples
shell: bash
run: bash .github/workflows/lint_examples.sh
37 changes: 37 additions & 0 deletions .github/workflows/test_examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

set -euo pipefail

export COMPONENTIZE_PY_TEST_COUNT=0
export COMPONENTIZE_PY_TEST_SEED=bc6ad1950594f1fe477144ef5b3669dd5962e49de4f3b666e5cbf9072507749a
export WASMTIME_BACKTRACE_DETAILS=1

cargo build --release

# CLI
(cd examples/cli \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we could send the output of wasmtime run to a file and then run diff to compare that with another file containing the expected output. Likewise for the matrix-math and sandbox examples.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding this, I wonder if it would be worth doing a Rust crate for this to make it easier to compare the output as well. I don't think I'll get to it today, but I think it could work nicely to solve both problems and shouldn't be too hard to write.

I'm not sure why I didn't default to this in the first place ;)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM. If you don't have time for it now, we could go ahead and merge this as-is and add the Rust crate as a follow-up PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realistically, I might not get to this until the weekend, so I'd say it is up to you. If I'm lucky I'll get it done tomorrow, but we'll see

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go ahead and merge this. No hurry on the Rust crate; this is already a big improvement.

&& ../../target/release/componentize-py -d ../../wit -w wasi:cli/[email protected] componentize app -o cli.wasm \
&& wasmtime run cli.wasm)

# HTTP
# Just compiling for now
(cd examples/http \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could potentially use curl and nc in background jobs to test the HTTP and TCP examples, respectively. Or else add a test crate written in Rust that exercises them. Not necessary for this PR, though -- just something we could follow up with later.

&& ../../target/release/componentize-py -d ../../wit -w wasi:http/[email protected] componentize app -o http.wasm)

# Matrix Math
(cd examples/matrix-math \
&& curl -OL https://github.com/dicej/wasi-wheels/releases/download/v0.0.1/numpy-wasi.tar.gz \
&& tar xf numpy-wasi.tar.gz \
&& ../../target/release/componentize-py -d ../../wit -w matrix-math componentize app -o matrix-math.wasm \
&& wasmtime run matrix-math.wasm '[[1, 2], [4, 5], [6, 7]]' '[[1, 2, 3], [4, 5, 6]]')

# Sandbox
(cd examples/sandbox \
&& ../../target/release/componentize-py -d sandbox.wit componentize --stub-wasi guest -o sandbox.wasm \
&& python -m wasmtime.bindgen sandbox.wasm --out-dir sandbox \
&& python host.py "2 + 2")

# TCP
# Just compiling for now
(cd examples/tcp \
&& ../../target/release/componentize-py -d ../../wit -w wasi:cli/[email protected] componentize app -o tcp.wasm)
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
target
__pycache__
.mypy_cache
.venv
bacon.toml
dist
Expand All @@ -9,7 +10,11 @@ examples/matrix-math/matrix_math
examples/matrix-math/wasmtime-py
examples/http/.spin
examples/http/http.wasm
examples/http/proxy
examples/http/poll_loop.py
examples/tcp/tcp.wasm
examples/tcp/command
examples/cli/cli.wasm
examples/cli/command
examples/sandbox/sandbox
examples/sandbox/sandbox.wasm
3 changes: 2 additions & 1 deletion examples/cli/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from command import exports


class Run(exports.Run):
def run(self):
def run(self) -> None:
print("Hello, world!")
38 changes: 29 additions & 9 deletions examples/http/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,39 @@
from proxy.types import Ok
from proxy.imports import types
from proxy.imports.types import (
Method_Get, Method_Post, Scheme, Scheme_Http, Scheme_Https, Scheme_Other, IncomingRequest, ResponseOutparam,
OutgoingResponse, Fields, OutgoingBody, OutgoingRequest
Method_Get,
Method_Post,
Scheme,
Scheme_Http,
Scheme_Https,
Scheme_Other,
IncomingRequest,
ResponseOutparam,
OutgoingResponse,
Fields,
OutgoingBody,
OutgoingRequest,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems my autoformatting got me here.. I can revert these changes if you like

)
from poll_loop import Stream, Sink, PollLoop
from typing import Tuple
from urllib import parse


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

def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
"""Handle the specified `request`, sending the response to `response_out`.

"""
def handle(self, request: IncomingRequest, response_out: ResponseOutparam) -> None:
"""Handle the specified `request`, sending the response to `response_out`."""
# Dispatch the request using `asyncio`, backed by a custom event loop
# based on WASI's `poll_oneoff` function.
loop = PollLoop()
asyncio.set_event_loop(loop)
loop.run_until_complete(handle_async(request, response_out))

async def handle_async(request: IncomingRequest, response_out: ResponseOutparam):

async def handle_async(
request: IncomingRequest, response_out: ResponseOutparam
) -> None:
"""Handle the specified `request`, sending the response to `response_out`."""

method = request.method()
Expand All @@ -46,7 +58,10 @@ async def handle_async(request: IncomingRequest, response_out: ResponseOutparam)
# buffering the response bodies), and stream the results back to the
# client as they become available.

urls = map(lambda pair: str(pair[1], "utf-8"), filter(lambda pair: pair[0] == "url", headers))
urls = map(
lambda pair: str(pair[1], "utf-8"),
filter(lambda pair: pair[0] == "url", headers),
)

response = OutgoingResponse(Fields.from_list([("content-type", b"text/plain")]))

Expand All @@ -64,7 +79,11 @@ async def handle_async(request: IncomingRequest, response_out: ResponseOutparam)
elif isinstance(method, Method_Post) and path == "/echo":
# Echo the request body back to the client without buffering.

response = OutgoingResponse(Fields.from_list(list(filter(lambda pair: pair[0] == "content-type", headers))))
response = OutgoingResponse(
Fields.from_list(
list(filter(lambda pair: pair[0] == "content-type", headers))
)
)

response_body = response.body()

Expand All @@ -87,6 +106,7 @@ async def handle_async(request: IncomingRequest, response_out: ResponseOutparam)
ResponseOutparam.set(response_out, Ok(response))
OutgoingBody.finish(body, None)


async def sha256(url: str) -> Tuple[str, str]:
"""Download the contents of the specified URL, computing the SHA-256
incrementally as the response body arrives.
Expand Down
9 changes: 5 additions & 4 deletions examples/matrix-math/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@
from matrix_math import exports
from matrix_math.types import Err


class MatrixMath(matrix_math.MatrixMath):
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()
return numpy.matmul(a, b).tolist() # type: ignore


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

print(MatrixMath().multiply(eval(args[0]), eval(args[1])))

16 changes: 9 additions & 7 deletions examples/sandbox/guest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@
from sandbox.types import Err
import json

def handle(e: Exception):

def handle(e: Exception) -> Err[str]:
message = str(e)
if message == '':
raise Err(f"{type(e).__name__}")
if message == "":
return Err(f"{type(e).__name__}")
else:
raise Err(f"{type(e).__name__}: {message}")
return Err(f"{type(e).__name__}: {message}")


class Sandbox(sandbox.Sandbox):
def eval(self, expression: str) -> str:
try:
return json.dumps(eval(expression))
except Exception as e:
handle(e)
raise handle(e)

def exec(self, statements: str):
def exec(self, statements: str) -> None:
try:
exec(statements)
except Exception as e:
handle(e)
raise handle(e)
17 changes: 10 additions & 7 deletions examples/tcp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@
from command import exports
from typing import Tuple


class Run(exports.Run):
def run(self):
def run(self) -> None:
args = sys.argv[1:]
if len(args) != 1:
print(f"usage: tcp <address>:<port>", file=sys.stderr)
print("usage: tcp <address>:<port>", file=sys.stderr)
exit(-1)

address, port = parse_address_and_port(args[0])
asyncio.run(send_and_receive(address, port))


IPAddress = IPv4Address | IPv6Address



def parse_address_and_port(address_and_port: str) -> Tuple[IPAddress, int]:
ip, separator, port = address_and_port.rpartition(':')
ip, separator, port = address_and_port.rpartition(":")
assert separator
return (ipaddress.ip_address(ip.strip("[]")), int(port))

async def send_and_receive(address: IPAddress, port: int):


async def send_and_receive(address: IPAddress, port: int) -> None:
rx, tx = await asyncio.open_connection(str(address), port)

tx.write(b"hello, world!")
Expand All @@ -33,4 +37,3 @@ async def send_and_receive(address: IPAddress, port: int):

tx.close()
await tx.wait_closed()

11 changes: 6 additions & 5 deletions src/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1348,14 +1348,14 @@ class {camel}(Flag):
if stub_runtime_calls {
format!(
"
def {snake}({params}):
def {snake}({params}){return_type}:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed these were already generated but not added

{docs}{NOT_IMPLEMENTED}
"
)
} else {
format!(
"
def {snake}({params}):
def {snake}({params}){return_type}:
{docs}tmp = componentize_py_runtime.call_import({index}, [{args}], {result_count})[0]
(_, func, args, _) = tmp.finalizer.detach()
self.handle = tmp.handle
Expand Down Expand Up @@ -1403,21 +1403,21 @@ class {camel}(Flag):
let docs =
format!(r#""""{newline}{indent}{doc}{newline}{indent}"""{newline}{indent}"#);
let enter = r#"
def __enter__(self):
def __enter__(self) -> Self:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this only works as of 3.11, but should be fine for the component

"""Returns self"""
return self
"#;
if stub_runtime_calls {
format!(
"{enter}
def __exit__(self, *args):
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None:
{docs}{NOT_IMPLEMENTED}
"
)
} else {
format!(
"{enter}
def __exit__(self, *args):
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the mypy docs, this should be the type signature

{docs}(_, func, args, _) = self.finalizer.detach()
self.handle = None
func(args[0], args[1])
Expand Down Expand Up @@ -1746,6 +1746,7 @@ def {snake}({params}){return_type}:

let python_imports =
"from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self
from types import TracebackType
from enum import Flag, Enum, auto
from dataclasses import dataclass
from abc import abstractmethod
Expand Down