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
2 changes: 1 addition & 1 deletion src/ai/types/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def _resolve_random(source: RandomSource) -> random.Random:
return source if isinstance(source, random.Random) else source()


@util.contextmanager_with_async_decorator
@util.contextmanager_any_sync
def use_random(source: RandomSource) -> Iterator[None]:
"""Draw message/part ids from ``source`` within this context.

Expand Down
34 changes: 27 additions & 7 deletions src/ai/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from types import TracebackType


class ContextManagerWithAsyncDecorator[T](Protocol):
class ContextManagerAnySync[T](Protocol):
def __enter__(self) -> T: ...

def __exit__(
Expand All @@ -32,6 +32,15 @@ def __exit__(
traceback: TracebackType | None,
) -> bool | None: ...

async def __aenter__(self) -> T: ...

async def __aexit__(
self,
typ: type[BaseException] | None,
value: BaseException | None,
traceback: TracebackType | None,
) -> bool | None: ...

@overload
def __call__[**P, R](
self, func: Callable[P, Coroutine[Any, Any, R]]
Expand Down Expand Up @@ -141,9 +150,20 @@ async def __aexit__(
return False


class _GeneratorContextManagerWithAsyncDecorator[T](
class _GeneratorContextManagerAnySync[T](
contextlib._GeneratorContextManager[T]
):
async def __aenter__(self) -> T:
return self.__enter__()

async def __aexit__(
self,
typ: type[BaseException] | None,
value: BaseException | None,
traceback: TracebackType | None,
) -> bool | None:
return self.__exit__(typ, value, traceback)

def __call__[_F: Callable[..., Any]](self, func: _F) -> _F:
if inspect.iscoroutinefunction(func):

Expand All @@ -161,16 +181,16 @@ def inner(*args: Any, **kwds: Any) -> Any:
return cast("_F", inner)


def contextmanager_with_async_decorator[**P, T](
def contextmanager_any_sync[**P, T](
func: Callable[P, Iterator[T]],
) -> Callable[P, ContextManagerWithAsyncDecorator[T]]:
"""@contextmanager decorator but the result can be a decorator for async."""
) -> Callable[P, ContextManagerAnySync[T]]:
"""@contextmanager decorator but the result is also usable in async."""

@functools.wraps(func)
def helper(
*args: P.args, **kwds: P.kwargs
) -> _GeneratorContextManagerWithAsyncDecorator[T]:
return _GeneratorContextManagerWithAsyncDecorator(
) -> _GeneratorContextManagerAnySync[T]:
return _GeneratorContextManagerAnySync(
cast("Callable[..., Generator[T, None, None]]", func), args, kwds
)

Expand Down
7 changes: 6 additions & 1 deletion tests/types/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,10 +455,15 @@ def test_use_random_overrides_and_restores() -> None:
assert messages.generate_id("msg").startswith("msg_")


async def test_use_random_decorator_handles_async_functions() -> None:
async def test_use_random_overrides_and_restores_async() -> None:
with messages.use_random(random.Random(0)):
expected = messages.generate_id("msg")

# Works across an await...
async with messages.use_random(random.Random(0)):
await asyncio.sleep(0)
assert messages.generate_id("msg") == expected

# Works as a decorator on an async fn, resolving the factory per call.
@messages.use_random(lambda: random.Random(0))
async def build() -> str:
Expand Down
Loading