Python 3.13 support #18

Merged
goodboy merged 29 commits from py313_support into main 2025-03-27 19:50:43 +00:00
40 changed files with 524 additions and 250 deletions

View File

@ -62,7 +62,9 @@ async def recv_and_spawn_net_killers(
await ctx.started() await ctx.started()
async with ( async with (
ctx.open_stream() as stream, ctx.open_stream() as stream,
trio.open_nursery() as n, trio.open_nursery(
strict_exception_groups=False,
) as tn,
): ):
async for i in stream: async for i in stream:
print(f'child echoing {i}') print(f'child echoing {i}')
@ -77,11 +79,11 @@ async def recv_and_spawn_net_killers(
i >= break_ipc_after i >= break_ipc_after
): ):
broke_ipc = True broke_ipc = True
n.start_soon( tn.start_soon(
iter_ipc_stream, iter_ipc_stream,
stream, stream,
) )
n.start_soon( tn.start_soon(
partial( partial(
break_ipc_then_error, break_ipc_then_error,
stream=stream, stream=stream,

View File

@ -21,11 +21,13 @@ async def name_error():
async def main(): async def main():
"""Test breakpoint in a streaming actor. '''
""" Test breakpoint in a streaming actor.
'''
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
# loglevel='cancel', loglevel='cancel',
# loglevel='devx', # loglevel='devx',
) as n: ) as n:

View File

@ -40,7 +40,7 @@ async def main():
""" """
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
# loglevel='cancel', loglevel='devx',
) as n: ) as n:
# spawn both actors # spawn both actors

View File

@ -32,8 +32,7 @@ async def main() -> None:
f'$PYTHONOBREAKPOINT: {pybp_var!r}\n' f'$PYTHONOBREAKPOINT: {pybp_var!r}\n'
f'`sys.breakpointhook`: {pybp_hook!r}\n' f'`sys.breakpointhook`: {pybp_hook!r}\n'
) )
breakpoint() breakpoint() # first bp, tractor hook set.
pass # first bp, tractor hook set.
# XXX AFTER EXIT (of actor-runtime) verify the hook is unset.. # XXX AFTER EXIT (of actor-runtime) verify the hook is unset..
# #
@ -43,8 +42,7 @@ async def main() -> None:
assert sys.breakpointhook assert sys.breakpointhook
# now ensure a regular builtin pause still works # now ensure a regular builtin pause still works
breakpoint() breakpoint() # last bp, stdlib hook restored
pass # last bp, stdlib hook restored
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -91,7 +91,7 @@ async def main() -> list[int]:
an: ActorNursery an: ActorNursery
async with tractor.open_nursery( async with tractor.open_nursery(
loglevel='cancel', loglevel='cancel',
debug_mode=True, # debug_mode=True,
) as an: ) as an:
seed = int(1e3) seed = int(1e3)

View File

@ -3,20 +3,18 @@ import trio
import tractor import tractor
async def sleepy_jane(): async def sleepy_jane() -> None:
uid = tractor.current_actor().uid uid: tuple = tractor.current_actor().uid
print(f'Yo i am actor {uid}') print(f'Yo i am actor {uid}')
await trio.sleep_forever() await trio.sleep_forever()
async def main(): async def main():
''' '''
Spawn a flat actor cluster, with one process per Spawn a flat actor cluster, with one process per detected core.
detected core.
''' '''
portal_map: dict[str, tractor.Portal] portal_map: dict[str, tractor.Portal]
results: dict[str, str]
# look at this hip new syntax! # look at this hip new syntax!
async with ( async with (
@ -25,11 +23,16 @@ async def main():
modules=[__name__] modules=[__name__]
) as portal_map, ) as portal_map,
trio.open_nursery() as n, trio.open_nursery(
strict_exception_groups=False,
) as tn,
): ):
for (name, portal) in portal_map.items(): for (name, portal) in portal_map.items():
n.start_soon(portal.run, sleepy_jane) tn.start_soon(
portal.run,
sleepy_jane,
)
await trio.sleep(0.5) await trio.sleep(0.5)
@ -41,4 +44,4 @@ if __name__ == '__main__':
try: try:
trio.run(main) trio.run(main)
except KeyboardInterrupt: except KeyboardInterrupt:
pass print('trio cancelled by KBI')

View File

@ -37,16 +37,14 @@ dependencies = [
# https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/#id5 # https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/#id5
# TODO, for 3.13 we must go go `0.27` which means we have to # TODO, for 3.13 we must go go `0.27` which means we have to
# disable strict egs or port to handling them internally! # disable strict egs or port to handling them internally!
# trio='^0.27' "trio>0.27",
"trio>=0.24,<0.25",
"tricycle>=0.4.1,<0.5", "tricycle>=0.4.1,<0.5",
"wrapt>=1.16.0,<2", "wrapt>=1.16.0,<2",
"colorlog>=6.8.2,<7", "colorlog>=6.8.2,<7",
# built-in multi-actor `pdb` REPL # built-in multi-actor `pdb` REPL
"pdbp>=1.5.0,<2", "pdbp>=1.6,<2", # windows only (from `pdbp`)
# typed IPC msging # typed IPC msging
# TODO, get back on release once 3.13 support is out! "msgspec>=0.19.0",
"msgspec",
] ]
# ------ project ------ # ------ project ------
@ -56,18 +54,14 @@ dev = [
# test suite # test suite
# TODO: maybe some of these layout choices? # TODO: maybe some of these layout choices?
# https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules # https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
"pytest>=8.2.0,<9", "pytest>=8.3.5",
"pexpect>=4.9.0,<5", "pexpect>=4.9.0,<5",
# `tractor.devx` tooling # `tractor.devx` tooling
"greenback>=1.2.1,<2", "greenback>=1.2.1,<2",
"stackscope>=0.2.2,<0.3", "stackscope>=0.2.2,<0.3",
# xonsh usage/integration (namely as @goodboy's sh of choice Bp)
"xonsh>=0.19.1",
"xontrib-vox>=0.0.1,<0.0.2",
"prompt-toolkit>=3.0.43,<4",
"xonsh-vox-tabcomplete>=0.5,<0.6",
"pyperclip>=1.9.0", "pyperclip>=1.9.0",
"prompt-toolkit>=3.0.50",
"xonsh>=0.19.2",
] ]
# TODO, add these with sane versions; were originally in # TODO, add these with sane versions; were originally in
# `requirements-docs.txt`.. # `requirements-docs.txt`..
@ -78,21 +72,39 @@ dev = [
# ------ dependency-groups ------ # ------ dependency-groups ------
# ------ dependency-groups ------
[tool.uv.sources] [tool.uv.sources]
msgspec = { git = "https://github.com/jcrist/msgspec.git" } # XXX NOTE, only for @goodboy's hacking on `pprint(sort_dicts=False)`
# for the `pp` alias..
# pdbp = { path = "../pdbp", editable = true }
# ------ tool.uv.sources ------ # ------ tool.uv.sources ------
# TODO, distributed (multi-host) extensions # TODO, distributed (multi-host) extensions
# linux kernel networking # linux kernel networking
# 'pyroute2 # 'pyroute2
# ------ tool.uv.sources ------
[tool.uv]
# XXX NOTE, prefer the sys python bc apparently the distis from
# `astral` are built in a way that breaks `pdbp`+`tabcompleter`'s
# likely due to linking against `libedit` over `readline`..
# |_https://docs.astral.sh/uv/concepts/python-versions/#managed-python-distributions
# |_https://gregoryszorc.com/docs/python-build-standalone/main/quirks.html#use-of-libedit-on-linux
#
# https://docs.astral.sh/uv/reference/settings/#python-preference
python-preference = 'system'
# ------ tool.uv ------
[tool.hatch.build.targets.sdist] [tool.hatch.build.targets.sdist]
include = ["tractor"] include = ["tractor"]
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
include = ["tractor"] include = ["tractor"]
# ------ dependency-groups ------ # ------ tool.hatch ------
[tool.towncrier] [tool.towncrier]
package = "tractor" package = "tractor"
@ -142,3 +154,5 @@ log_cli = false
# TODO: maybe some of these layout choices? # TODO: maybe some of these layout choices?
# https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules # https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
# pythonpath = "src" # pythonpath = "src"
# ------ tool.pytest ------

View File

@ -75,7 +75,10 @@ def pytest_configure(config):
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def debug_mode(request): def debug_mode(request):
return request.config.option.tractor_debug_mode debug_mode: bool = request.config.option.tractor_debug_mode
# if debug_mode:
# breakpoint()
return debug_mode
@pytest.fixture(scope='session', autouse=True) @pytest.fixture(scope='session', autouse=True)
@ -92,6 +95,12 @@ def spawn_backend(request) -> str:
return request.config.option.spawn_backend return request.config.option.spawn_backend
# @pytest.fixture(scope='function', autouse=True)
# def debug_enabled(request) -> str:
# from tractor import _state
# if _state._runtime_vars['_debug_mode']:
# breakpoint()
_ci_env: bool = os.environ.get('CI', False) _ci_env: bool = os.environ.get('CI', False)

View File

@ -309,10 +309,13 @@ def test_subactor_breakpoint(
child.expect(EOF) child.expect(EOF)
assert in_prompt_msg( assert in_prompt_msg(
child, child, [
['RemoteActorError:', 'MessagingError:',
'RemoteActorError:',
"('breakpoint_forever'", "('breakpoint_forever'",
'bdb.BdbQuit',] 'bdb.BdbQuit',
],
pause_on_false=True,
) )

View File

@ -3,7 +3,6 @@ Sketchy network blackoutz, ugly byzantine gens, puedes eschuchar la
cancelacion?.. cancelacion?..
''' '''
import itertools
from functools import partial from functools import partial
from types import ModuleType from types import ModuleType
@ -230,13 +229,10 @@ def test_ipc_channel_break_during_stream(
# get raw instance from pytest wrapper # get raw instance from pytest wrapper
value = excinfo.value value = excinfo.value
if isinstance(value, ExceptionGroup): if isinstance(value, ExceptionGroup):
value = next( excs = value.exceptions
itertools.dropwhile( assert len(excs) == 1
lambda exc: not isinstance(exc, expect_final_exc), final_exc = excs[0]
value.exceptions, assert isinstance(final_exc, expect_final_exc)
)
)
assert value
@tractor.context @tractor.context
@ -259,15 +255,16 @@ async def break_ipc_after_started(
def test_stream_closed_right_after_ipc_break_and_zombie_lord_engages(): def test_stream_closed_right_after_ipc_break_and_zombie_lord_engages():
''' '''
Verify that is a subactor's IPC goes down just after bringing up a stream Verify that is a subactor's IPC goes down just after bringing up
the parent can trigger a SIGINT and the child will be reaped out-of-IPC by a stream the parent can trigger a SIGINT and the child will be
the localhost process supervision machinery: aka "zombie lord". reaped out-of-IPC by the localhost process supervision machinery:
aka "zombie lord".
''' '''
async def main(): async def main():
with trio.fail_after(3): with trio.fail_after(3):
async with tractor.open_nursery() as n: async with tractor.open_nursery() as an:
portal = await n.start_actor( portal = await an.start_actor(
'ipc_breaker', 'ipc_breaker',
enable_modules=[__name__], enable_modules=[__name__],
) )

View File

@ -307,7 +307,15 @@ async def inf_streamer(
async with ( async with (
ctx.open_stream() as stream, ctx.open_stream() as stream,
trio.open_nursery() as tn,
# XXX TODO, INTERESTING CASE!!
# - if we don't collapse the eg then the embedded
# `trio.EndOfChannel` doesn't propagate directly to the above
# .open_stream() parent, resulting in it also raising instead
# of gracefully absorbing as normal.. so how to handle?
trio.open_nursery(
strict_exception_groups=False,
) as tn,
): ):
async def close_stream_on_sentinel(): async def close_stream_on_sentinel():
async for msg in stream: async for msg in stream:

View File

@ -519,7 +519,9 @@ def test_cancel_via_SIGINT_other_task(
async def main(): async def main():
# should never timeout since SIGINT should cancel the current program # should never timeout since SIGINT should cancel the current program
with trio.fail_after(timeout): with trio.fail_after(timeout):
async with trio.open_nursery() as n: async with trio.open_nursery(
strict_exception_groups=False,
) as n:
await n.start(spawn_and_sleep_forever) await n.start(spawn_and_sleep_forever)
if 'mp' in spawn_backend: if 'mp' in spawn_backend:
time.sleep(0.1) time.sleep(0.1)
@ -612,6 +614,12 @@ def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon(
nurse.start_soon(delayed_kbi) nurse.start_soon(delayed_kbi)
await p.run(do_nuthin) await p.run(do_nuthin)
# need to explicitly re-raise the lone kbi..now
except* KeyboardInterrupt as kbi_eg:
assert (len(excs := kbi_eg.exceptions) == 1)
raise excs[0]
finally: finally:
duration = time.time() - start duration = time.time() - start
if duration > timeout: if duration > timeout:

View File

@ -874,13 +874,13 @@ def chk_pld_type(
return roundtrip return roundtrip
def test_limit_msgspec(): def test_limit_msgspec(
debug_mode: bool,
):
async def main(): async def main():
async with tractor.open_root_actor( async with tractor.open_root_actor(
debug_mode=True debug_mode=debug_mode,
): ):
# ensure we can round-trip a boxing `PayloadMsg` # ensure we can round-trip a boxing `PayloadMsg`
assert chk_pld_type( assert chk_pld_type(
payload_spec=Any, payload_spec=Any,

View File

@ -95,8 +95,8 @@ async def trio_main(
# stash a "service nursery" as "actor local" (aka a Python global) # stash a "service nursery" as "actor local" (aka a Python global)
global _nursery global _nursery
n = _nursery tn = _nursery
assert n assert tn
async def consume_stream(): async def consume_stream():
async with wrapper_mngr() as stream: async with wrapper_mngr() as stream:
@ -104,10 +104,10 @@ async def trio_main(
print(msg) print(msg)
# run 2 tasks to ensure broadcaster chan use # run 2 tasks to ensure broadcaster chan use
n.start_soon(consume_stream) tn.start_soon(consume_stream)
n.start_soon(consume_stream) tn.start_soon(consume_stream)
n.start_soon(trio_sleep_and_err) tn.start_soon(trio_sleep_and_err)
await trio.sleep_forever() await trio.sleep_forever()
@ -117,8 +117,10 @@ async def open_actor_local_nursery(
ctx: tractor.Context, ctx: tractor.Context,
): ):
global _nursery global _nursery
async with trio.open_nursery() as n: async with trio.open_nursery(
_nursery = n strict_exception_groups=False,
) as tn:
_nursery = tn
await ctx.started() await ctx.started()
await trio.sleep(10) await trio.sleep(10)
# await trio.sleep(1) # await trio.sleep(1)
@ -132,7 +134,7 @@ async def open_actor_local_nursery(
# never yields back.. aka a scenario where the # never yields back.. aka a scenario where the
# ``tractor.context`` task IS NOT in the service n's cancel # ``tractor.context`` task IS NOT in the service n's cancel
# scope. # scope.
n.cancel_scope.cancel() tn.cancel_scope.cancel()
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -157,7 +159,7 @@ def test_actor_managed_trio_nursery_task_error_cancels_aio(
async with tractor.open_nursery() as n: async with tractor.open_nursery() as n:
p = await n.start_actor( p = await n.start_actor(
'nursery_mngr', 'nursery_mngr',
infect_asyncio=asyncio_mode, infect_asyncio=asyncio_mode, # TODO, is this enabling debug mode?
enable_modules=[__name__], enable_modules=[__name__],
) )
async with ( async with (

View File

@ -181,7 +181,9 @@ async def spawn_and_check_registry(
try: try:
async with tractor.open_nursery() as n: async with tractor.open_nursery() as n:
async with trio.open_nursery() as trion: async with trio.open_nursery(
strict_exception_groups=False,
) as trion:
portals = {} portals = {}
for i in range(3): for i in range(3):
@ -316,7 +318,9 @@ async def close_chans_before_nursery(
async with portal2.open_stream_from( async with portal2.open_stream_from(
stream_forever stream_forever
) as agen2: ) as agen2:
async with trio.open_nursery() as n: async with trio.open_nursery(
strict_exception_groups=False,
) as n:
n.start_soon(streamer, agen1) n.start_soon(streamer, agen1)
n.start_soon(cancel, use_signal, .5) n.start_soon(cancel, use_signal, .5)
try: try:

View File

@ -19,7 +19,7 @@ from tractor._testing import (
@pytest.fixture @pytest.fixture
def run_example_in_subproc( def run_example_in_subproc(
loglevel: str, loglevel: str,
testdir: pytest.Testdir, testdir: pytest.Pytester,
reg_addr: tuple[str, int], reg_addr: tuple[str, int],
): ):
@ -81,28 +81,36 @@ def run_example_in_subproc(
# walk yields: (dirpath, dirnames, filenames) # walk yields: (dirpath, dirnames, filenames)
[ [
(p[0], f) for p in os.walk(examples_dir()) for f in p[2] (p[0], f)
for p in os.walk(examples_dir())
for f in p[2]
if '__' not in f if (
and f[0] != '_' '__' not in f
and 'debugging' not in p[0] and f[0] != '_'
and 'integration' not in p[0] and 'debugging' not in p[0]
and 'advanced_faults' not in p[0] and 'integration' not in p[0]
and 'multihost' not in p[0] and 'advanced_faults' not in p[0]
and 'multihost' not in p[0]
)
], ],
ids=lambda t: t[1], ids=lambda t: t[1],
) )
def test_example(run_example_in_subproc, example_script): def test_example(
"""Load and run scripts from this repo's ``examples/`` dir as a user run_example_in_subproc,
example_script,
):
'''
Load and run scripts from this repo's ``examples/`` dir as a user
would copy and pasing them into their editor. would copy and pasing them into their editor.
On windows a little more "finessing" is done to make On windows a little more "finessing" is done to make
``multiprocessing`` play nice: we copy the ``__main__.py`` into the ``multiprocessing`` play nice: we copy the ``__main__.py`` into the
test directory and invoke the script as a module with ``python -m test directory and invoke the script as a module with ``python -m
test_example``. test_example``.
"""
ex_file = os.path.join(*example_script) '''
ex_file: str = os.path.join(*example_script)
if 'rpc_bidir_streaming' in ex_file and sys.version_info < (3, 9): if 'rpc_bidir_streaming' in ex_file and sys.version_info < (3, 9):
pytest.skip("2-way streaming example requires py3.9 async with syntax") pytest.skip("2-way streaming example requires py3.9 async with syntax")
@ -128,7 +136,8 @@ def test_example(run_example_in_subproc, example_script):
# shouldn't eventually once we figure out what's # shouldn't eventually once we figure out what's
# a better way to be explicit about aio side # a better way to be explicit about aio side
# cancels? # cancels?
and 'asyncio.exceptions.CancelledError' not in last_error and
'asyncio.exceptions.CancelledError' not in last_error
): ):
raise Exception(errmsg) raise Exception(errmsg)

View File

@ -2,7 +2,9 @@
Broadcast channels for fan-out to local tasks. Broadcast channels for fan-out to local tasks.
""" """
from contextlib import asynccontextmanager from contextlib import (
asynccontextmanager as acm,
)
from functools import partial from functools import partial
from itertools import cycle from itertools import cycle
import time import time
@ -15,6 +17,7 @@ import tractor
from tractor.trionics import ( from tractor.trionics import (
broadcast_receiver, broadcast_receiver,
Lagged, Lagged,
collapse_eg,
) )
@ -62,7 +65,7 @@ async def ensure_sequence(
break break
@asynccontextmanager @acm
async def open_sequence_streamer( async def open_sequence_streamer(
sequence: list[int], sequence: list[int],
@ -74,9 +77,9 @@ async def open_sequence_streamer(
async with tractor.open_nursery( async with tractor.open_nursery(
arbiter_addr=reg_addr, arbiter_addr=reg_addr,
start_method=start_method, start_method=start_method,
) as tn: ) as an:
portal = await tn.start_actor( portal = await an.start_actor(
'sequence_echoer', 'sequence_echoer',
enable_modules=[__name__], enable_modules=[__name__],
) )
@ -155,9 +158,12 @@ def test_consumer_and_parent_maybe_lag(
) as stream: ) as stream:
try: try:
async with trio.open_nursery() as n: async with (
collapse_eg(),
trio.open_nursery() as tn,
):
n.start_soon( tn.start_soon(
ensure_sequence, ensure_sequence,
stream, stream,
sequence.copy(), sequence.copy(),
@ -230,8 +236,8 @@ def test_faster_task_to_recv_is_cancelled_by_slower(
) as stream: ) as stream:
async with trio.open_nursery() as n: async with trio.open_nursery() as tn:
n.start_soon( tn.start_soon(
ensure_sequence, ensure_sequence,
stream, stream,
sequence.copy(), sequence.copy(),
@ -253,7 +259,7 @@ def test_faster_task_to_recv_is_cancelled_by_slower(
continue continue
print('cancelling faster subtask') print('cancelling faster subtask')
n.cancel_scope.cancel() tn.cancel_scope.cancel()
try: try:
value = await stream.receive() value = await stream.receive()
@ -371,13 +377,13 @@ def test_ensure_slow_consumers_lag_out(
f'on {lags}:{value}') f'on {lags}:{value}')
return return
async with trio.open_nursery() as nursery: async with trio.open_nursery() as tn:
for i in range(1, num_laggers): for i in range(1, num_laggers):
task_name = f'sub_{i}' task_name = f'sub_{i}'
laggers[task_name] = 0 laggers[task_name] = 0
nursery.start_soon( tn.start_soon(
partial( partial(
sub_and_print, sub_and_print,
delay=i*0.001, delay=i*0.001,
@ -497,6 +503,7 @@ def test_no_raise_on_lag():
# internals when the no raise flag is set. # internals when the no raise flag is set.
loglevel='warning', loglevel='warning',
), ),
collapse_eg(),
trio.open_nursery() as n, trio.open_nursery() as n,
): ):
n.start_soon(slow) n.start_soon(slow)

View File

@ -64,7 +64,9 @@ def test_stashed_child_nursery(use_start_soon):
async def main(): async def main():
async with ( async with (
trio.open_nursery() as pn, trio.open_nursery(
strict_exception_groups=False,
) as pn,
): ):
cn = await pn.start(mk_child_nursery) cn = await pn.start(mk_child_nursery)
assert cn assert cn
@ -101,6 +103,7 @@ def test_stashed_child_nursery(use_start_soon):
def test_acm_embedded_nursery_propagates_enter_err( def test_acm_embedded_nursery_propagates_enter_err(
canc_from_finally: bool, canc_from_finally: bool,
unmask_from_canc: bool, unmask_from_canc: bool,
debug_mode: bool,
): ):
''' '''
Demo how a masking `trio.Cancelled` could be handled by unmasking from the Demo how a masking `trio.Cancelled` could be handled by unmasking from the
@ -174,7 +177,9 @@ def test_acm_embedded_nursery_propagates_enter_err(
await trio.lowlevel.checkpoint() await trio.lowlevel.checkpoint()
async def _main(): async def _main():
with tractor.devx.open_crash_handler() as bxerr: with tractor.devx.maybe_open_crash_handler(
pdb=debug_mode,
) as bxerr:
assert not bxerr.value assert not bxerr.value
async with ( async with (

View File

@ -44,6 +44,7 @@ from ._state import (
current_actor as current_actor, current_actor as current_actor,
is_root_process as is_root_process, is_root_process as is_root_process,
current_ipc_ctx as current_ipc_ctx, current_ipc_ctx as current_ipc_ctx,
debug_mode as debug_mode
) )
from ._exceptions import ( from ._exceptions import (
ContextCancelled as ContextCancelled, ContextCancelled as ContextCancelled,
@ -66,3 +67,4 @@ from ._root import (
from ._ipc import Channel as Channel from ._ipc import Channel as Channel
from ._portal import Portal as Portal from ._portal import Portal as Portal
from ._runtime import Actor as Actor from ._runtime import Actor as Actor
from . import hilevel as hilevel

View File

@ -19,10 +19,13 @@ Actor cluster helpers.
''' '''
from __future__ import annotations from __future__ import annotations
from contextlib import (
from contextlib import asynccontextmanager as acm asynccontextmanager as acm,
)
from multiprocessing import cpu_count from multiprocessing import cpu_count
from typing import AsyncGenerator, Optional from typing import (
AsyncGenerator,
)
import trio import trio
import tractor import tractor

View File

@ -950,7 +950,7 @@ class Context:
# f'Context.cancel() => {self.chan.uid}\n' # f'Context.cancel() => {self.chan.uid}\n'
f'c)=> {self.chan.uid}\n' f'c)=> {self.chan.uid}\n'
# f'{self.chan.uid}\n' # f'{self.chan.uid}\n'
f' |_ @{self.dst_maddr}\n' f' |_ @{self.dst_maddr}\n'
f' >> {self.repr_rpc}\n' f' >> {self.repr_rpc}\n'
# f' >> {self._nsf}() -> {codec}[dict]:\n\n' # f' >> {self._nsf}() -> {codec}[dict]:\n\n'
# TODO: pull msg-type from spec re #320 # TODO: pull msg-type from spec re #320
@ -1003,7 +1003,8 @@ class Context:
) )
else: else:
log.cancel( log.cancel(
'Timed out on cancel request of remote task?\n' f'Timed out on cancel request of remote task?\n'
f'\n'
f'{reminfo}' f'{reminfo}'
) )
@ -1560,12 +1561,12 @@ class Context:
strict_pld_parity=strict_pld_parity, strict_pld_parity=strict_pld_parity,
hide_tb=hide_tb, hide_tb=hide_tb,
) )
except BaseException as err: except BaseException as _bexc:
err = _bexc
if not isinstance(err, MsgTypeError): if not isinstance(err, MsgTypeError):
__tracebackhide__: bool = False __tracebackhide__: bool = False
raise raise err
# TODO: maybe a flag to by-pass encode op if already done # TODO: maybe a flag to by-pass encode op if already done
# here in caller? # here in caller?
@ -1982,7 +1983,10 @@ async def open_context_from_portal(
ctxc_from_callee: ContextCancelled|None = None ctxc_from_callee: ContextCancelled|None = None
try: try:
async with ( async with (
trio.open_nursery() as tn, trio.open_nursery(
strict_exception_groups=False,
) as tn,
msgops.maybe_limit_plds( msgops.maybe_limit_plds(
ctx=ctx, ctx=ctx,
spec=ctx_meta.get('pld_spec'), spec=ctx_meta.get('pld_spec'),

View File

@ -238,7 +238,7 @@ def _trio_main(
nest_from_op( nest_from_op(
input_op='>(', # see syntax ideas above input_op='>(', # see syntax ideas above
tree_str=actor_info, tree_str=actor_info,
back_from_op=1, back_from_op=2, # since "complete"
) )
) )
logmeth = log.info logmeth = log.info

View File

@ -22,6 +22,7 @@ from __future__ import annotations
import builtins import builtins
import importlib import importlib
from pprint import pformat from pprint import pformat
from pdb import bdb
import sys import sys
from types import ( from types import (
TracebackType, TracebackType,
@ -181,6 +182,7 @@ def get_err_type(type_name: str) -> BaseException|None:
builtins, builtins,
_this_mod, _this_mod,
trio, trio,
bdb,
]: ]:
if type_ref := getattr( if type_ref := getattr(
ns, ns,

View File

@ -255,8 +255,8 @@ class MsgpackTCPStream(MsgTransport):
raise TransportClosed( raise TransportClosed(
message=( message=(
f'IPC transport already closed by peer\n' f'IPC transport already closed by peer\n'
f'x)> {type(trans_err)}\n' f'x]> {type(trans_err)}\n'
f' |_{self}\n' f' |_{self}\n'
), ),
loglevel=loglevel, loglevel=loglevel,
) from trans_err ) from trans_err
@ -273,8 +273,8 @@ class MsgpackTCPStream(MsgTransport):
raise TransportClosed( raise TransportClosed(
message=( message=(
f'IPC transport already manually closed locally?\n' f'IPC transport already manually closed locally?\n'
f'x)> {type(closure_err)} \n' f'x]> {type(closure_err)} \n'
f' |_{self}\n' f' |_{self}\n'
), ),
loglevel='error', loglevel='error',
raise_on_report=( raise_on_report=(
@ -289,8 +289,8 @@ class MsgpackTCPStream(MsgTransport):
raise TransportClosed( raise TransportClosed(
message=( message=(
f'IPC transport already gracefully closed\n' f'IPC transport already gracefully closed\n'
f')>\n' f']>\n'
f'|_{self}\n' f' |_{self}\n'
), ),
loglevel='transport', loglevel='transport',
# cause=??? # handy or no? # cause=??? # handy or no?

View File

@ -533,6 +533,10 @@ async def open_portal(
async with maybe_open_nursery( async with maybe_open_nursery(
tn, tn,
shield=shield, shield=shield,
strict_exception_groups=False,
# ^XXX^ TODO? soo roll our own then ??
# -> since we kinda want the "if only one `.exception` then
# just raise that" interface?
) as tn: ) as tn:
if not channel.connected(): if not channel.connected():

View File

@ -111,8 +111,8 @@ async def open_root_actor(
Runtime init entry point for ``tractor``. Runtime init entry point for ``tractor``.
''' '''
__tracebackhide__: bool = hide_tb
_debug.hide_runtime_frames() _debug.hide_runtime_frames()
__tracebackhide__: bool = hide_tb
# TODO: stick this in a `@cm` defined in `devx._debug`? # TODO: stick this in a `@cm` defined in `devx._debug`?
# #
@ -362,7 +362,10 @@ async def open_root_actor(
) )
# start the actor runtime in a new task # start the actor runtime in a new task
async with trio.open_nursery() as nursery: async with trio.open_nursery(
strict_exception_groups=False,
# ^XXX^ TODO? instead unpack any RAE as per "loose" style?
) as nursery:
# ``_runtime.async_main()`` creates an internal nursery # ``_runtime.async_main()`` creates an internal nursery
# and blocks here until any underlying actor(-process) # and blocks here until any underlying actor(-process)
@ -387,6 +390,12 @@ async def open_root_actor(
BaseExceptionGroup, BaseExceptionGroup,
) as err: ) as err:
# TODO, in beginning to handle the subsubactor with
# crashed grandparent cases..
#
# was_locked: bool = await _debug.maybe_wait_for_debugger(
# child_in_debug=True,
# )
# XXX NOTE XXX see equiv note inside # XXX NOTE XXX see equiv note inside
# `._runtime.Actor._stream_handler()` where in the # `._runtime.Actor._stream_handler()` where in the
# non-root or root-that-opened-this-mahually case we # non-root or root-that-opened-this-mahually case we
@ -457,12 +466,19 @@ def run_daemon(
start_method: str | None = None, start_method: str | None = None,
debug_mode: bool = False, debug_mode: bool = False,
# TODO, support `infected_aio=True` mode by,
# - calling the appropriate entrypoint-func from `.to_asyncio`
# - maybe init-ing `greenback` as done above in
# `open_root_actor()`.
**kwargs **kwargs
) -> None: ) -> None:
''' '''
Spawn daemon actor which will respond to RPC; the main task simply Spawn a root (daemon) actor which will respond to RPC; the main
starts the runtime and then sleeps forever. task simply starts the runtime and then blocks via embedded
`trio.sleep_forever()`.
This is a very minimal convenience wrapper around starting This is a very minimal convenience wrapper around starting
a "run-until-cancelled" root actor which can be started with a set a "run-until-cancelled" root actor which can be started with a set
@ -475,7 +491,6 @@ def run_daemon(
importlib.import_module(path) importlib.import_module(path)
async def _main(): async def _main():
async with open_root_actor( async with open_root_actor(
registry_addrs=registry_addrs, registry_addrs=registry_addrs,
name=name, name=name,

View File

@ -620,7 +620,11 @@ async def _invoke(
tn: trio.Nursery tn: trio.Nursery
rpc_ctx_cs: CancelScope rpc_ctx_cs: CancelScope
async with ( async with (
trio.open_nursery() as tn, trio.open_nursery(
strict_exception_groups=False,
# ^XXX^ TODO? instead unpack any RAE as per "loose" style?
) as tn,
msgops.maybe_limit_plds( msgops.maybe_limit_plds(
ctx=ctx, ctx=ctx,
spec=ctx_meta.get('pld_spec'), spec=ctx_meta.get('pld_spec'),
@ -733,8 +737,8 @@ async def _invoke(
# XXX: do we ever trigger this block any more? # XXX: do we ever trigger this block any more?
except ( except (
BaseExceptionGroup, BaseExceptionGroup,
trio.Cancelled,
BaseException, BaseException,
trio.Cancelled,
) as scope_error: ) as scope_error:
if ( if (
@ -847,8 +851,8 @@ async def try_ship_error_to_remote(
log.critical( log.critical(
'IPC transport failure -> ' 'IPC transport failure -> '
f'failed to ship error to {remote_descr}!\n\n' f'failed to ship error to {remote_descr}!\n\n'
f'X=> {channel.uid}\n\n' f'{type(msg)!r}[{msg.boxed_type_str}] X=> {channel.uid}\n'
f'\n'
# TODO: use `.msg.preetty_struct` for this! # TODO: use `.msg.preetty_struct` for this!
f'{msg}\n' f'{msg}\n'
) )

View File

@ -1283,7 +1283,8 @@ class Actor:
msg: str = ( msg: str = (
f'Actor-runtime cancel request from {requester_type}\n\n' f'Actor-runtime cancel request from {requester_type}\n\n'
f'<=c) {requesting_uid}\n' f'<=c) {requesting_uid}\n'
f' |_{self}\n' f' |_{self}\n'
f'\n'
) )
# TODO: what happens here when we self-cancel tho? # TODO: what happens here when we self-cancel tho?
@ -1303,13 +1304,15 @@ class Actor:
lock_req_ctx.has_outcome lock_req_ctx.has_outcome
): ):
msg += ( msg += (
'-> Cancelling active debugger request..\n' f'\n'
f'-> Cancelling active debugger request..\n'
f'|_{_debug.Lock.repr()}\n\n' f'|_{_debug.Lock.repr()}\n\n'
f'|_{lock_req_ctx}\n\n' f'|_{lock_req_ctx}\n\n'
) )
# lock_req_ctx._scope.cancel() # lock_req_ctx._scope.cancel()
# TODO: wrap this in a method-API.. # TODO: wrap this in a method-API..
debug_req.req_cs.cancel() debug_req.req_cs.cancel()
# if lock_req_ctx:
# self-cancel **all** ongoing RPC tasks # self-cancel **all** ongoing RPC tasks
await self.cancel_rpc_tasks( await self.cancel_rpc_tasks(
@ -1718,11 +1721,15 @@ async def async_main(
# parent is kept alive as a resilient service until # parent is kept alive as a resilient service until
# cancellation steps have (mostly) occurred in # cancellation steps have (mostly) occurred in
# a deterministic way. # a deterministic way.
async with trio.open_nursery() as root_nursery: async with trio.open_nursery(
strict_exception_groups=False,
) as root_nursery:
actor._root_n = root_nursery actor._root_n = root_nursery
assert actor._root_n assert actor._root_n
async with trio.open_nursery() as service_nursery: async with trio.open_nursery(
strict_exception_groups=False,
) as service_nursery:
# This nursery is used to handle all inbound # This nursery is used to handle all inbound
# connections to us such that if the TCP server # connections to us such that if the TCP server
# is killed, connections can continue to process # is killed, connections can continue to process

View File

@ -327,9 +327,10 @@ async def soft_kill(
uid: tuple[str, str] = portal.channel.uid uid: tuple[str, str] = portal.channel.uid
try: try:
log.cancel( log.cancel(
'Soft killing sub-actor via portal request\n' f'Soft killing sub-actor via portal request\n'
f'c)> {portal.chan.uid}\n' f'\n'
f' |_{proc}\n' f'(c=> {portal.chan.uid}\n'
f' |_{proc}\n'
) )
# wait on sub-proc to signal termination # wait on sub-proc to signal termination
await wait_func(proc) await wait_func(proc)

View File

@ -108,6 +108,7 @@ def is_main_process() -> bool:
return mp.current_process().name == 'MainProcess' return mp.current_process().name == 'MainProcess'
# TODO, more verby name?
def debug_mode() -> bool: def debug_mode() -> bool:
''' '''
Bool determining if "debug mode" is on which enables Bool determining if "debug mode" is on which enables

View File

@ -376,7 +376,7 @@ class MsgStream(trio.abc.Channel):
f'Stream self-closed by {self._ctx.side!r}-side before EoC\n' f'Stream self-closed by {self._ctx.side!r}-side before EoC\n'
# } bc a stream is a "scope"/msging-phase inside an IPC # } bc a stream is a "scope"/msging-phase inside an IPC
f'x}}>\n' f'x}}>\n'
f'|_{self}\n' f' |_{self}\n'
) )
log.cancel(message) log.cancel(message)
self._eoc = trio.EndOfChannel(message) self._eoc = trio.EndOfChannel(message)

View File

@ -395,17 +395,23 @@ async def _open_and_supervise_one_cancels_all_nursery(
# `ActorNursery.start_actor()`). # `ActorNursery.start_actor()`).
# errors from this daemon actor nursery bubble up to caller # errors from this daemon actor nursery bubble up to caller
async with trio.open_nursery() as da_nursery: async with trio.open_nursery(
strict_exception_groups=False,
# ^XXX^ TODO? instead unpack any RAE as per "loose" style?
) as da_nursery:
try: try:
# This is the inner level "run in actor" nursery. It is # This is the inner level "run in actor" nursery. It is
# awaited first since actors spawned in this way (using # awaited first since actors spawned in this way (using
# ``ActorNusery.run_in_actor()``) are expected to only # `ActorNusery.run_in_actor()`) are expected to only
# return a single result and then complete (i.e. be canclled # return a single result and then complete (i.e. be canclled
# gracefully). Errors collected from these actors are # gracefully). Errors collected from these actors are
# immediately raised for handling by a supervisor strategy. # immediately raised for handling by a supervisor strategy.
# As such if the strategy propagates any error(s) upwards # As such if the strategy propagates any error(s) upwards
# the above "daemon actor" nursery will be notified. # the above "daemon actor" nursery will be notified.
async with trio.open_nursery() as ria_nursery: async with trio.open_nursery(
strict_exception_groups=False,
# ^XXX^ TODO? instead unpack any RAE as per "loose" style?
) as ria_nursery:
an = ActorNursery( an = ActorNursery(
actor, actor,
@ -472,8 +478,8 @@ async def _open_and_supervise_one_cancels_all_nursery(
ContextCancelled, ContextCancelled,
}: }:
log.cancel( log.cancel(
'Actor-nursery caught remote cancellation\n\n' 'Actor-nursery caught remote cancellation\n'
'\n'
f'{inner_err.tb_str}' f'{inner_err.tb_str}'
) )
else: else:
@ -565,7 +571,9 @@ async def _open_and_supervise_one_cancels_all_nursery(
@acm @acm
# @api_frame # @api_frame
async def open_nursery( async def open_nursery(
hide_tb: bool = True,
**kwargs, **kwargs,
# ^TODO, paramspec for `open_root_actor()`
) -> typing.AsyncGenerator[ActorNursery, None]: ) -> typing.AsyncGenerator[ActorNursery, None]:
''' '''
@ -583,7 +591,7 @@ async def open_nursery(
which cancellation scopes correspond to each spawned subactor set. which cancellation scopes correspond to each spawned subactor set.
''' '''
__tracebackhide__: bool = True __tracebackhide__: bool = hide_tb
implicit_runtime: bool = False implicit_runtime: bool = False
actor: Actor = current_actor(err_on_no_runtime=False) actor: Actor = current_actor(err_on_no_runtime=False)
an: ActorNursery|None = None an: ActorNursery|None = None
@ -599,7 +607,10 @@ async def open_nursery(
# mark us for teardown on exit # mark us for teardown on exit
implicit_runtime: bool = True implicit_runtime: bool = True
async with open_root_actor(**kwargs) as actor: async with open_root_actor(
hide_tb=hide_tb,
**kwargs,
) as actor:
assert actor is current_actor() assert actor is current_actor()
try: try:
@ -637,8 +648,10 @@ async def open_nursery(
# show frame on any internal runtime-scope error # show frame on any internal runtime-scope error
if ( if (
an an
and not an.cancelled and
and an._scope_error not an.cancelled
and
an._scope_error
): ):
__tracebackhide__: bool = False __tracebackhide__: bool = False

View File

@ -19,7 +19,10 @@ Various helpers/utils for auditing your `tractor` app and/or the
core runtime. core runtime.
''' '''
from contextlib import asynccontextmanager as acm from contextlib import (
asynccontextmanager as acm,
)
import os
import pathlib import pathlib
import tractor import tractor
@ -59,7 +62,12 @@ def mk_cmd(
exs_subpath: str = 'debugging', exs_subpath: str = 'debugging',
) -> str: ) -> str:
''' '''
Generate a shell command suitable to pass to ``pexpect.spawn()``. Generate a shell command suitable to pass to `pexpect.spawn()`
which runs the script as a python program's entrypoint.
In particular ensure we disable the new tb coloring via unsetting
`$PYTHON_COLORS` so that `pexpect` can pattern match without
color-escape-codes.
''' '''
script_path: pathlib.Path = ( script_path: pathlib.Path = (
@ -67,10 +75,15 @@ def mk_cmd(
/ exs_subpath / exs_subpath
/ f'{ex_name}.py' / f'{ex_name}.py'
) )
return ' '.join([ py_cmd: str = ' '.join([
'python', 'python',
str(script_path) str(script_path)
]) ])
# XXX, required for py 3.13+
# https://docs.python.org/3/using/cmdline.html#using-on-controlling-color
# https://docs.python.org/3/using/cmdline.html#envvar-PYTHON_COLORS
os.environ['PYTHON_COLORS'] = '0'
return py_cmd
@acm @acm

View File

@ -317,8 +317,6 @@ class Lock:
we_released: bool = False we_released: bool = False
ctx_in_debug: Context|None = cls.ctx_in_debug ctx_in_debug: Context|None = cls.ctx_in_debug
repl_task: Task|Thread|None = DebugStatus.repl_task repl_task: Task|Thread|None = DebugStatus.repl_task
message: str = ''
try: try:
if not DebugStatus.is_main_trio_thread(): if not DebugStatus.is_main_trio_thread():
thread: threading.Thread = threading.current_thread() thread: threading.Thread = threading.current_thread()
@ -333,6 +331,10 @@ class Lock:
return False return False
task: Task = current_task() task: Task = current_task()
message: str = (
'TTY NOT RELEASED on behalf of caller\n'
f'|_{task}\n'
)
# sanity check that if we're the root actor # sanity check that if we're the root actor
# the lock is marked as such. # the lock is marked as such.
@ -347,11 +349,6 @@ class Lock:
else: else:
assert DebugStatus.repl_task is not task assert DebugStatus.repl_task is not task
message: str = (
'TTY lock was NOT released on behalf of caller\n'
f'|_{task}\n'
)
lock: trio.StrictFIFOLock = cls._debug_lock lock: trio.StrictFIFOLock = cls._debug_lock
owner: Task = lock.statistics().owner owner: Task = lock.statistics().owner
if ( if (
@ -366,23 +363,21 @@ class Lock:
# correct task, greenback-spawned-task and/or thread # correct task, greenback-spawned-task and/or thread
# being set to the `.repl_task` such that the above # being set to the `.repl_task` such that the above
# condition matches and we actually release the lock. # condition matches and we actually release the lock.
#
# This is particular of note from `.pause_from_sync()`! # This is particular of note from `.pause_from_sync()`!
): ):
cls._debug_lock.release() cls._debug_lock.release()
we_released: bool = True we_released: bool = True
if repl_task: if repl_task:
message: str = ( message: str = (
'Lock released on behalf of root-actor-local REPL owner\n' 'TTY released on behalf of root-actor-local REPL owner\n'
f'|_{repl_task}\n' f'|_{repl_task}\n'
) )
else: else:
message: str = ( message: str = (
'TTY lock released by us on behalf of remote peer?\n' 'TTY released by us on behalf of remote peer?\n'
f'|_ctx_in_debug: {ctx_in_debug}\n\n' f'{ctx_in_debug}\n'
) )
# mk_pdb().set_trace()
# elif owner:
except RuntimeError as rte: except RuntimeError as rte:
log.exception( log.exception(
@ -400,7 +395,8 @@ class Lock:
req_handler_finished: trio.Event|None = Lock.req_handler_finished req_handler_finished: trio.Event|None = Lock.req_handler_finished
if ( if (
not lock_stats.owner not lock_stats.owner
and req_handler_finished is None and
req_handler_finished is None
): ):
message += ( message += (
'-> No new task holds the TTY lock!\n\n' '-> No new task holds the TTY lock!\n\n'
@ -418,8 +414,8 @@ class Lock:
repl_task repl_task
) )
message += ( message += (
f'A non-caller task still owns this lock on behalf of ' f'A non-caller task still owns this lock on behalf of\n'
f'`{behalf_of_task}`\n' f'{behalf_of_task}\n'
f'lock owner task: {lock_stats.owner}\n' f'lock owner task: {lock_stats.owner}\n'
) )
@ -447,8 +443,6 @@ class Lock:
if message: if message:
log.devx(message) log.devx(message)
else:
import pdbp; pdbp.set_trace()
return we_released return we_released
@ -668,10 +662,11 @@ async def lock_stdio_for_peer(
fail_reason: str = ( fail_reason: str = (
f'on behalf of peer\n\n' f'on behalf of peer\n\n'
f'x)<=\n' f'x)<=\n'
f' |_{subactor_task_uid!r}@{ctx.chan.uid!r}\n\n' f' |_{subactor_task_uid!r}@{ctx.chan.uid!r}\n'
f'\n'
'Forcing `Lock.release()` due to acquire failure!\n\n' 'Forcing `Lock.release()` due to acquire failure!\n\n'
f'x)=> {ctx}\n' f'x)=>\n'
f' {ctx}'
) )
if isinstance(req_err, trio.Cancelled): if isinstance(req_err, trio.Cancelled):
fail_reason = ( fail_reason = (
@ -1179,7 +1174,7 @@ async def request_root_stdio_lock(
log.devx( log.devx(
'Initing stdio-lock request task with root actor' 'Initing stdio-lock request task with root actor'
) )
# TODO: likely we can implement this mutex more generally as # TODO: can we implement this mutex more generally as
# a `._sync.Lock`? # a `._sync.Lock`?
# -[ ] simply add the wrapping needed for the debugger specifics? # -[ ] simply add the wrapping needed for the debugger specifics?
# - the `__pld_spec__` impl and maybe better APIs for the client # - the `__pld_spec__` impl and maybe better APIs for the client
@ -1190,6 +1185,7 @@ async def request_root_stdio_lock(
# - https://docs.python.org/3.8/library/multiprocessing.html#multiprocessing.RLock # - https://docs.python.org/3.8/library/multiprocessing.html#multiprocessing.RLock
DebugStatus.req_finished = trio.Event() DebugStatus.req_finished = trio.Event()
DebugStatus.req_task = current_task() DebugStatus.req_task = current_task()
req_err: BaseException|None = None
try: try:
from tractor._discovery import get_root from tractor._discovery import get_root
# NOTE: we need this to ensure that this task exits # NOTE: we need this to ensure that this task exits
@ -1212,6 +1208,7 @@ async def request_root_stdio_lock(
# ) # )
DebugStatus.req_cs = req_cs DebugStatus.req_cs = req_cs
req_ctx: Context|None = None req_ctx: Context|None = None
ctx_eg: BaseExceptionGroup|None = None
try: try:
# TODO: merge into single async with ? # TODO: merge into single async with ?
async with get_root() as portal: async with get_root() as portal:
@ -1242,7 +1239,12 @@ async def request_root_stdio_lock(
) )
# try: # try:
assert status.subactor_uid == actor_uid if (locker := status.subactor_uid) != actor_uid:
raise DebugStateError(
f'Root actor locked by another peer !?\n'
f'locker: {locker!r}\n'
f'actor_uid: {actor_uid}\n'
)
assert status.cid assert status.cid
# except AttributeError: # except AttributeError:
# log.exception('failed pldspec asserts!') # log.exception('failed pldspec asserts!')
@ -1279,10 +1281,11 @@ async def request_root_stdio_lock(
f'Exitting {req_ctx.side!r}-side of locking req_ctx\n' f'Exitting {req_ctx.side!r}-side of locking req_ctx\n'
) )
except ( except* (
tractor.ContextCancelled, tractor.ContextCancelled,
trio.Cancelled, trio.Cancelled,
): ) as _taskc_eg:
ctx_eg = _taskc_eg
log.cancel( log.cancel(
'Debug lock request was CANCELLED?\n\n' 'Debug lock request was CANCELLED?\n\n'
f'<=c) {req_ctx}\n' f'<=c) {req_ctx}\n'
@ -1291,21 +1294,23 @@ async def request_root_stdio_lock(
) )
raise raise
except ( except* (
BaseException, BaseException,
) as ctx_err: ) as _ctx_eg:
ctx_eg = _ctx_eg
message: str = ( message: str = (
'Failed during debug request dialog with root actor?\n\n' 'Failed during debug request dialog with root actor?\n'
) )
if (req_ctx := DebugStatus.req_ctx): if (req_ctx := DebugStatus.req_ctx):
message += ( message += (
f'<=x) {req_ctx}\n\n' f'<=x)\n'
f' |_{req_ctx}\n'
f'Cancelling IPC ctx!\n' f'Cancelling IPC ctx!\n'
) )
try: try:
await req_ctx.cancel() await req_ctx.cancel()
except trio.ClosedResourceError as terr: except trio.ClosedResourceError as terr:
ctx_err.add_note( ctx_eg.add_note(
# f'Failed with {type(terr)!r} x)> `req_ctx.cancel()` ' # f'Failed with {type(terr)!r} x)> `req_ctx.cancel()` '
f'Failed with `req_ctx.cancel()` <x) {type(terr)!r} ' f'Failed with `req_ctx.cancel()` <x) {type(terr)!r} '
) )
@ -1314,21 +1319,45 @@ async def request_root_stdio_lock(
message += 'Failed in `Portal.open_context()` call ??\n' message += 'Failed in `Portal.open_context()` call ??\n'
log.exception(message) log.exception(message)
ctx_err.add_note(message) ctx_eg.add_note(message)
raise ctx_err raise ctx_eg
except ( except BaseException as _req_err:
tractor.ContextCancelled, req_err = _req_err
trio.Cancelled,
): # XXX NOTE, since new `trio` enforces strict egs by default
log.cancel( # we have to always handle the eg explicitly given the
'Debug lock request CANCELLED?\n' # `Portal.open_context()` call above (which implicitly opens
f'{req_ctx}\n' # a nursery).
) match req_err:
raise case BaseExceptionGroup():
# for an eg of just one taskc, just unpack and raise
# since we want to propagate a plane ol' `Cancelled`
# up from the `.pause()` call.
excs: list[BaseException] = req_err.exceptions
if (
len(excs) == 1
and
type(exc := excs[0]) in (
tractor.ContextCancelled,
trio.Cancelled,
)
):
log.cancel(
'Debug lock request CANCELLED?\n'
f'{req_ctx}\n'
)
raise exc
case (
tractor.ContextCancelled(),
trio.Cancelled(),
):
log.cancel(
'Debug lock request CANCELLED?\n'
f'{req_ctx}\n'
)
raise exc
except BaseException as req_err:
# log.error('Failed to request root stdio-lock?')
DebugStatus.req_err = req_err DebugStatus.req_err = req_err
DebugStatus.release() DebugStatus.release()
@ -1343,7 +1372,7 @@ async def request_root_stdio_lock(
'Failed during stdio-locking dialog from root actor\n\n' 'Failed during stdio-locking dialog from root actor\n\n'
f'<=x)\n' f'<=x)\n'
f'|_{DebugStatus.req_ctx}\n' f' |_{DebugStatus.req_ctx}\n'
) from req_err ) from req_err
finally: finally:
@ -1406,7 +1435,7 @@ def any_connected_locker_child() -> bool:
actor: Actor = current_actor() actor: Actor = current_actor()
if not is_root_process(): if not is_root_process():
raise RuntimeError('This is a root-actor only API!') raise InternalError('This is a root-actor only API!')
if ( if (
(ctx := Lock.ctx_in_debug) (ctx := Lock.ctx_in_debug)
@ -2143,11 +2172,12 @@ async def _pause(
# `_enter_repl_sync()` into a common @cm? # `_enter_repl_sync()` into a common @cm?
except BaseException as _pause_err: except BaseException as _pause_err:
pause_err: BaseException = _pause_err pause_err: BaseException = _pause_err
_repl_fail_report: str|None = _repl_fail_msg
if isinstance(pause_err, bdb.BdbQuit): if isinstance(pause_err, bdb.BdbQuit):
log.devx( log.devx(
'REPL for pdb was explicitly quit!\n' 'REPL for pdb was explicitly quit!\n'
) )
_repl_fail_msg = None _repl_fail_report = None
# when the actor is mid-runtime cancellation the # when the actor is mid-runtime cancellation the
# `Actor._service_n` might get closed before we can spawn # `Actor._service_n` might get closed before we can spawn
@ -2167,16 +2197,16 @@ async def _pause(
return return
elif isinstance(pause_err, trio.Cancelled): elif isinstance(pause_err, trio.Cancelled):
_repl_fail_msg = ( _repl_fail_report += (
'You called `tractor.pause()` from an already cancelled scope!\n\n' 'You called `tractor.pause()` from an already cancelled scope!\n\n'
'Consider `await tractor.pause(shield=True)` to make it work B)\n' 'Consider `await tractor.pause(shield=True)` to make it work B)\n'
) )
else: else:
_repl_fail_msg += f'on behalf of {repl_task} ??\n' _repl_fail_report += f'on behalf of {repl_task} ??\n'
if _repl_fail_msg: if _repl_fail_report:
log.exception(_repl_fail_msg) log.exception(_repl_fail_report)
if not actor.is_infected_aio(): if not actor.is_infected_aio():
DebugStatus.release(cancel_req_task=True) DebugStatus.release(cancel_req_task=True)
@ -2257,6 +2287,13 @@ def _set_trace(
repl.set_trace(frame=caller_frame) repl.set_trace(frame=caller_frame)
# XXX TODO! XXX, ensure `pytest -s` doesn't just
# hang on this being called in a test.. XD
# -[ ] maybe something in our test suite or is there
# some way we can detect output capture is enabled
# from the process itself?
# |_ronny: ?
#
async def pause( async def pause(
*, *,
hide_tb: bool = True, hide_tb: bool = True,
@ -3051,7 +3088,8 @@ async def maybe_wait_for_debugger(
if ( if (
not debug_mode() not debug_mode()
and not child_in_debug and
not child_in_debug
): ):
return False return False
@ -3109,7 +3147,7 @@ async def maybe_wait_for_debugger(
logmeth( logmeth(
msg msg
+ +
'\nRoot is waiting on tty lock to release from\n\n' '\n^^ Root is waiting on tty lock release.. ^^\n'
# f'{caller_frame_info}\n' # f'{caller_frame_info}\n'
) )
@ -3163,6 +3201,15 @@ async def maybe_wait_for_debugger(
return False return False
class BoxedMaybeException(Struct):
'''
Box a maybe-exception for post-crash introspection usage
from the body of a `open_crash_handler()` scope.
'''
value: BaseException|None = None
# TODO: better naming and what additionals? # TODO: better naming and what additionals?
# - [ ] optional runtime plugging? # - [ ] optional runtime plugging?
# - [ ] detection for sync vs. async code? # - [ ] detection for sync vs. async code?
@ -3172,11 +3219,11 @@ async def maybe_wait_for_debugger(
@cm @cm
def open_crash_handler( def open_crash_handler(
catch: set[BaseException] = { catch: set[BaseException] = {
# Exception,
BaseException, BaseException,
}, },
ignore: set[BaseException] = { ignore: set[BaseException] = {
KeyboardInterrupt, KeyboardInterrupt,
trio.Cancelled,
}, },
tb_hide: bool = True, tb_hide: bool = True,
): ):
@ -3193,9 +3240,6 @@ def open_crash_handler(
''' '''
__tracebackhide__: bool = tb_hide __tracebackhide__: bool = tb_hide
class BoxedMaybeException(Struct):
value: BaseException|None = None
# TODO, yield a `outcome.Error`-like boxed type? # TODO, yield a `outcome.Error`-like boxed type?
# -[~] use `outcome.Value/Error` X-> frozen! # -[~] use `outcome.Value/Error` X-> frozen!
# -[x] write our own..? # -[x] write our own..?
@ -3237,6 +3281,8 @@ def open_crash_handler(
def maybe_open_crash_handler( def maybe_open_crash_handler(
pdb: bool = False, pdb: bool = False,
tb_hide: bool = True, tb_hide: bool = True,
**kwargs,
): ):
''' '''
Same as `open_crash_handler()` but with bool input flag Same as `open_crash_handler()` but with bool input flag
@ -3247,9 +3293,11 @@ def maybe_open_crash_handler(
''' '''
__tracebackhide__: bool = tb_hide __tracebackhide__: bool = tb_hide
rtctx = nullcontext rtctx = nullcontext(
enter_result=BoxedMaybeException()
)
if pdb: if pdb:
rtctx = open_crash_handler rtctx = open_crash_handler(**kwargs)
with rtctx(): with rtctx as boxed_maybe_exc:
yield yield boxed_maybe_exc

View File

@ -258,6 +258,9 @@ class PldRx(Struct):
f'|_pld={pld!r}\n' f'|_pld={pld!r}\n'
) )
return pld return pld
except TypeError as typerr:
__tracebackhide__: bool = False
raise typerr
# XXX pld-value type failure # XXX pld-value type failure
except ValidationError as valerr: except ValidationError as valerr:
@ -796,8 +799,14 @@ def validate_payload_msg(
__tracebackhide__: bool = hide_tb __tracebackhide__: bool = hide_tb
codec: MsgCodec = current_codec() codec: MsgCodec = current_codec()
msg_bytes: bytes = codec.encode(pld_msg) msg_bytes: bytes = codec.encode(pld_msg)
roundtripped: Started|None = None
try: try:
roundtripped: Started = codec.decode(msg_bytes) roundtripped: Started = codec.decode(msg_bytes)
except TypeError as typerr:
__tracebackhide__: bool = False
raise typerr
try:
ctx: Context = getattr(ipc, 'ctx', ipc) ctx: Context = getattr(ipc, 'ctx', ipc)
pld: PayloadT = ctx.pld_rx.decode_pld( pld: PayloadT = ctx.pld_rx.decode_pld(
msg=roundtripped, msg=roundtripped,
@ -822,6 +831,11 @@ def validate_payload_msg(
) )
raise ValidationError(complaint) raise ValidationError(complaint)
# usually due to `.decode()` input type
except TypeError as typerr:
__tracebackhide__: bool = False
raise typerr
# raise any msg type error NO MATTER WHAT! # raise any msg type error NO MATTER WHAT!
except ValidationError as verr: except ValidationError as verr:
try: try:
@ -832,9 +846,13 @@ def validate_payload_msg(
verb_header='Trying to send ', verb_header='Trying to send ',
is_invalid_payload=True, is_invalid_payload=True,
) )
except BaseException: except BaseException as _be:
if not roundtripped:
raise verr
be = _be
__tracebackhide__: bool = False __tracebackhide__: bool = False
raise raise be
if not raise_mte: if not raise_mte:
return mte return mte

View File

@ -29,3 +29,6 @@ from ._broadcast import (
BroadcastReceiver as BroadcastReceiver, BroadcastReceiver as BroadcastReceiver,
Lagged as Lagged, Lagged as Lagged,
) )
from ._beg import (
collapse_eg as collapse_eg,
)

View File

@ -0,0 +1,58 @@
# 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/>.
'''
`BaseExceptionGroup` related utils and helpers pertaining to
first-class-`trio` from a historical perspective B)
'''
from contextlib import (
asynccontextmanager as acm,
)
def maybe_collapse_eg(
beg: BaseExceptionGroup,
) -> BaseException:
'''
If the input beg can collapse to a single non-eg sub-exception,
return it instead.
'''
if len(excs := beg.exceptions) == 1:
return excs[0]
return beg
@acm
async def collapse_eg():
'''
If `BaseExceptionGroup` raised in the body scope is
"collapse-able" (in the same way that
`trio.open_nursery(strict_exception_groups=False)` works) then
only raise the lone emedded non-eg in in place.
'''
try:
yield
except* BaseException as beg:
if (
exc := maybe_collapse_eg(beg)
) is not beg:
raise exc
raise beg

View File

@ -15,7 +15,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
''' '''
``tokio`` style broadcast channel. `tokio` style broadcast channel.
https://docs.rs/tokio/1.11.0/tokio/sync/broadcast/index.html https://docs.rs/tokio/1.11.0/tokio/sync/broadcast/index.html
''' '''

View File

@ -57,6 +57,8 @@ async def maybe_open_nursery(
shield: bool = False, shield: bool = False,
lib: ModuleType = trio, lib: ModuleType = trio,
**kwargs, # proxy thru
) -> AsyncGenerator[trio.Nursery, Any]: ) -> AsyncGenerator[trio.Nursery, Any]:
''' '''
Create a new nursery if None provided. Create a new nursery if None provided.
@ -67,7 +69,7 @@ async def maybe_open_nursery(
if nursery is not None: if nursery is not None:
yield nursery yield nursery
else: else:
async with lib.open_nursery() as nursery: async with lib.open_nursery(**kwargs) as nursery:
nursery.cancel_scope.shield = shield nursery.cancel_scope.shield = shield
yield nursery yield nursery
@ -143,9 +145,14 @@ async def gather_contexts(
'Use a non-lazy iterator or sequence type intead!' 'Use a non-lazy iterator or sequence type intead!'
) )
async with trio.open_nursery() as n: async with trio.open_nursery(
strict_exception_groups=False,
# ^XXX^ TODO? soo roll our own then ??
# -> since we kinda want the "if only one `.exception` then
# just raise that" interface?
) as tn:
for mngr in mngrs: for mngr in mngrs:
n.start_soon( tn.start_soon(
_enter_and_wait, _enter_and_wait,
mngr, mngr,
unwrapped, unwrapped,

88
uv.lock
View File

@ -126,7 +126,31 @@ wheels = [
[[package]] [[package]]
name = "msgspec" name = "msgspec"
version = "0.19.0" version = "0.19.0"
source = { git = "https://github.com/jcrist/msgspec.git#dd965dce22e5278d4935bea923441ecde31b5325" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939 },
{ url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202 },
{ url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029 },
{ url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682 },
{ url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003 },
{ url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833 },
{ url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184 },
{ url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485 },
{ url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910 },
{ url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633 },
{ url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594 },
{ url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053 },
{ url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081 },
{ url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467 },
{ url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498 },
{ url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950 },
{ url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647 },
{ url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563 },
{ url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996 },
{ url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087 },
{ url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432 },
]
[[package]] [[package]]
name = "outcome" name = "outcome"
@ -240,7 +264,7 @@ wheels = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.3.4" version = "8.3.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
@ -248,9 +272,9 @@ dependencies = [
{ name = "packaging" }, { name = "packaging" },
{ name = "pluggy" }, { name = "pluggy" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
] ]
[[package]] [[package]]
@ -314,17 +338,15 @@ dev = [
{ name = "pytest" }, { name = "pytest" },
{ name = "stackscope" }, { name = "stackscope" },
{ name = "xonsh" }, { name = "xonsh" },
{ name = "xonsh-vox-tabcomplete" },
{ name = "xontrib-vox" },
] ]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "colorlog", specifier = ">=6.8.2,<7" }, { name = "colorlog", specifier = ">=6.8.2,<7" },
{ name = "msgspec", git = "https://github.com/jcrist/msgspec.git" }, { name = "msgspec", specifier = ">=0.19.0" },
{ name = "pdbp", specifier = ">=1.5.0,<2" }, { name = "pdbp", specifier = ">=1.6,<2" },
{ name = "tricycle", specifier = ">=0.4.1,<0.5" }, { name = "tricycle", specifier = ">=0.4.1,<0.5" },
{ name = "trio", specifier = ">=0.24,<0.25" }, { name = "trio", specifier = ">0.27" },
{ name = "wrapt", specifier = ">=1.16.0,<2" }, { name = "wrapt", specifier = ">=1.16.0,<2" },
] ]
@ -332,13 +354,11 @@ requires-dist = [
dev = [ dev = [
{ name = "greenback", specifier = ">=1.2.1,<2" }, { name = "greenback", 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.43,<4" }, { name = "prompt-toolkit", specifier = ">=3.0.50" },
{ name = "pyperclip", specifier = ">=1.9.0" }, { name = "pyperclip", specifier = ">=1.9.0" },
{ name = "pytest", specifier = ">=8.2.0,<9" }, { name = "pytest", specifier = ">=8.3.5" },
{ name = "stackscope", specifier = ">=0.2.2,<0.3" }, { name = "stackscope", specifier = ">=0.2.2,<0.3" },
{ name = "xonsh", specifier = ">=0.19.1" }, { name = "xonsh", specifier = ">=0.19.2" },
{ name = "xonsh-vox-tabcomplete", specifier = ">=0.5,<0.6" },
{ name = "xontrib-vox", specifier = ">=0.0.1,<0.0.2" },
] ]
[[package]] [[package]]
@ -355,7 +375,7 @@ wheels = [
[[package]] [[package]]
name = "trio" name = "trio"
version = "0.24.0" version = "0.29.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "attrs" }, { name = "attrs" },
@ -365,9 +385,9 @@ dependencies = [
{ name = "sniffio" }, { name = "sniffio" },
{ name = "sortedcontainers" }, { name = "sortedcontainers" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8a/f3/07c152213222c615fe2391b8e1fea0f5af83599219050a549c20fcbd9ba2/trio-0.24.0.tar.gz", hash = "sha256:ffa09a74a6bf81b84f8613909fb0beaee84757450183a7a2e0b47b455c0cac5d", size = 545131 } sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/14/fb/9299cf74953f473a15accfdbe2c15218e766bae8c796f2567c83bae03e98/trio-0.24.0-py3-none-any.whl", hash = "sha256:c3bd3a4e3e3025cd9a2241eae75637c43fe0b9e88b4c97b9161a55b9e54cd72c", size = 460205 }, { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 },
] ]
[[package]] [[package]]
@ -434,33 +454,13 @@ wheels = [
[[package]] [[package]]
name = "xonsh" name = "xonsh"
version = "0.19.1" version = "0.19.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/6e/b54a0b2685535995ee50f655103c463f9d339455c9b08c4bce3e03e7bb17/xonsh-0.19.1.tar.gz", hash = "sha256:5d3de649c909f6d14bc69232219bcbdb8152c830e91ddf17ad169c672397fb97", size = 796468 } sdist = { url = "https://files.pythonhosted.org/packages/68/4e/56e95a5e607eb3b0da37396f87cde70588efc8ef819ab16f02d5b8378dc4/xonsh-0.19.2.tar.gz", hash = "sha256:cfdd0680d954a2c3aefd6caddcc7143a3d06aa417ed18365a08219bb71b960b0", size = 799960 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/e6/db44068c5725af9678e37980ae9503165393d51b80dc8517fa4ec74af1cf/xonsh-0.19.1-py310-none-any.whl", hash = "sha256:83eb6610ed3535f8542abd80af9554fb7e2805b0b3f96e445f98d4b5cf1f7046", size = 640686 }, { url = "https://files.pythonhosted.org/packages/6c/13/281094759df87b23b3c02dc4a16603ab08ea54d7f6acfeb69f3341137c7a/xonsh-0.19.2-py310-none-any.whl", hash = "sha256:ec7f163fd3a4943782aa34069d4e72793328c916a5975949dbec8536cbfc089b", size = 642301 },
{ url = "https://files.pythonhosted.org/packages/77/4e/e487e82349866b245c559433c9ba626026a2e66bd17d7f9ac1045082f146/xonsh-0.19.1-py311-none-any.whl", hash = "sha256:c176e515b0260ab803963d1f0924f1e32f1064aa6fd5d791aa0cf6cda3a924ae", size = 640680 }, { url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286 },
{ url = "https://files.pythonhosted.org/packages/5d/88/09060815548219b8f6953a06c247cb5c92d03cbdf7a02a980bda1b5754db/xonsh-0.19.1-py312-none-any.whl", hash = "sha256:fe1266c86b117aced3bdc4d5972420bda715864435d0bd3722d63451e8001036", size = 640604 }, { url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386 },
{ url = "https://files.pythonhosted.org/packages/83/ff/7873cb8184cffeafddbf861712831c2baa2e9dbecdbfd33b1228f0db0019/xonsh-0.19.1-py313-none-any.whl", hash = "sha256:3f158b6fc0bba954e0b989004d4261bafc4bd94c68c2abd75b825da23e5a869c", size = 641166 }, { url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873 },
{ url = "https://files.pythonhosted.org/packages/cc/03/b9f8dd338df0a330011d104e63d4d0acd8bbbc1e990ff049487b6bdf585d/xonsh-0.19.1-py39-none-any.whl", hash = "sha256:a900a6eb87d881a7ef90b1ac8522ba3699582f0bcb1e9abd863d32f6d63faf04", size = 632912 }, { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602 },
]
[[package]]
name = "xonsh-vox-tabcomplete"
version = "0.5"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/fd/af0c2ee6c067c2a4dc64ec03598c94de1f6ec5984b3116af917f3add4a16/xonsh_vox_tabcomplete-0.5-py3-none-any.whl", hash = "sha256:9701b198180f167071234e77eab87b7befa97c1873b088d0b3fbbe6d6d8dcaad", size = 14381 },
]
[[package]]
name = "xontrib-vox"
version = "0.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "xonsh" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/ac/a5db68a1f2e4036f7ff4c8546b1cbe29edee2ff40e0ff931836745988b79/xontrib-vox-0.0.1.tar.gz", hash = "sha256:c1f0b155992b4b0ebe6dcfd651084a8707ade7372f7e456c484d2a85339d9907", size = 16504 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/23/58/dcdf11849c8340033da00669527ce75d8292a4e8d82605c082ed236a081a/xontrib_vox-0.0.1-py3-none-any.whl", hash = "sha256:df2bbb815832db5b04d46684f540eac967ee40ef265add2662a95d6947d04c70", size = 13467 },
] ]