From 0a97dd4e7184dd530f3e913b84f91f7e6ca9ecc6 Mon Sep 17 00:00:00 2001 From: goodboy Date: Thu, 2 Jul 2026 12:19:43 -0400 Subject: [PATCH] Defer `asyncio` import via PEP-562 `.to_asyncio` `asyncio` (~5ms) only matters for infected-aio actors yet gets imported by every cold `import tractor` via module-lvl `.to_asyncio` imports in the debug-REPL + spawn-entry mods. Deats, - `devx.debug._trace`/`._tty_lock`: mv `import asyncio` under `TYPE_CHECKING` + fn-local it at the two `asyncio.current_task()` call-sites; fn-local the `run_trio_task_in_future` imports in the infected-aio-only branches. - `spawn._entry`: fn-local `run_as_asyncio_guest` inside the `infect_asyncio=True` branches of `_mp_main()`/ `_trio_main()`. - `tractor/__init__.py`: add a PEP-562 module `__getattr__` lazy-loading `.to_asyncio` on first attr-access so the public `tractor.to_asyncio.` API (e.g. `LinkedTaskChannel` annots in `test_child_manages_service_nursery.py` + downstream users) keeps working unchanged. Prompt-IO: ai/prompt-io/claude/20260702T155626Z_65bf9df5_prompt_io.md (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- tractor/__init__.py | 23 +++++++++++++++++++++++ tractor/devx/debug/_trace.py | 11 +++++++++-- tractor/devx/debug/_tty_lock.py | 9 +++++++-- tractor/spawn/_entry.py | 8 +++++++- 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/tractor/__init__.py b/tractor/__init__.py index 287ac940..42df61e8 100644 --- a/tractor/__init__.py +++ b/tractor/__init__.py @@ -74,3 +74,26 @@ from .discovery._registry import ( Arbiter as Arbiter, ) # from . import hilevel as hilevel + + +def __getattr__(name: str): + ''' + PEP 562 lazy sub-module loading, presently only for + `.to_asyncio` which (transitively) imports `asyncio` + itself: a non-trivial multi-ms chunk of the eager + `import tractor` cost (gh #470) unneeded by + `trio`-only apps. + + Any `tractor.to_asyncio.` access (or a + `from tractor import to_asyncio`) still works, the + sub-mod is simply imported on first-access instead + of at pkg-import time. + + ''' + if name == 'to_asyncio': + from importlib import import_module + return import_module('.to_asyncio', __name__) + + raise AttributeError( + f'module {__name__!r} has no attribute {name!r}' + ) diff --git a/tractor/devx/debug/_trace.py b/tractor/devx/debug/_trace.py index 30c2b3c1..5bac9d6c 100644 --- a/tractor/devx/debug/_trace.py +++ b/tractor/devx/debug/_trace.py @@ -24,7 +24,6 @@ mult-process support within a single actor tree. ''' from __future__ import annotations -import asyncio import bdb from contextlib import ( AbstractContextManager, @@ -53,7 +52,6 @@ from trio import ( ) import tractor from tractor.log import get_logger -from tractor.to_asyncio import run_trio_task_in_future from tractor._context import Context from tractor.runtime import _state from tractor._exceptions import ( @@ -85,6 +83,10 @@ from ..pformat import ( ) if TYPE_CHECKING: + # NOTE, `asyncio` (and `.to_asyncio`) are + # lazy-imported at their use-sites to keep them off + # the eager `import tractor` path (gh #470). + import asyncio from trio.lowlevel import Task from threading import Thread from tractor.runtime._runtime import ( @@ -164,6 +166,7 @@ async def _pause( 'An `asyncio` task should not be calling this!?' ) from rte else: + import asyncio task = asyncio.current_task() if debug_func is not None: @@ -946,6 +949,7 @@ def pause_from_sync( asyncio_task: asyncio.Task|None = None if is_infected_aio: + import asyncio asyncio_task = asyncio.current_task() # TODO: we could also check for a non-`.to_thread` context @@ -1059,6 +1063,9 @@ def pause_from_sync( greenback: ModuleType = maybe_import_greenback() if greenback.has_portal(): + from tractor.to_asyncio import ( + run_trio_task_in_future, + ) DebugStatus.shield_sigint() fute: asyncio.Future = run_trio_task_in_future( partial( diff --git a/tractor/devx/debug/_tty_lock.py b/tractor/devx/debug/_tty_lock.py index 0016b61e..0b63798d 100644 --- a/tractor/devx/debug/_tty_lock.py +++ b/tractor/devx/debug/_tty_lock.py @@ -20,7 +20,6 @@ Root-actor TTY mutex-locking machinery. ''' from __future__ import annotations -import asyncio from contextlib import ( AbstractContextManager, asynccontextmanager as acm, @@ -52,7 +51,6 @@ from trio import ( TaskStatus, ) import tractor -from tractor.to_asyncio import run_trio_task_in_future from tractor.log import get_logger from tractor._context import Context from tractor.runtime import _state @@ -66,6 +64,10 @@ from tractor.runtime._state import ( ) if TYPE_CHECKING: + # NOTE, `asyncio` (and `.to_asyncio`) are + # lazy-imported at their use-sites to keep them off + # the eager `import tractor` path (gh #470). + import asyncio from trio.lowlevel import Task from threading import Thread from tractor.ipc import ( @@ -910,6 +912,9 @@ class DebugStatus: async def _set_repl_release(): repl_release.set() + from tractor.to_asyncio import ( + run_trio_task_in_future, + ) fute: asyncio.Future = run_trio_task_in_future( _set_repl_release ) diff --git a/tractor/spawn/_entry.py b/tractor/spawn/_entry.py index 3143d9c4..83f1d3ab 100644 --- a/tractor/spawn/_entry.py +++ b/tractor/spawn/_entry.py @@ -38,7 +38,11 @@ from ..devx import ( pformat, ) # from ..msg import pretty_struct -from ..to_asyncio import run_as_asyncio_guest +# +# NOTE, `.to_asyncio` (and thus `asyncio` itself) is +# lazy-imported at the `infect_asyncio=True` use-sites +# below to keep it off the eager `import tractor` path +# (gh #470). from ..discovery._addr import UnwrappedAddress from ..runtime._runtime import ( async_main, @@ -96,6 +100,7 @@ def _mp_main( ) try: if infect_asyncio: + from ..to_asyncio import run_as_asyncio_guest actor._infected_aio = True run_as_asyncio_guest(trio_main) else: @@ -156,6 +161,7 @@ def _trio_main( ) try: if infect_asyncio: + from ..to_asyncio import run_as_asyncio_guest actor._infected_aio = True run_as_asyncio_guest(trio_main) else: