Merge pull request #439 from goodboy/revert_wrapt_tractor_test_deco

Drop `wrapt` for `tractor_test`, revert to `functools`
subint_spawner_backend
Bd 2026-04-10 16:36:03 -04:00 committed by GitHub
commit 2db6f97130
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1 changed files with 46 additions and 51 deletions

View File

@ -21,25 +21,24 @@ and applications.
''' '''
from functools import ( from functools import (
partial, partial,
wraps,
) )
import inspect import inspect
import platform import platform
from typing import ( from typing import (
Callable, Callable,
Type,
) )
import pytest import pytest
import tractor import tractor
import trio import trio
import wrapt
def tractor_test( def tractor_test(
wrapped: Callable|None = None, wrapped: Callable|None = None,
*, *,
# @tractor_test(<deco-params>) # @tractor_test(<deco-params>)
timeout:float = 30, timeout: float = 30,
hide_tb: bool = True, hide_tb: bool = True,
): ):
''' '''
@ -89,26 +88,34 @@ def tractor_test(
''' '''
__tracebackhide__: bool = hide_tb __tracebackhide__: bool = hide_tb
# handle the decorator not called with () case. # handle @tractor_test (no parens) vs @tractor_test(timeout=10)
# 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: if wrapped is None:
return wrapt.PartialCallableObjectProxy( return partial(
tractor_test, tractor_test,
timeout=timeout, timeout=timeout,
hide_tb=hide_tb hide_tb=hide_tb,
) )
@wrapt.decorator funcname: str = wrapped.__name__
def wrapper( if not inspect.iscoroutinefunction(wrapped):
wrapped: Callable, raise TypeError(
instance: object|Type|None, f'Test-fn {funcname!r} must be an async-function !!'
args: tuple, )
kwargs: dict,
): # NOTE: we intentionally use `functools.wraps` instead of
# `@wrapt.decorator` here bc wrapt's transparent proxy makes
# `inspect.iscoroutinefunction(wrapper)` return `True` (it
# proxies `__code__` from the wrapped async fn), which causes
# pytest to skip the test as an "unhandled coroutine".
# `functools.wraps` preserves the signature for fixture
# injection (via `__wrapped__`) without leaking the async
# nature.
@wraps(wrapped)
def wrapper(**kwargs):
__tracebackhide__: bool = hide_tb __tracebackhide__: bool = hide_tb
# NOTE, ensure we inject any test-fn declared fixture names. # NOTE, ensure we inject any test-fn declared fixture
# names.
for kw in [ for kw in [
'reg_addr', 'reg_addr',
'loglevel', 'loglevel',
@ -120,69 +127,57 @@ def tractor_test(
if kw in inspect.signature(wrapped).parameters: if kw in inspect.signature(wrapped).parameters:
assert kw in kwargs assert kw in kwargs
# Extract runtime settings as locals for
# `open_root_actor()`; these must NOT leak into
# `kwargs` when the test fn doesn't declare them
# (the original pre-wrapt code had the same guard).
reg_addr = kwargs.get('reg_addr')
loglevel = kwargs.get('loglevel')
debug_mode = kwargs.get('debug_mode', False)
start_method = kwargs.get('start_method') start_method = kwargs.get('start_method')
if platform.system() == "Windows": if platform.system() == 'Windows':
if start_method is None: if start_method is None:
kwargs['start_method'] = 'trio' start_method = 'trio'
elif start_method != 'trio': elif start_method != 'trio':
raise ValueError( raise ValueError(
'ONLY the `start_method="trio"` is supported on Windows.' 'ONLY the `start_method="trio"` is supported on Windows.'
) )
# open a root-actor, passing certain # Open a root-actor, passing runtime-settings
# `tractor`-runtime-settings, then invoke the test-fn body as # extracted above as closure locals, then invoke
# the root-most task. # the test-fn body as the root-most task.
# #
# https://wrapt.readthedocs.io/en/master/decorators.html#processing-function-arguments # NOTE: `kwargs` is forwarded as-is to
async def _main( # `wrapped()` — it only contains what pytest
*args, # injected based on the test fn's signature.
async def _main(**kwargs):
# runtime-settings
loglevel:str|None = None,
reg_addr:tuple|None = None,
start_method: str|None = None,
debug_mode: bool = False,
tpt_proto: str|None = None,
**kwargs,
):
__tracebackhide__: bool = hide_tb __tracebackhide__: bool = hide_tb
with trio.fail_after(timeout): with trio.fail_after(timeout):
async with tractor.open_root_actor( async with tractor.open_root_actor(
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,
# TODO: only enable when pytest is passed --pdb # TODO: only enable when pytest is passed
# --pdb
debug_mode=debug_mode, debug_mode=debug_mode,
): ):
# invoke test-fn body IN THIS task # invoke test-fn body IN THIS task
await wrapped( await wrapped(**kwargs)
*args,
**kwargs,
)
funcname = wrapped.__name__
if not inspect.iscoroutinefunction(wrapped):
raise TypeError(
f"Test-fn {funcname!r} must be an async-function !!"
)
# invoke runtime via a root task. # invoke runtime via a root task.
return trio.run( return trio.run(
partial( partial(
_main, _main,
*args,
**kwargs, **kwargs,
) )
) )
return wrapper
return wrapper(
wrapped,
)
def pytest_addoption( def pytest_addoption(