Compare commits

..

No commits in common. "fc5e80fea5e509ecc7ca416cf829f38d83aee40f" and "532a9834f30c41b6215b9e9a92a2e31d9feef728" have entirely different histories.

13 changed files with 108 additions and 155 deletions

View File

@ -148,13 +148,9 @@ jobs:
- name: Run tests - name: Run tests
run: > run: >
uv run uv run
pytest pytest tests/ -rsx
tests/
-rsx
--spawn-backend=${{ matrix.spawn_backend }} --spawn-backend=${{ matrix.spawn_backend }}
--tpt-proto=${{ matrix.tpt_proto }} --tpt-proto=${{ matrix.tpt_proto }}
--capture=fd
# ^XXX^ can't work with --spawn-method=main_thread_forkserver
# XXX legacy NOTE XXX # XXX legacy NOTE XXX
# #

View File

@ -62,7 +62,6 @@ dev = [
{include-group = 'devx'}, {include-group = 'devx'},
{include-group = 'testing'}, {include-group = 'testing'},
{include-group = 'repl'}, {include-group = 'repl'},
{include-group = 'sync_pause'},
] ]
devx = [ devx = [
# `tractor.devx` tooling # `tractor.devx` tooling

View File

@ -4,7 +4,6 @@
''' '''
from __future__ import annotations from __future__ import annotations
import platform import platform
import os
import signal import signal
import time import time
from typing import ( from typing import (
@ -57,7 +56,6 @@ type PexpectSpawner = Callable[
@pytest.fixture @pytest.fixture
def spawn( def spawn(
start_method: str, start_method: str,
loglevel: str,
testdir: pytest.Pytester, testdir: pytest.Pytester,
reg_addr: tuple[str, int], reg_addr: tuple[str, int],
@ -69,12 +67,11 @@ def spawn(
''' '''
supported_spawners: set[str] = { supported_spawners: set[str] = {
'trio', 'trio',
# `examples/debugging/<script>.py` picks up the spawn # ?TODO, other spawners that will work?
# backend via the `TRACTOR_SPAWN_METHOD` env-var which # - [ ] need to pass `start_method={spawner}` to underlying
# is honored inside `tractor._root.open_root_actor()`, # `examples/debugging/<script>.py` somehow?
# so no per-script edits are required. # 'main_thread_forkserver',
'main_thread_forkserver', # 'subint_forkserver',
'subint_forkserver',
} }
if start_method not in supported_spawners: if start_method not in supported_spawners:
pytest.skip( pytest.skip(
@ -91,35 +88,12 @@ def spawn(
https://docs.python.org/3/using/cmdline.html#using-on-controlling-color https://docs.python.org/3/using/cmdline.html#using-on-controlling-color
''' '''
import os
# disable colored tbs # disable colored tbs
os.environ['PYTHON_COLORS'] = '0' os.environ['PYTHON_COLORS'] = '0'
# disable all ANSI color output # disable all ANSI color output
# os.environ['NO_COLOR'] = '1' # os.environ['NO_COLOR'] = '1'
def set_spawn_method():
'''
Drive the actor-spawn backend inside the spawned
`examples/debugging/<script>.py` subproc via env-var
(consumed by `tractor._root.open_root_actor()`),
without requiring per-script CLI plumbing.
'''
os.environ['TRACTOR_SPAWN_METHOD'] = start_method
def set_loglevel():
'''
Forward the test-suite parametrized `loglevel` into the
spawned `examples/debugging/<script>.py` subproc via
env-var (consumed by `tractor._root.open_root_actor()`),
so console verbosity can be cranked or silenced from
the test harness without per-script edits.
'''
if loglevel:
os.environ['TRACTOR_LOGLEVEL'] = loglevel
else:
os.environ.pop('TRACTOR_LOGLEVEL', None)
spawned: PexpectSpawner|None = None spawned: PexpectSpawner|None = None
def _spawn( def _spawn(
@ -129,8 +103,6 @@ def spawn(
) -> pty_spawn.spawn: ) -> pty_spawn.spawn:
nonlocal spawned nonlocal spawned
unset_colors() unset_colors()
set_spawn_method()
set_loglevel()
spawned = testdir.spawn( spawned = testdir.spawn(
cmd=mk_cmd( cmd=mk_cmd(
cmd, cmd,
@ -174,14 +146,6 @@ def spawn(
if ptyproc.isalive(): if ptyproc.isalive():
ptyproc.kill(signal.SIGKILL) ptyproc.kill(signal.SIGKILL)
# Scope our env-var mutations to this single fixture invocation
# — both `TRACTOR_SPAWN_METHOD` and `TRACTOR_LOGLEVEL` are
# honored by `tractor._root.open_root_actor()` so leaking them
# past this test could inadvertently re-route a later in-process
# tractor test's spawn-backend / loglevel.
os.environ.pop('TRACTOR_SPAWN_METHOD', None)
os.environ.pop('TRACTOR_LOGLEVEL', None)
# TODO? ensure we've cleaned up any UDS-paths? # TODO? ensure we've cleaned up any UDS-paths?
# breakpoint() # breakpoint()

View File

@ -841,7 +841,7 @@ def test_multi_nested_subactors_error_through_nurseries(
) )
# @pytest.mark.timeout(15) @pytest.mark.timeout(15)
@has_nested_actors @has_nested_actors
def test_root_nursery_cancels_before_child_releases_tty_lock( def test_root_nursery_cancels_before_child_releases_tty_lock(
spawn, spawn,

View File

@ -535,13 +535,13 @@ async def kill_transport(
# At timeout the plugin hard-kills the pytest process — that's # At timeout the plugin hard-kills the pytest process — that's
# the intended behavior here; the alternative is an unattended # the intended behavior here; the alternative is an unattended
# suite run that never returns. # suite run that never returns.
# @pytest.mark.timeout( @pytest.mark.timeout(
# 30, 30,
# # NOTE should be a 2.1s happy path. # NOTE should be a 2.1s happy path.
# # XXX for `main_thread_forkserver` this is SUPER SENSITIVE # XXX for `main_thread_forkserver` this is SUPER SENSITIVE
# # so keep it higher to avoid flaky runs.. # so keep it higher to avoid flaky runs..
# method='thread', method='thread',
# ) )
@pytest.mark.skipon_spawn_backend( @pytest.mark.skipon_spawn_backend(
'subint', 'subint',
# 'main_thread_forkserver', # 'main_thread_forkserver',

View File

@ -182,7 +182,7 @@ async def run_fork_in_non_trio_thread(
# `subint_sigint_starvation_issue.md`. Each test also has an # `subint_sigint_starvation_issue.md`. Each test also has an
# inner `trio.fail_after()` so assertion failures fire fast # inner `trio.fail_after()` so assertion failures fire fast
# under normal conditions. # under normal conditions.
# @pytest.mark.timeout(30, method='thread') @pytest.mark.timeout(30, method='thread')
def test_fork_from_worker_thread_via_trio( def test_fork_from_worker_thread_via_trio(
) -> None: ) -> None:
''' '''

View File

@ -179,10 +179,10 @@ def test_subint_happy_teardown(
# `subint_sigint_starvation_issue.md` GIL-starvation flavor, # `subint_sigint_starvation_issue.md` GIL-starvation flavor,
# so `method='thread'` keeps us safe in case ordering or # so `method='thread'` keeps us safe in case ordering or
# load shifts the failure mode. # load shifts the failure mode.
# @pytest.mark.timeout( @pytest.mark.timeout(
# 3, # NOTE never passes pre-3.14+ subints support. 3, # NOTE never passes pre-3.14+ subints support.
# method='thread', method='thread',
# ) )
def test_subint_non_checkpointing_child( def test_subint_non_checkpointing_child(
reg_addr: tuple[str, int|str], reg_addr: tuple[str, int|str],
) -> None: ) -> None:

View File

@ -430,10 +430,10 @@ async def inf_streamer(
print('streamer exited .open_streamer() block') print('streamer exited .open_streamer() block')
# @pytest.mark.timeout( @pytest.mark.timeout(
# 6, 6,
# method='signal', method='signal',
# ) )
def test_local_task_fanout_from_stream( def test_local_task_fanout_from_stream(
reg_addr: tuple, reg_addr: tuple,
debug_mode: bool, debug_mode: bool,

View File

@ -458,10 +458,10 @@ async def spawn_and_error(
# `test_nested_multierrors`. See # `test_nested_multierrors`. See
# `ai/conc-anal/subint_forkserver_test_cancellation_leak_issue.md` # `ai/conc-anal/subint_forkserver_test_cancellation_leak_issue.md`
# / #449 for the post-mortem. # / #449 for the post-mortem.
# @pytest.mark.timeout( @pytest.mark.timeout(
# 10, 10,
# method='thread', method='thread',
# ) )
@tractor_test @tractor_test
async def test_nested_multierrors( async def test_nested_multierrors(
reg_addr: tuple, reg_addr: tuple,

View File

@ -241,7 +241,6 @@ async def open_root_actor(
f'_registry_addrs: {registry_addrs!r}\n' f'_registry_addrs: {registry_addrs!r}\n'
) )
# debug.mk_pdb().set_trace()
async with maybe_block_bp( async with maybe_block_bp(
debug_mode=debug_mode, debug_mode=debug_mode,
maybe_enable_greenback=maybe_enable_greenback, maybe_enable_greenback=maybe_enable_greenback,
@ -285,75 +284,6 @@ async def open_root_actor(
) )
enable_modules.extend(rpc_module_paths) enable_modules.extend(rpc_module_paths)
# `TRACTOR_LOGLEVEL` env-var wins over any caller-passed
# `loglevel` so devs/test-runs can crank (or silence)
# console verbosity without touching application code.
env_ll_report: str = ''
if env_ll := os.environ.get('TRACTOR_LOGLEVEL'):
loglevel = env_ll
env_ll_report: str = (
f'Detected env-var setting,\n'
f'TRACTOR_LOGLEVEL={env_ll!r}\n'
f'\n'
f'Setting console loglevel per,\n'
f'loglevel={loglevel!r}\n'
)
if (
loglevel
and
loglevel.upper() != env_ll.upper()
):
env_ll_report += (
f'\n'
f'NOTE env-var OVERRIDES caller-passed,\n'
f'loglevel={loglevel!r}\n'
)
loglevel: str = (
loglevel
or
log._default_loglevel
)
loglevel: str = loglevel.upper()
assert loglevel
_log = log.get_console_log(
level=loglevel,
name='tractor',
logger=logger,
)
assert _log
if env_ll_report:
_log.info(env_ll_report)
# `TRACTOR_SPAWN_METHOD` env-var wins over any caller-passed
# `start_method` so devs/test-runs can swap the actor spawn
# backend without touching application code (e.g. driving
# the `examples/debugging/<script>.py` suite under each
# backend from `tests/devx/conftest.py::spawn`).
if env_sm := os.environ.get('TRACTOR_SPAWN_METHOD'):
start_method: str = env_sm
env_sm_report: str = (
f'Detected env-var setting,\n'
f'TRACTOR_SPAWN_METHOD={env_sm!r}\n'
f'\n'
f'Setting spawn backend as,\n'
f'start_method={env_sm!r}\n'
)
if (
start_method
and
start_method != env_sm
):
_log.warning(
env_sm_report
+
f'NOTE env-var OVERRIDES caller-passed,\n'
f'`start_method={start_method!r}`\n'
)
else:
_log.info(env_sm_report)
if start_method is not None: if start_method is not None:
_spawn.try_set_start_method(start_method) _spawn.try_set_start_method(start_method)
@ -370,6 +300,12 @@ async def open_root_actor(
wrap_address(uw_addr) wrap_address(uw_addr)
for uw_addr in uw_reg_addrs for uw_addr in uw_reg_addrs
] ]
loglevel: str = (
loglevel
or
log._default_loglevel
)
loglevel: str = loglevel.upper()
# Debug-mode is currently only supported for backends whose # Debug-mode is currently only supported for backends whose
# subactor root runtime is trio-native (so `tractor.devx. # subactor root runtime is trio-native (so `tractor.devx.
@ -405,6 +341,13 @@ async def open_root_actor(
f'{_spawn._spawn_method!r}.' f'{_spawn._spawn_method!r}.'
) )
assert loglevel
_log = log.get_console_log(
level=loglevel,
name='tractor',
)
assert _log
# TODO: factor this into `.devx._stackscope`!! # TODO: factor this into `.devx._stackscope`!!
if ( if (
debug_mode debug_mode

View File

@ -346,6 +346,7 @@ from __future__ import annotations
import errno import errno
import os import os
import signal import signal
import sys
import threading import threading
from functools import partial from functools import partial
from typing import ( from typing import (
@ -369,6 +370,7 @@ from ._spawn import (
cancel_on_completion, cancel_on_completion,
soft_kill, soft_kill,
) )
from ._subint import _has_subints
if TYPE_CHECKING: if TYPE_CHECKING:
from tractor.discovery._addr import UnwrappedAddress from tractor.discovery._addr import UnwrappedAddress
@ -830,6 +832,13 @@ async def main_thread_forkserver_proc(
thread instead of `trio.lowlevel.open_process()`. thread instead of `trio.lowlevel.open_process()`.
''' '''
if not _has_subints:
raise RuntimeError(
f'The {"main_thread_forkserver"!r} spawn backend '
f'requires Python 3.14+.\n'
f'Current runtime: {sys.version}'
)
# Backend-scoped config pulled from `proc_kwargs`. Using # Backend-scoped config pulled from `proc_kwargs`. Using
# `proc_kwargs` (vs a first-class kwarg on this function) # `proc_kwargs` (vs a first-class kwarg on this function)
# matches how other backends expose per-spawn tuning # matches how other backends expose per-spawn tuning

View File

@ -135,15 +135,13 @@ def try_set_start_method(
case 'mp_spawn': case 'mp_spawn':
_ctx = mp.get_context('spawn') _ctx = mp.get_context('spawn')
case ( case 'trio':
'trio'
| 'main_thread_forkserver'
):
_ctx = None _ctx = None
case ( case (
'subint' 'subint'
| 'subint_fork' | 'subint_fork'
| 'main_thread_forkserver'
| 'subint_forkserver' | 'subint_forkserver'
): ):
# All subint-family backends need no `mp.context`; # All subint-family backends need no `mp.context`;

68
uv.lock
View File

@ -682,7 +682,6 @@ dependencies = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "greenback", marker = "python_full_version < '3.14'" },
{ name = "pexpect" }, { name = "pexpect" },
{ name = "prompt-toolkit" }, { name = "prompt-toolkit" },
{ name = "psutil" }, { name = "psutil" },
@ -737,7 +736,6 @@ requires-dist = [
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "greenback", marker = "python_full_version == '3.13.*'", specifier = ">=1.2.1,<2" },
{ name = "pexpect", specifier = ">=4.9.0,<5" }, { name = "pexpect", specifier = ">=4.9.0,<5" },
{ name = "prompt-toolkit", specifier = ">=3.0.50" }, { name = "prompt-toolkit", specifier = ">=3.0.50" },
{ name = "psutil", specifier = ">=7.0.0" }, { name = "psutil", specifier = ">=7.0.0" },
@ -746,7 +744,7 @@ dev = [
{ name = "pytest-timeout", specifier = ">=2.3" }, { name = "pytest-timeout", specifier = ">=2.3" },
{ name = "stackscope", specifier = ">=0.2.2,<0.3" }, { name = "stackscope", specifier = ">=0.2.2,<0.3" },
{ name = "typing-extensions", specifier = ">=4.14.1" }, { name = "typing-extensions", specifier = ">=4.14.1" },
{ name = "xonsh", specifier = ">=0.23.0" }, { name = "xonsh", editable = "../xonsh" },
] ]
devx = [ devx = [
{ name = "stackscope", specifier = ">=0.2.2,<0.3" }, { name = "stackscope", specifier = ">=0.2.2,<0.3" },
@ -758,7 +756,7 @@ repl = [
{ name = "prompt-toolkit", specifier = ">=3.0.50" }, { name = "prompt-toolkit", specifier = ">=3.0.50" },
{ name = "psutil", specifier = ">=7.0.0" }, { name = "psutil", specifier = ">=7.0.0" },
{ name = "pyperclip", specifier = ">=1.9.0" }, { name = "pyperclip", specifier = ">=1.9.0" },
{ name = "xonsh", specifier = ">=0.23.0" }, { name = "xonsh", editable = "../xonsh" },
] ]
subints = [{ name = "msgspec", marker = "python_full_version >= '3.14'", specifier = ">=0.21.0" }] subints = [{ name = "msgspec", marker = "python_full_version >= '3.14'", specifier = ">=0.21.0" }]
sync-pause = [{ name = "greenback", marker = "python_full_version == '3.13.*'", specifier = ">=1.2.1,<2" }] sync-pause = [{ name = "greenback", marker = "python_full_version == '3.13.*'", specifier = ">=1.2.1,<2" }]
@ -872,15 +870,61 @@ wheels = [
[[package]] [[package]]
name = "xonsh" name = "xonsh"
version = "0.23.2" source = { editable = "../xonsh" }
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/60/e5/2dfa99e21a8118bed0e73ed50e91962fdad01b900e23497064e8810b03b5/xonsh-0.23.2.tar.gz", hash = "sha256:633608c8292938af0f242f05326cc2912f25fa72bd808824ab0534a6df304402", size = 1030659, upload-time = "2026-04-26T19:28:40.744Z" } [package.metadata]
wheels = [ requires-dist = [
{ url = "https://files.pythonhosted.org/packages/53/0d/bf7869dd57b40888ea1da8fc88f70d8e94ec2f8ee236ea4c22a757593235/xonsh-0.23.2-py311-none-any.whl", hash = "sha256:a38dd84e23e97fc42e0156c80024b3449474dfcbb6c3a344bd38c45a2b2de44d", size = 756215, upload-time = "2026-04-26T19:28:38.875Z" }, { name = "click", marker = "extra == 'full'" },
{ url = "https://files.pythonhosted.org/packages/f7/9f/b1bb0c15bf2120469c94b062f4b854588370ab94c7a1679c84ff646bf50b/xonsh-0.23.2-py312-none-any.whl", hash = "sha256:190a348fa19774de8e697af5f44c9adb95aca687fa475b31dda23d1a3462a3c6", size = 756224, upload-time = "2026-04-26T19:28:39.17Z" }, { name = "coverage", marker = "extra == 'test'", specifier = ">=5.3.1" },
{ url = "https://files.pythonhosted.org/packages/83/23/8e037579ac86d8f266b4116338f902eab04175b88574a6438ee739dd3084/xonsh-0.23.2-py313-none-any.whl", hash = "sha256:4ebbf42a94f505d25694f154556ca0caa149a3f59870ec850bd13ad8df519dce", size = 756728, upload-time = "2026-04-26T19:28:39.493Z" }, { name = "distro", marker = "sys_platform == 'linux' and extra == 'full'" },
{ url = "https://files.pythonhosted.org/packages/05/ec/090300d9c5f14f58b5a684302f43535457f733a62f11673aa3ac38460717/xonsh-0.23.2-py314-none-any.whl", hash = "sha256:5efcd0f6db8f9f1dace256de2c04c3c044f2d86b48434187c43a69d602283a9e", size = 756767, upload-time = "2026-04-26T19:28:37.218Z" }, { name = "distro", marker = "extra == 'linux'" },
{ name = "furo", marker = "extra == 'doc'" },
{ name = "gnureadline", marker = "sys_platform == 'darwin' and extra == 'full'" },
{ name = "gnureadline", marker = "extra == 'mac'" },
{ name = "matplotlib", marker = "extra == 'doc'" },
{ name = "myst-parser", marker = "extra == 'doc'" },
{ name = "numpydoc", marker = "extra == 'doc'" },
{ name = "pre-commit", marker = "extra == 'dev'" },
{ name = "prompt-toolkit", marker = "extra == 'bestshell'", specifier = ">=3.0.29" },
{ name = "prompt-toolkit", marker = "extra == 'ptk'", specifier = ">=3.0.29" },
{ name = "prompt-toolkit", marker = "extra == 'test'", specifier = ">=3.0.29" },
{ name = "psutil", marker = "extra == 'doc'" },
{ name = "pygments", marker = "extra == 'bestshell'", specifier = ">=2.2" },
{ name = "pygments", marker = "extra == 'pygments'", specifier = ">=2.2" },
{ name = "pygments", marker = "extra == 'test'", specifier = ">=2.2" },
{ name = "pyperclip", marker = "extra == 'ptk'" },
{ name = "pyte", marker = "extra == 'test'", specifier = ">=0.8.0" },
{ name = "pytest", marker = "extra == 'test'", specifier = ">=7" },
{ name = "pytest-cov", marker = "extra == 'test'" },
{ name = "pytest-mock", marker = "extra == 'test'" },
{ name = "pytest-rerunfailures", marker = "extra == 'test'" },
{ name = "pytest-subprocess", marker = "extra == 'test'" },
{ name = "pytest-timeout", marker = "extra == 'test'" },
{ name = "pyzmq", marker = "extra == 'doc'" },
{ name = "re-ver", marker = "extra == 'dev'" },
{ name = "requests", marker = "extra == 'test'" },
{ name = "restructuredtext-lint", marker = "extra == 'test'" },
{ name = "runthis-sphinxext", marker = "extra == 'doc'" },
{ name = "setproctitle", marker = "sys_platform == 'win32' and extra == 'full'" },
{ name = "setproctitle", marker = "extra == 'proctitle'" },
{ name = "sphinx", marker = "extra == 'doc'", specifier = ">=3.1" },
{ name = "sphinx-autobuild", marker = "extra == 'doc'" },
{ name = "sphinx-prompt", marker = "extra == 'doc'" },
{ name = "sphinx-reredirects", marker = "extra == 'doc'" },
{ name = "sphinx-sitemap", marker = "extra == 'doc'" },
{ name = "tomli", marker = "extra == 'dev'" },
{ name = "tornado", marker = "extra == 'doc'" },
{ name = "ujson", marker = "extra == 'full'" },
{ name = "virtualenv", marker = "extra == 'test'", specifier = ">=20.16.2" },
{ name = "xonsh", extras = ["bestshell"], marker = "extra == 'doc'" },
{ name = "xonsh", extras = ["bestshell"], marker = "extra == 'test'" },
{ name = "xonsh", extras = ["doc", "test"], marker = "extra == 'dev'" },
{ name = "xonsh", extras = ["ptk", "pygments"], marker = "extra == 'full'" },
] ]
provides-extras = ["ptk", "pygments", "mac", "linux", "proctitle", "full", "bestshell", "test", "dev", "doc"]
[package.metadata.requires-dev]
dev = [{ name = "xonsh", extras = ["dev"] }]
[[package]] [[package]]
name = "zipp" name = "zipp"