From d318f1f8f4f6a056a8ca67d7f15e4e2a72507ea3 Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 17 Apr 2026 12:44:37 -0400 Subject: [PATCH] Add `'subint'` spawn backend scaffold (#379) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Land the scaffolding for a future sub-interpreter (PEP 734 `concurrent.interpreters`) actor spawn backend per issue #379. The spawn flow itself is not yet implemented; `subint_proc()` raises a placeholder `NotImplementedError` pointing at the tracking issue — this commit only wires up the registry, the py-version gate, and the harness. Deats, - bump `pyproject.toml` `requires-python` to `>=3.12, <3.15` and list the `3.14` classifier — the new stdlib `concurrent.interpreters` module only ships on 3.14 - extend `SpawnMethodKey = Literal[..., 'subint']` - `try_set_start_method('subint')` grows a new `match` arm that feature-detects the stdlib module and raises `RuntimeError` with a clear banner on py<3.14 - `_methods` registers the new `subint_proc()` via the same bottom-of-module late-import pattern used for `._trio` / `._mp` Also, - new `tractor/spawn/_subint.py` — top-level `try: from concurrent import interpreters` guards `_has_subints: bool`; `subint_proc()` signature mirrors `trio_proc`/`mp_proc` so the Phase B.2 impl can drop in without touching the registry - re-add `import sys` to `_spawn.py` (needed for the py-version msg in the gate-error) - `_testing.pytest.pytest_configure` wraps `try_set_start_method()` in a `pytest.UsageError` handler so `--spawn-backend=subint` on py<3.14 prints a clean banner instead of a traceback (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- pyproject.toml | 3 +- tractor/_testing/pytest.py | 8 ++- tractor/spawn/_spawn.py | 15 ++++++ tractor/spawn/_subint.py | 100 +++++++++++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 tractor/spawn/_subint.py diff --git a/pyproject.toml b/pyproject.toml index 4bc78323..ef842845 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ name = "tractor" version = "0.1.0a6dev0" description = 'structured concurrent `trio`-"actors"' authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }] -requires-python = ">=3.12, <3.14" +requires-python = ">=3.12, <3.15" readme = "docs/README.rst" license = "AGPL-3.0-or-later" keywords = [ @@ -31,6 +31,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: System :: Distributed Computing", ] dependencies = [ diff --git a/tractor/_testing/pytest.py b/tractor/_testing/pytest.py index 1d803c9e..c33406ff 100644 --- a/tractor/_testing/pytest.py +++ b/tractor/_testing/pytest.py @@ -227,7 +227,13 @@ def pytest_addoption( def pytest_configure(config): backend = config.option.spawn_backend from tractor.spawn._spawn import try_set_start_method - try_set_start_method(backend) + try: + try_set_start_method(backend) + except RuntimeError as err: + # e.g. `--spawn-backend=subint` on Python < 3.14 — turn the + # runtime gate error into a clean pytest usage error so the + # suite exits with a helpful banner instead of a traceback. + raise pytest.UsageError(str(err)) from err # register custom marks to avoid warnings see, # https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers diff --git a/tractor/spawn/_spawn.py b/tractor/spawn/_spawn.py index f9cc0a51..09368f73 100644 --- a/tractor/spawn/_spawn.py +++ b/tractor/spawn/_spawn.py @@ -22,6 +22,7 @@ over multiple backends. from __future__ import annotations import multiprocessing as mp import platform +import sys from typing import ( Any, Awaitable, @@ -61,6 +62,7 @@ SpawnMethodKey = Literal[ 'trio', # supported on all platforms 'mp_spawn', 'mp_forkserver', # posix only + 'subint', # py3.14+ via `concurrent.interpreters` (PEP 734) ] _spawn_method: SpawnMethodKey = 'trio' @@ -113,6 +115,17 @@ def try_set_start_method( case 'trio': _ctx = None + case 'subint': + # subints need no `mp.context`; feature-gate 3.14+ + from ._subint import _has_subints + if not _has_subints: + raise RuntimeError( + f'Spawn method {key!r} requires Python 3.14+ ' + f'(stdlib `concurrent.interpreters`, PEP 734).\n' + f'Current runtime: {sys.version}' + ) + _ctx = None + case _: raise ValueError( f'Spawn method `{key}` is invalid!\n' @@ -437,6 +450,7 @@ async def new_proc( # `hard_kill`/`proc_waiter` from this module. from ._trio import trio_proc from ._mp import mp_proc +from ._subint import subint_proc # proc spawning backend target map @@ -444,4 +458,5 @@ _methods: dict[SpawnMethodKey, Callable] = { 'trio': trio_proc, 'mp_spawn': mp_proc, 'mp_forkserver': mp_proc, + 'subint': subint_proc, } diff --git a/tractor/spawn/_subint.py b/tractor/spawn/_subint.py new file mode 100644 index 00000000..09793496 --- /dev/null +++ b/tractor/spawn/_subint.py @@ -0,0 +1,100 @@ +# tractor: structured concurrent "actors". +# Copyright 2018-eternity Tyler Goodlet. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +''' +Sub-interpreter (`subint`) actor spawning backend. + +Spawns each sub-actor as a CPython PEP 734 sub-interpreter +(`concurrent.interpreters.Interpreter`) — same-process state +isolation with faster start-up than an OS subproc, while +preserving tractor's IPC-based actor boundaries. + +Availability +------------ +Requires Python 3.14+ for the stdlib `concurrent.interpreters` +module. On older runtimes the module still imports (so the +registry stays introspectable) but `subint_proc()` raises. + +Status +------ +SCAFFOLDING STUB — `subint_proc()` is **not yet implemented**. +The real impl lands in Phase B.2 (see issue #379). + +''' +from __future__ import annotations +import sys +from typing import ( + Any, + TYPE_CHECKING, +) + +import trio +from trio import TaskStatus + + +try: + from concurrent import interpreters as _interpreters # type: ignore + _has_subints: bool = True +except ImportError: + _interpreters = None # type: ignore + _has_subints: bool = False + + +if TYPE_CHECKING: + from tractor.discovery._addr import UnwrappedAddress + from tractor.runtime._portal import Portal + from tractor.runtime._runtime import Actor + from tractor.runtime._supervise import ActorNursery + + +async def subint_proc( + name: str, + actor_nursery: ActorNursery, + subactor: Actor, + errors: dict[tuple[str, str], Exception], + + # passed through to actor main + bind_addrs: list[UnwrappedAddress], + parent_addr: UnwrappedAddress, + _runtime_vars: dict[str, Any], # serialized and sent to _child + *, + infect_asyncio: bool = False, + task_status: TaskStatus[Portal] = trio.TASK_STATUS_IGNORED, + proc_kwargs: dict[str, any] = {} + +) -> None: + ''' + Create a new sub-actor hosted inside a PEP 734 + sub-interpreter running in a dedicated OS thread, + reusing tractor's existing UDS/TCP IPC handshake + for parent<->child channel setup. + + NOT YET IMPLEMENTED — placeholder stub pending the + Phase B.2 impl. + + ''' + if not _has_subints: + raise RuntimeError( + f'The {"subint"!r} spawn backend requires Python 3.14+ ' + f'(stdlib `concurrent.interpreters`, PEP 734).\n' + f'Current runtime: {sys.version}' + ) + + raise NotImplementedError( + 'The `subint` spawn backend scaffolding is in place but ' + 'the spawn-flow itself is not yet implemented.\n' + 'Tracking: https://github.com/goodboy/tractor/issues/379' + )