diff --git a/.github/workflows/lint_examples.sh b/.github/workflows/lint_examples.sh new file mode 100755 index 0000000..7b85c40 --- /dev/null +++ b/.github/workflows/lint_examples.sh @@ -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/command@0.2.0 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/proxy@0.2.0 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/command@0.2.0 bindings . \ + && mypy --strict .) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f46eda0..1cdb4b0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -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 diff --git a/.github/workflows/test_examples.sh b/.github/workflows/test_examples.sh new file mode 100755 index 0000000..dddd905 --- /dev/null +++ b/.github/workflows/test_examples.sh @@ -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 \ + && ../../target/release/componentize-py -d ../../wit -w wasi:cli/command@0.2.0 componentize app -o cli.wasm \ + && wasmtime run cli.wasm) + +# HTTP +# Just compiling for now +(cd examples/http \ + && ../../target/release/componentize-py -d ../../wit -w wasi:http/proxy@0.2.0 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/command@0.2.0 componentize app -o tcp.wasm) diff --git a/.gitignore b/.gitignore index 237ffe1..b76d7a0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target __pycache__ +.mypy_cache .venv bacon.toml dist @@ -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 diff --git a/examples/cli/app.py b/examples/cli/app.py index 9d5cd96..9abeb06 100644 --- a/examples/cli/app.py +++ b/examples/cli/app.py @@ -1,5 +1,6 @@ from command import exports + class Run(exports.Run): - def run(self): + def run(self) -> None: print("Hello, world!") diff --git a/examples/http/app.py b/examples/http/app.py index 3d4741c..de6a78a 100644 --- a/examples/http/app.py +++ b/examples/http/app.py @@ -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, ) 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() @@ -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")])) @@ -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() @@ -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. diff --git a/examples/matrix-math/app.py b/examples/matrix-math/app.py index 58e3a92..b16902e 100644 --- a/examples/matrix-math/app.py +++ b/examples/matrix-math/app.py @@ -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 ", file=sys.stderr) + print("usage: matrix-math ", file=sys.stderr) exit(-1) print(MatrixMath().multiply(eval(args[0]), eval(args[1]))) - diff --git a/examples/sandbox/guest.py b/examples/sandbox/guest.py index 86266ec..43f13e6 100644 --- a/examples/sandbox/guest.py +++ b/examples/sandbox/guest.py @@ -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) diff --git a/examples/tcp/app.py b/examples/tcp/app.py index 3978d2b..bb47257 100644 --- a/examples/tcp/app.py +++ b/examples/tcp/app.py @@ -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
:", file=sys.stderr) + print("usage: tcp
:", 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!") @@ -33,4 +37,3 @@ async def send_and_receive(address: IPAddress, port: int): tx.close() await tx.wait_closed() - diff --git a/src/summary.rs b/src/summary.rs index e9c76e8..d10238b 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -1348,14 +1348,14 @@ class {camel}(Flag): if stub_runtime_calls { format!( " - def {snake}({params}): + def {snake}({params}){return_type}: {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 @@ -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: """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: {docs}(_, func, args, _) = self.finalizer.detach() self.handle = None func(args[0], args[1]) @@ -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