Use `wrapt` for `tractor_test()` decorator

Refactor the test-fn deco to use `wrapt.decorator`
instead of `functools.wraps` for better fn-sig
preservation and optional-args support via
`PartialCallableObjectProxy`.

Deats,
- add `timeout` and `hide_tb` deco params
- wrap test-fn body with `trio.fail_after(timeout)`
- consolidate per-fixture `if` checks into a loop
- add `iscoroutinefunction()` type-check on wrapped fn
- set `__tracebackhide__` at each wrapper level

Also,
- update imports for new subpkg paths:
  `tractor.spawn._spawn`, `tractor.discovery._addr`,
  `tractor.runtime._state`
  (see upcoming, likely large patch commit ;)

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
multicast_revertable_streams
Gud Boi 2026-03-23 18:36:56 -04:00
parent 94458807ce
commit 6827ceba12
1 changed files with 109 additions and 51 deletions

View File

@ -21,17 +21,27 @@ and applications.
''' '''
from functools import ( from functools import (
partial, partial,
wraps,
) )
import inspect import inspect
import platform import platform
from typing import (
Callable,
Type,
)
import pytest import pytest
import tractor import tractor
import trio import trio
import wrapt
def tractor_test(fn): def tractor_test(
wrapped: Callable|None = None,
*,
# @tractor_test(<deco-params>)
timeout:float = 30,
hide_tb: bool = True,
):
''' '''
Decorator for async test fns to decorator-wrap them as "native" Decorator for async test fns to decorator-wrap them as "native"
looking sync funcs runnable by `pytest` and auto invoked with looking sync funcs runnable by `pytest` and auto invoked with
@ -45,8 +55,18 @@ def tractor_test(fn):
Basic deco use: Basic deco use:
--------------- ---------------
@tractor_test @tractor_test(
async def test_whatever(): timeout=10,
)
async def test_whatever(
# fixture param declarations
loglevel: str,
start_method: str,
reg_addr: tuple,
tpt_proto: str,
debug_mode: bool,
):
# already inside a root-actor runtime `trio.Task`
await ... await ...
@ -67,52 +87,74 @@ def tractor_test(fn):
`tractor.open_root_actor()` funcargs. `tractor.open_root_actor()` funcargs.
''' '''
@wraps(fn) __tracebackhide__: bool = hide_tb
# handle the decorator not called with () case.
# i.e. in `wrapt` support a deco-with-optional-args,
# https://wrapt.readthedocs.io/en/master/decorators.html#decorators-with-optional-arguments
if wrapped is None:
return wrapt.PartialCallableObjectProxy(
tractor_test,
timeout=timeout,
hide_tb=hide_tb
)
@wrapt.decorator
def wrapper( def wrapper(
wrapped: Callable,
instance: object|Type|None,
args: tuple,
kwargs: dict,
):
__tracebackhide__: bool = hide_tb
# NOTE, ensure we inject any test-fn declared fixture names.
for kw in [
'reg_addr',
'loglevel',
'start_method',
'debug_mode',
'tpt_proto',
'timeout',
]:
if kw in inspect.signature(wrapped).parameters:
assert kwargs[kw]
if (
(start_method := kwargs.get('start_method'))
is
None
):
if (
platform.system() == "Windows"
and
start_method != 'trio'
):
raise ValueError(
'ONLY the `start_method="trio"` is supported on Windows.'
)
# open a root-actor, passing certain
# `tractor`-runtime-settings, then invoke the test-fn body as
# the root-most task.
#
# https://wrapt.readthedocs.io/en/master/decorators.html#processing-function-arguments
async def _main(
*args, *args,
loglevel=None,
reg_addr=None, # runtime-settings
loglevel:str|None = None,
reg_addr:tuple|None = None,
start_method: str|None = None, start_method: str|None = None,
debug_mode: bool = False, debug_mode: bool = False,
tpt_proto: str|None=None, tpt_proto: str|None = None,
**kwargs
**kwargs,
): ):
# __tracebackhide__ = True __tracebackhide__: bool = hide_tb
# NOTE: inject ant test func declared fixture with trio.fail_after(timeout):
# names by manually checking!
if 'reg_addr' in inspect.signature(fn).parameters:
# injects test suite fixture value to test as well
# as `run()`
kwargs['reg_addr'] = reg_addr
if 'loglevel' in inspect.signature(fn).parameters:
# allows test suites to define a 'loglevel' fixture
# that activates the internal logging
kwargs['loglevel'] = loglevel
if start_method is None:
if platform.system() == "Windows":
start_method = 'trio'
if 'start_method' in inspect.signature(fn).parameters:
# set of subprocess spawning backends
kwargs['start_method'] = start_method
if 'debug_mode' in inspect.signature(fn).parameters:
# set of subprocess spawning backends
kwargs['debug_mode'] = debug_mode
if 'tpt_proto' in inspect.signature(fn).parameters:
# set of subprocess spawning backends
kwargs['tpt_proto'] = tpt_proto
if kwargs:
# use explicit root actor start
async def _main():
async with tractor.open_root_actor( async with tractor.open_root_actor(
# **kwargs,
registry_addrs=[reg_addr] if reg_addr else None, registry_addrs=[reg_addr] if reg_addr else None,
loglevel=loglevel, loglevel=loglevel,
start_method=start_method, start_method=start_method,
@ -121,17 +163,31 @@ def tractor_test(fn):
debug_mode=debug_mode, debug_mode=debug_mode,
): ):
await fn(*args, **kwargs) # invoke test-fn body IN THIS task
await wrapped(
*args,
**kwargs,
)
main = _main funcname = wrapped.__name__
if not inspect.iscoroutinefunction(wrapped):
raise TypeError(
f"Test-fn {funcname!r} must be an async-function !!"
)
else: # invoke runtime via a root task.
# use implicit root actor start return trio.run(
main = partial(fn, *args, **kwargs) partial(
_main,
*args,
**kwargs,
)
)
return trio.run(main)
return wrapper return wrapper(
wrapped,
)
def pytest_addoption( def pytest_addoption(
@ -179,7 +235,8 @@ def pytest_addoption(
def pytest_configure(config): def pytest_configure(config):
backend = config.option.spawn_backend backend = config.option.spawn_backend
tractor._spawn.try_set_start_method(backend) from tractor.spawn._spawn import try_set_start_method
try_set_start_method(backend)
# register custom marks to avoid warnings see, # register custom marks to avoid warnings see,
# https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers # https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers
@ -225,7 +282,8 @@ def tpt_protos(request) -> list[str]:
# XXX ensure we support the protocol by name via lookup! # XXX ensure we support the protocol by name via lookup!
for proto_key in proto_keys: for proto_key in proto_keys:
addr_type = tractor._addr._address_types[proto_key] from tractor.discovery import _addr
addr_type = _addr._address_types[proto_key]
assert addr_type.proto_key == proto_key assert addr_type.proto_key == proto_key
yield proto_keys yield proto_keys
@ -256,7 +314,7 @@ def tpt_proto(
# f'tpt-proto={proto_key!r}\n' # f'tpt-proto={proto_key!r}\n'
# ) # )
from tractor import _state from tractor.runtime import _state
if _state._def_tpt_proto != proto_key: if _state._def_tpt_proto != proto_key:
_state._def_tpt_proto = proto_key _state._def_tpt_proto = proto_key
_state._runtime_vars['_enable_tpts'] = [ _state._runtime_vars['_enable_tpts'] = [