From 452a32fb237cbeb01795000076eab26fba78995c Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 10 Apr 2026 12:08:46 -0400 Subject: [PATCH 1/2] Drop `wrapt` for `tractor_test`, revert to `functools` Realized a bit late that (pretty sure) i already tried this using `wrapt` idea and waay back and found the same "issue" XD The `wrapt.decorator` transparently proxies `__code__` from the async test fn, fooling `pytest`'s coroutine detection into skipping wrapped tests as "unhandled coroutines". `functools.wraps` preserves the sig for fixture injection via `__wrapped__` without leaking the async nature. So i let `claude` rework the latest code to go back to using the old stdlib wrapping again.. Deats, - `functools.partial` replaces `wrapt.PartialCallableObjectProxy`. - wrapper takes plain `**kwargs`; runtime settings extracted via `kwargs.get()` in `_main()`. - `iscoroutinefunction()` guard moved before wrapper definition. - drop all `*args` passing (fixture kwargs only). (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/_testing/pytest.py | 96 +++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 53 deletions(-) diff --git a/tractor/_testing/pytest.py b/tractor/_testing/pytest.py index dc996499..26cbc68b 100644 --- a/tractor/_testing/pytest.py +++ b/tractor/_testing/pytest.py @@ -21,25 +21,24 @@ and applications. ''' from functools import ( partial, + wraps, ) import inspect import platform from typing import ( Callable, - Type, ) import pytest import tractor import trio -import wrapt def tractor_test( wrapped: Callable|None = None, *, # @tractor_test() - timeout:float = 30, + timeout: float = 30, hide_tb: bool = True, ): ''' @@ -89,26 +88,34 @@ def tractor_test( ''' __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 + # handle @tractor_test (no parens) vs @tractor_test(timeout=10) if wrapped is None: - return wrapt.PartialCallableObjectProxy( + return partial( tractor_test, timeout=timeout, - hide_tb=hide_tb + hide_tb=hide_tb, ) - @wrapt.decorator - def wrapper( - wrapped: Callable, - instance: object|Type|None, - args: tuple, - kwargs: dict, - ): + funcname: str = wrapped.__name__ + if not inspect.iscoroutinefunction(wrapped): + raise TypeError( + f'Test-fn {funcname!r} must be an async-function !!' + ) + + # 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 - # NOTE, ensure we inject any test-fn declared fixture names. + # NOTE, ensure we inject any test-fn declared fixture + # names. for kw in [ 'reg_addr', 'loglevel', @@ -121,7 +128,7 @@ def tractor_test( assert kw in kwargs start_method = kwargs.get('start_method') - if platform.system() == "Windows": + if platform.system() == 'Windows': if start_method is None: kwargs['start_method'] = 'trio' elif start_method != 'trio': @@ -129,60 +136,43 @@ def tractor_test( '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. + # Open a root-actor, passing certain runtime-settings + # extracted from the fixture kwargs, 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, - - # 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, - ): + # NOTE: we use `kwargs.get()` (not named params) so that + # the fixture values remain in `kwargs` and are forwarded + # to `wrapped()` — the test fn may declare the same + # fixtures in its own signature. + async def _main(**kwargs): __tracebackhide__: bool = hide_tb + reg_addr = kwargs.get('reg_addr') with trio.fail_after(timeout): async with tractor.open_root_actor( - registry_addrs=[reg_addr] if reg_addr else None, - loglevel=loglevel, - start_method=start_method, + registry_addrs=( + [reg_addr] if reg_addr else None + ), + loglevel=kwargs.get('loglevel'), + start_method=kwargs.get('start_method'), - # TODO: only enable when pytest is passed --pdb - debug_mode=debug_mode, + # TODO: only enable when pytest is passed + # --pdb + debug_mode=kwargs.get('debug_mode', False), ): # invoke test-fn body IN THIS task - await wrapped( - *args, - **kwargs, - ) - - funcname = wrapped.__name__ - if not inspect.iscoroutinefunction(wrapped): - raise TypeError( - f"Test-fn {funcname!r} must be an async-function !!" - ) + await wrapped(**kwargs) # invoke runtime via a root task. return trio.run( partial( _main, - *args, **kwargs, ) ) - - return wrapper( - wrapped, - ) + return wrapper def pytest_addoption( From 9af6adc181971cd9139cf080cf42dac9460e9d3c Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 10 Apr 2026 16:20:01 -0400 Subject: [PATCH 2/2] Fix runtime kwarg leaking in `tractor_test` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `functools` rewrite forwarded all `kwargs` through `_main(**kwargs)` to `wrapped(**kwargs)` unchanged — the Windows `start_method` default could leak to test fns that don't declare it. The pre-wrapt code guarded against this with named wrapper params. Extract runtime settings (`reg_addr`, `loglevel`, `debug_mode`, `start_method`) as closure locals in `wrapper`; `_main` uses them directly for `open_root_actor()` while `kwargs` passes to `wrapped()` unmodified. Review: PR #439 (Copilot) https://github.com/goodboy/tractor/pull/439#pullrequestreview-4091005202 (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/_testing/pytest.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/tractor/_testing/pytest.py b/tractor/_testing/pytest.py index 26cbc68b..f843fb4c 100644 --- a/tractor/_testing/pytest.py +++ b/tractor/_testing/pytest.py @@ -127,38 +127,43 @@ def tractor_test( if kw in inspect.signature(wrapped).parameters: 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') if platform.system() == 'Windows': if start_method is None: - kwargs['start_method'] = 'trio' + start_method = 'trio' elif start_method != 'trio': raise ValueError( 'ONLY the `start_method="trio"` is supported on Windows.' ) - # Open a root-actor, passing certain runtime-settings - # extracted from the fixture kwargs, then invoke the - # test-fn body as the root-most task. + # Open a root-actor, passing runtime-settings + # extracted above as closure locals, then invoke + # the test-fn body as the root-most task. # - # NOTE: we use `kwargs.get()` (not named params) so that - # the fixture values remain in `kwargs` and are forwarded - # to `wrapped()` — the test fn may declare the same - # fixtures in its own signature. + # NOTE: `kwargs` is forwarded as-is to + # `wrapped()` — it only contains what pytest + # injected based on the test fn's signature. async def _main(**kwargs): __tracebackhide__: bool = hide_tb - reg_addr = kwargs.get('reg_addr') with trio.fail_after(timeout): async with tractor.open_root_actor( registry_addrs=( [reg_addr] if reg_addr else None ), - loglevel=kwargs.get('loglevel'), - start_method=kwargs.get('start_method'), + loglevel=loglevel, + start_method=start_method, # TODO: only enable when pytest is passed # --pdb - debug_mode=kwargs.get('debug_mode', False), + debug_mode=debug_mode, ): # invoke test-fn body IN THIS task