diff --git a/bundled/componentize_py_async_support/__init__.py b/bundled/componentize_py_async_support/__init__.py index a7bd016..d402566 100644 --- a/bundled/componentize_py_async_support/__init__.py +++ b/bundled/componentize_py_async_support/__init__.py @@ -1,3 +1,11 @@ +"""This module contains code to integrate Component Model async features with +Python's `asyncio` framework. + +Most of the code here is used internally by code generated by `componentize-py` +for worlds which use async features. As of this writing, the only item meant to +be used directly by application code is the `spawn` function. + +""" import asyncio import componentize_py_runtime import subprocess @@ -441,6 +449,11 @@ async def _return_result(export_index: int, borrows: int, coroutine: Any) -> Non _future_state.get().pending_count -= 1 def first_poll(export_index: int, borrows: int, coroutine: Any) -> int: + """Internal function called by generated code for exported functions. + + This is not meant to be called by application code. + + """ context = Context() future_state = _FutureState(None, {}, [], 1) context.run(_set_future_state, future_state) @@ -464,6 +477,11 @@ def _poll(future_state: _FutureState) -> int: return _CallbackCode.WAIT | (waitable_set << 4) def callback(event0: int, event1: int, event2: int) -> int: + """Internal function called by generated code for exported functions. + + This is not meant to be called by application code. + + """ future_state = componentize_py_runtime.context_get() componentize_py_runtime.context_set(None) @@ -493,6 +511,11 @@ def callback(event0: int, event1: int, event2: int) -> int: return _poll(future_state) async def await_result[T](result: Result[T, tuple[int, int]]) -> T: + """Internal function called by generated code for imported functions. + + This is not meant to be called directly by application code. + + """ global _loop global _future_state @@ -522,6 +545,24 @@ async def _wrap_spawned(coroutine: Any) -> None: _future_state.get().pending_count -= 1 def spawn(coroutine: Any) -> None: + """Spawn an awaitable to be executed as part of the current Component Model + task. + + Each call to an async function exported by a component creates a new + Component Model task which is represented as a Python awaitable. By + default, that task will exit after returning a value. However, this + function may be used to do additional work concurrently, and that additional + work may extend the lifetime of the task arbitrarily after it has returned a + value. + + For example, implementing `wasi:http/service@0.3.x` involves returning a + `response` resource, but sending the response body must happen after + returning since it involves writing to a stream which the caller will only + have received as part of the `response` resource. Therefore, the only way + to write the response body is by using this function. See the `http-p3` + example in the `componentize-py` Git repository for what that looks like. + + """ global _future_state _future_state.get().pending_count += 1 diff --git a/bundled/componentize_py_async_support/futures.py b/bundled/componentize_py_async_support/futures.py index a50c16e..10c58e4 100644 --- a/bundled/componentize_py_async_support/futures.py +++ b/bundled/componentize_py_async_support/futures.py @@ -9,12 +9,40 @@ T = TypeVar('T') class FutureReader(Generic[T]): + """Represents the readable end of a Component Model `future`. + + Each object of this type should be closed promptly using either context + management (e.g. a `with` statement) or by calling `read` in order to notify + the owner of the writable end of the `future` that the readable end has been + closed. If the object becomes unreachable without being closed, it will be + closed via finalization. + + """ def __init__(self, type_: int, handle: int): + """Constructor for internal use by generated code. + + Application code should not call this directly. Instead, + `componentize-py` will generate a constructor function for each unique + `future` type used by the target world. Each such function will return + a (`FutureReader`, `FutureWriter`) pair and use this constructor behind + the scenes. + + """ self.type_ = type_ self.handle: int | None = handle self.finalizer = weakref.finalize(self, componentize_py_runtime.future_drop_readable, type_, handle) async def read(self) -> T: + """Asynchronously read the value sent to this `future`. + + The awaitable returned by this function will resolve when a value has + been delivered to the `future`. + + Calling this function consumes the target object; any attempt to call + `read` more than once will raise an `AssertionError`. + + """ + self.finalizer.detach() handle = self.handle self.handle = None @@ -42,23 +70,51 @@ def __exit__(self, return None -async def write(type_: int, handle: int, value: Any) -> None: +async def _write(type_: int, handle: int, value: Any) -> None: await componentize_py_async_support.await_result( componentize_py_runtime.future_write(type_, handle, value) ) componentize_py_runtime.future_drop_writable(type_, handle) -def write_default(type_: int, handle: int, default: Callable[[], Any]) -> None: - componentize_py_async_support.spawn(write(type_, handle, default())) +def _write_default(type_: int, handle: int, default: Callable[[], Any]) -> None: + componentize_py_async_support.spawn(_write(type_, handle, default())) class FutureWriter(Generic[T]): + """Represents the writable end of a Component Model `future`. + + Each object of this type should be closed promptly using either context + management (e.g. a `with` statement) or by calling `write` in order to send + a value to the readable end of the `future. If the object becomes + unreachable without being closed, it will be closed via finalization after + sending the default value which was specified during construction. + + """ def __init__(self, type_: int, handle: int, default: Callable[[], T]): + """Constructor for internal use by generated code. + + Application code should not call this directly. Instead, + `componentize-py` will generate a constructor function for each unique + `future` type used by the target world. Each such function will return + a (`FutureReader`, `FutureWriter`) pair and use this constructor behind + the scenes. + + """ self.type_ = type_ self.handle: int | None = handle self.default = default - self.finalizer = weakref.finalize(self, write_default, type_, handle, default) + self.finalizer = weakref.finalize(self, _write_default, type_, handle, default) async def write(self, value: T) -> bool: + """Asynchronously write a value to the `future`. + + The awaitable returned by this function will resolve once either the + value has been delivered to the readable end of the `future` or the + readable end has been closed. + + Calling this function consumes the target object; any attempt to call + `write` more than once will raise an `AssertionError`. + + """ self.finalizer.detach() handle = self.handle self.handle = None diff --git a/bundled/componentize_py_async_support/streams.py b/bundled/componentize_py_async_support/streams.py index 7650d62..0b0b95a 100644 --- a/bundled/componentize_py_async_support/streams.py +++ b/bundled/componentize_py_async_support/streams.py @@ -7,27 +7,60 @@ from componentize_py_async_support import _ReturnCode class ByteStreamReader: + """Represents the readable end of a Component Model `stream`. + + Each object of this type should be closed promptly using context management + (e.g. a `with` statement) in order to notify the owner of the writable end + of the `stream` that the readable end has been closed. If the object + becomes unreachable without being closed, it will be closed via + finalization. + + """ def __init__(self, type_: int, handle: int): + """Constructor for internal use by generated code. + + Application code should not call this directly. Instead, + `componentize-py` will generate `byte_stream` function if the target + world uses the `stream` type. That function will return a + (`ByteStreamReader`, `ByteStreamWriter`) pair and use this constructor + behind the scenes. + + """ self.writer_dropped = False self.type_ = type_ self.handle: int | None = handle self.finalizer = weakref.finalize(self, componentize_py_runtime.stream_drop_readable, type_, handle) async def read(self, max_count: int) -> bytes: + """Asynchronously read up to `max_count` bytes sent to this `stream`. + + The awaitable returned by this function will resolve when either at + least one byte has been delivered to the `stream` or the writable end + has been closed (in which case the `writer_dropped` field will be set to + `True`). + + Only one `read` operation is allowed at a time for a given object. Any + attempt to start a second read while the first is still in progress will + raise an `AssertionError`. + + """ if self.writer_dropped: return bytes() - code, values = await self._read(max_count) + handle = self.handle + self.handle = None + code, values = await self._read(max_count, handle) + self.handle = handle if code == _ReturnCode.DROPPED: self.writer_dropped = True return values - async def _read(self, max_count: int) -> tuple[int, bytes]: - if self.handle is not None: + async def _read(self, max_count: int, handle: int | None) -> tuple[int, bytes]: + if handle is not None: return cast(tuple[int, bytes], await componentize_py_async_support.await_result( - componentize_py_runtime.stream_read(self.type_, self.handle, max_count) + componentize_py_runtime.stream_read(self.type_, handle, max_count) )) else: raise AssertionError @@ -47,32 +80,77 @@ def __exit__(self, return None class ByteStreamWriter: + """Represents the writable end of a Component Model `stream`. + + Each object of this type should be closed promptly using context management + (e.g. a `with` statement) in order to notify the owner of the readable end + of the `stream` that the writable end has been closed. If the object + becomes unreachable without being closed, it will be closed via + finalization. + + """ def __init__(self, type_: int, handle: int): + """Constructor for internal use by generated code. + + Application code should not call this directly. Instead, + `componentize-py` will generate `byte_stream` function if the target + world uses the `stream` type. That function will return a + (`ByteStreamReader`, `ByteStreamWriter`) pair and use this constructor + behind the scenes. + + """ self.reader_dropped = False self.type_ = type_ self.handle: int | None = handle self.finalizer = weakref.finalize(self, componentize_py_runtime.stream_drop_writable, type_, handle) async def write(self, source: bytes) -> int: + """Asynchronously write (some of) the specified bytes to the `stream`. + + The awaitable returned by this function will resolve when either at + least one byte has been delivered to the `stream` or the readable end + has been closed (in which case the `reader_dropped` field will be set to + `True`). + + The return value is the total number of bytes delivered (which may be + less than `len(source)`). See also `write_all`, which attempts to write + the entire buffer before returning. + + Only one `write` operation is allowed at a time for a given object. Any + attempt to start a second write while the first is still in progress + will raise an `AssertionError`. + + """ if self.reader_dropped: return 0 - code, count = await self._write(source) + handle = self.handle + self.handle = None + code, count = await self._write(source, handle) + self.handle = handle if code == _ReturnCode.DROPPED: self.reader_dropped = True return count - async def _write(self, source: bytes) -> tuple[int, int]: - if self.handle is not None: + async def _write(self, source: bytes, handle: int | None) -> tuple[int, int]: + if handle is not None: return await componentize_py_async_support.await_result( - componentize_py_runtime.stream_write(self.type_, self.handle, source) + componentize_py_runtime.stream_write(self.type_, handle, source) ) else: raise AssertionError async def write_all(self, source: bytes) -> int: + """Asynchronously write the specified bytes to the `stream`. + + This calls `write` in a loop until either the entire buffer has been + delivered or the readable end of the `stream` has been closed. The + return value is the total number of bytes delivered (which may be less + than `len(source)`). + + """ total = 0 while len(source) > 0 and not self.reader_dropped: @@ -99,27 +177,60 @@ def __exit__(self, T = TypeVar('T') class StreamReader(Generic[T]): + """Represents the readable end of a Component Model `stream`. + + Each object of this type should be closed promptly using context management + (e.g. a `with` statement) in order to notify the owner of the writable end + of the `stream` that the readable end has been closed. If the object + becomes unreachable without being closed, it will be closed via + finalization. + + """ def __init__(self, type_: int, handle: int): + """Constructor for internal use by generated code. + + Application code should not call this directly. Instead, + `componentize-py` will generate a constructor function for each unique + `stream` type used by the target world. Each such function will return + a (`StreamReader`, `StreamWriter`) pair and use this constructor behind + the scenes. + + """ self.writer_dropped = False self.type_ = type_ self.handle: int | None = handle self.finalizer = weakref.finalize(self, componentize_py_runtime.stream_drop_readable, type_, handle) async def read(self, max_count: int) -> list[T]: + """Asynchronously read up to `max_count` items sent to this `stream`. + + The awaitable returned by this function will resolve when either at + least one item has been delivered to the `stream` or the writable end + has been closed (in which case the `writer_dropped` field will be set to + `True`). + + Only one `read` operation is allowed at a time for a given object. Any + attempt to start a second read while the first is still in progress will + raise an `AssertionError`. + + """ if self.writer_dropped: return [] - code, values = await self._read(max_count) + handle = self.handle + self.handle = None + code, values = await self._read(max_count, handle) + self.handle = handle if code == _ReturnCode.DROPPED: self.writer_dropped = True return values - async def _read(self, max_count: int) -> tuple[int, list[T]]: - if self.handle is not None: + async def _read(self, max_count: int, handle: int | None) -> tuple[int, list[T]]: + if handle is not None: return cast(tuple[int, list[T]], await componentize_py_async_support.await_result( - componentize_py_runtime.stream_read(self.type_, self.handle, max_count) + componentize_py_runtime.stream_read(self.type_, handle, max_count) )) else: raise AssertionError @@ -139,32 +250,77 @@ def __exit__(self, return None class StreamWriter(Generic[T]): + """Represents the writable end of a Component Model `stream`. + + Each object of this type should be closed promptly using context management + (e.g. a `with` statement) in order to notify the owner of the readable end + of the `stream` that the writable end has been closed. If the object + becomes unreachable without being closed, it will be closed via + finalization. + + """ def __init__(self, type_: int, handle: int): + """Constructor for internal use by generated code. + + Application code should not call this directly. Instead, + `componentize-py` will generate a constructor function for each unique + `stream` type used by the target world. Each such function will return + a (`StreamReader`, `StreamWriter`) pair and use this constructor behind + the scenes. + + """ self.reader_dropped = False self.type_ = type_ self.handle: int | None = handle self.finalizer = weakref.finalize(self, componentize_py_runtime.stream_drop_writable, type_, handle) async def write(self, source: list[T]) -> int: + """Asynchronously write (some of) the specified itmes to the `stream`. + + The awaitable returned by this function will resolve when either at + least one item has been delivered to the `stream` or the readable end + has been closed (in which case the `reader_dropped` field will be set to + `True`). + + The return value is the total number of items delivered (which may be + less than `len(source)`). See also `write_all`, which attempts to write + the entire buffer before returning. + + Only one `write` operation is allowed at a time for a given object. Any + attempt to start a second write while the first is still in progress will + raise an `AssertionError`. + + """ if self.reader_dropped: return 0 - code, count = await self._write(source) + handle = self.handle + self.handle = None + code, count = await self._write(source, handle) + self.handle = handle if code == _ReturnCode.DROPPED: self.reader_dropped = True return count - async def _write(self, source: list[T]) -> tuple[int, int]: - if self.handle is not None: + async def _write(self, source: list[T], handle: int | None) -> tuple[int, int]: + if handle is not None: return await componentize_py_async_support.await_result( - componentize_py_runtime.stream_write(self.type_, self.handle, source) + componentize_py_runtime.stream_write(self.type_, handle, source) ) else: raise AssertionError async def write_all(self, source: list[T]) -> int: + """Asynchronously write the specified items to the `stream`. + + This calls `write` in a loop until either the entire buffer has been + delivered or the readable end of the `stream` has been closed. The + return value is the total number of items delivered (which may be less + than `len(source)`). + + """ total = 0 while len(source) > 0 and not self.reader_dropped: diff --git a/bundled/componentize_py_runtime.pyi b/bundled/componentize_py_runtime.pyi index ff09cf8..ed2f42b 100644 --- a/bundled/componentize_py_runtime.pyi +++ b/bundled/componentize_py_runtime.pyi @@ -1,3 +1,9 @@ +"""Internal functions used by generated code to interact with the +`componentize-py` runtime. + +These are not meant to be called directly by application code. + +""" from typing import Any from componentize_py_types import Result diff --git a/bundled/componentize_py_types.py b/bundled/componentize_py_types.py index 9867d14..4269f15 100644 --- a/bundled/componentize_py_types.py +++ b/bundled/componentize_py_types.py @@ -4,16 +4,29 @@ S = TypeVar('S') @dataclass class Some(Generic[S]): + """Represents the "present" (i.e. non-absent) case of an optional value. + + This is used to disambiguate values of a nested ComponentModel `option` type + (e.g. `option>` or similar). Non-nested `option` values are + represented using `typing.Optional`, i.e. nullable types. + + """ value: S T = TypeVar('T') @dataclass class Ok(Generic[T]): + """Represents the success case of a Component Model `result` value.""" value: T E = TypeVar('E') @dataclass(frozen=True) class Err(Generic[E], Exception): + """Represents the failure case of a Component Model `result` value.""" value: E Result = Union[Ok[T], Err[E]] +"""Represents a Component Model `result` value, i.e. a variant type representing +either success payload or failure payload. + +"""