Add `'subint'` spawn backend scaffold (#379)

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
subint_forkserver_backend
Gud Boi 2026-04-17 12:44:37 -04:00
parent 64ddc42ad8
commit d318f1f8f4
4 changed files with 124 additions and 2 deletions

View File

@ -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 = [

View File

@ -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:
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

View File

@ -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,
}

View File

@ -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 <https://www.gnu.org/licenses/>.
'''
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'
)