Compare commits

...

33 Commits

Author SHA1 Message Date
Tyler Goodlet 4a6bac2fb1 Drop explicit `tabcompleter` dep, `pdpp` already sub-depends on it? 2025-03-24 21:44:59 -04:00
Tyler Goodlet 968140677b Bump up to `pytest>=8.3.5` to match "GH actions"
Ensure it's only for the `--dev` optional deps.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 4abc0afbee Bump to `msgspec>=0.19.0` for py 3.13 support! 2025-03-24 21:44:59 -04:00
Tyler Goodlet 4d865b8846 Bind another `_bexc` for debuggin 2025-03-24 21:44:59 -04:00
Tyler Goodlet 38497fab6a Comment-tag pause points in `asycnio_bp.py`
Thought i already did this but, obvi needed these to make the expect
matches pass in our test.
2025-03-24 21:44:59 -04:00
Tyler Goodlet fd6d2506bd Unpack errors from `pdb.bdb`
Like any `bdb.BdbQuit` that might be relayed from a remote context after
a REPl exit with the `quit` cmd. This fixes various issues while
debugging where it may not be clear to the parent task that the child
was terminated with a purposefully unrecoverable error.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 78028ca5f5 Show frames when decode is handed bad input 2025-03-24 21:44:59 -04:00
Tyler Goodlet 307e3cf593 Another loosie in the trioisms suite 2025-03-24 21:44:59 -04:00
Tyler Goodlet 68784a859a Match `maybe_open_crash_handler()` to non-maybe version
Such that it will deliver a `BoxedMaybeException` to the caller
regardless whether `pdb` is set, and proxy through all `**kwargs`.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 42389f8693 Use `collapse_eg()` in broadcaster suite
Around the test embedded `trio.open_nursery()` calls as expected. Also
tidy up the various nursery var names.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 539ae7d26d Draft some eg collapsing helpers
Inside a new `.trionics._beg` and exposed from the subpkg ns in
anticipation of the `strict_exception_groups=False` being removed by
`trio` in py 3.15.

Notes,
- mk an embedded single-exc "extractor" using a `BaseExceptionGroup.exceptions` length
  check, when 1 return the lone child.
- use the above in a new `@acm`, async bc it's most likely to be composed in an
  `async with` tuple-style sequence block, called `collapse_eg()` which
  acts a one line "absorber" for when the above mentioned flag is no
  logner supported by `trio.open_nursery()`.

All untested atm fwiw.. but soon to be used in our test suite(s) likely!
2025-03-24 21:44:59 -04:00
Tyler Goodlet 802774641f Fix docs tests with yet another loosie-goosie
So the KBI propagates up to the actor nursery scope and also avoid
running any `examples/multihost/` subdir scripts.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 1a0e6314f2 Another couple loose-ifies for discovery and advanced fault suites 2025-03-24 21:44:59 -04:00
Tyler Goodlet e510d84293 Add (masked) meta-debug-fixture for determining if `debug_mode` is set in harness.. 2025-03-24 21:44:59 -04:00
Tyler Goodlet ea29a1c479 Various test tweaks related to 3.13 egs
Including changes like,
- loose eg flagging in various test emedded `trio.open_nursery()`s.
- changes to eg handling (like using `except*`).
- added `debug_mode` integration to tests that needed some REPLin
  in order to figure out appropriate updates.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 0af83bd4d8 Go to loose egs in `Actor` root & service nurseries (for now..) 2025-03-24 21:44:59 -04:00
Tyler Goodlet 0696e7d9d0 Fix `roundtripped` ref error in `validate_payload_msg()` 2025-03-24 21:44:59 -04:00
Tyler Goodlet d6ed798922 Hide `open_nursery()` frame by def 2025-03-24 21:44:59 -04:00
Tyler Goodlet f63fb8d4cd Moar sclang log fmting tweaks 2025-03-24 21:44:59 -04:00
Tyler Goodlet 66bc38fcac Add equiv of `AsyncioCancelled` for aio side
Such that a `TrioCancelled` is raised in the aio task via
`.set_exception()` to explicitly indicate and allow that task to handle
a taskc request from the parent `trio.Task`.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 030a9b5625 Expose `._state.debug_mode()` predicate at top level 2025-03-24 21:44:59 -04:00
Tyler Goodlet b6e4e581fc Another loose-egs flag in `test_child_manages_service_nursery` 2025-03-24 21:44:59 -04:00
Tyler Goodlet 67c68343a8 Handle egs on failed `request_root_stdio_lock()`
Namely when the subactor fails to lock the root, in which case we
try to be very verbose about how/what failed in logging as well
as ensure we cancel the employed IPC ctx.

Implement the outer `BaseException` handler to handle both styles,
- match on an eg (or the prior std cancel excs) only raising a lone
  sub-exc from for former.
- always `as _req_err:` and assign to a new func-global `req_err`
  to enable the above matching.

Other,
- raise `DebugStateError` on `status.subactor_uid != actor_uid`.
- fix a `_repl_fail_report` ref error due to making silly assumptions
  about the `_repl_fail_msg` global; now copy from global as default.
- various log-fmt and logic expression styling tweaks.
- ignore `trio.Cancelled` by default in `open_crash_handler()`.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 3e37508585 A couple more loose-egs flag flips
Namely inside,
- `ActorNursery.open_portal()` which uses
  `.trionics.maybe_open_nursery()` and is now adjusted to
  pass-through `**kwargs` for at least this flag.
- inside the `.trionics.gather_contexts()`.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 934f4531cf Disable tb colors in `._testing.mk_cmd()`
Unset the appropriate cpython osenv var such that our `pexpect` script
runs in the test suite can maintain original matching logic.
2025-03-24 21:44:59 -04:00
Tyler Goodlet 908ffc9e3e Log format tweaks for sclang reprs
A space here, a newline there..
2025-03-24 21:44:59 -04:00
Tyler Goodlet 0e76a2d88e Expose `hide_tb: bool` from `.open_nursery()`
Such that it gets passed through to `.open_root_actor()` in the
`implicit_runtime==True` case - useful for debugging cases where
`.devx._debug` APIs might be used to avoid REPL clobbering in subactors.
2025-03-24 21:44:59 -04:00
Tyler Goodlet de91ca7159 Flip to `strict_exception_groups=False` in core tns
Since it'll likely need a bit of detailing to get the test suite running
identically with strict egs (exception groups), i've opted to just flip
the switch on a few core nursery scopes for now until as such a time
i can focus enough to port the matching internals.. Xp
2025-03-24 21:44:59 -04:00
Tyler Goodlet 0bb9dfd8f8 Clean up some imports in `._clustering` 2025-03-24 21:44:59 -04:00
Tyler Goodlet 34ce5fda26 Drop `asyncio`-canc error from `._exceptions` 2025-03-24 21:44:59 -04:00
Tyler Goodlet 82544e5f4a Bump various (dev) deps and prefer sys python
Since it turns out there's a few gotchas moving to python 3.13,
- we need to pin to new(er) `trio` which now flips to strict exception
  groups (something to be handled in a follow up patch).
- since we're now using `uv` we should (at least for now) prefer the
  system `python` (over astral's distis) since they compile for
  `libedit` in terms of what the (new) `readline.backend: str` will read
  as; this will break our tab-completion and vi-mode settings in
  the `pdbp` REPL without a user configuring a `~/.editrc`
  appropriately.
- go back to using latest `pdbp` (not a local dev version) since it
  should work fine presuming the previous bullet is addressed.

Lock bumps,
- for now use latest `trio==0.29.0` (which i gotta feeling might have
  broken some existing attempts at strict-eg handling i've tried..)
- update to latest `xonsh`, `pdbp` and its dep `tabcompleter`

Other cleaning,
- put back in various deps "comments" from `poetry` content.
- drop the `xonsh-vox` and `xontrib-vox` dev deps; no `vox` support with
  `uv` rn anyway..
2025-03-24 21:44:59 -04:00
Tyler Goodlet b1018a13fe Continue supporting py3.11+
Apparently the only thing needing a guard was use of
`asyncio.Queue.shutdown()` and the paired `QueueShutDown` exception?

Cool.
2025-03-24 21:44:46 -04:00
Tyler Goodlet 90287b9875 Fix an `aio_err` ref bug 2025-03-24 21:44:09 -04:00
41 changed files with 570 additions and 277 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

@ -25,7 +25,7 @@ async def bp_then_error(
) -> None: ) -> None:
# sync with ``trio``-side (caller) task # sync with `trio`-side (caller) task
to_trio.send_nowait('start') to_trio.send_nowait('start')
# NOTE: what happens here inside the hook needs some refinement.. # NOTE: what happens here inside the hook needs some refinement..
@ -33,8 +33,7 @@ async def bp_then_error(
# we set `Lock.local_task_in_debug = 'sync'`, we probably want # we set `Lock.local_task_in_debug = 'sync'`, we probably want
# some further, at least, meta-data about the task/actor in debug # some further, at least, meta-data about the task/actor in debug
# in terms of making it clear it's `asyncio` mucking about. # in terms of making it clear it's `asyncio` mucking about.
breakpoint() breakpoint() # asyncio-side
# short checkpoint / delay # short checkpoint / delay
await asyncio.sleep(0.5) # asyncio-side await asyncio.sleep(0.5) # asyncio-side
@ -58,7 +57,6 @@ async def trio_ctx(
# this will block until the ``asyncio`` task sends a "first" # this will block until the ``asyncio`` task sends a "first"
# message, see first line in above func. # message, see first line in above func.
async with ( async with (
to_asyncio.open_channel_from( to_asyncio.open_channel_from(
bp_then_error, bp_then_error,
# raise_after_bp=not bp_before_started, # raise_after_bp=not bp_before_started,
@ -69,7 +67,7 @@ async def trio_ctx(
assert first == 'start' assert first == 'start'
if bp_before_started: if bp_before_started:
await tractor.pause() await tractor.pause() # trio-side
await ctx.started(first) # trio-side await ctx.started(first) # trio-side
@ -111,7 +109,7 @@ async def main(
# pause in parent to ensure no cross-actor # pause in parent to ensure no cross-actor
# locking problems exist! # locking problems exist!
await tractor.pause() await tractor.pause() # trio-root
if cancel_from_root: if cancel_from_root:
await ctx.cancel() await ctx.cancel()

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

@ -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,
@ -103,7 +104,16 @@ class AsyncioTaskExited(Exception):
''' '''
class TrioTaskExited(AsyncioCancelled): class TrioCancelled(Exception):
'''
Trio cancelled translation (non-base) error
for use with the `to_asyncio` module
to be raised in the `asyncio.Task` to indicate
that the `trio` side raised `Cancelled` or an error.
'''
class TrioTaskExited(Exception):
''' '''
The `trio`-side task exited without explicitly cancelling the The `trio`-side task exited without explicitly cancelling the
`asyncio.Task` peer. `asyncio.Task` peer.
@ -172,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,
@ -406,6 +417,9 @@ class RemoteActorError(Exception):
String-name of the (last hop's) boxed error type. String-name of the (last hop's) boxed error type.
''' '''
# TODO, maybe support also serializing the
# `ExceptionGroup.exeptions: list[BaseException]` set under
# certain conditions?
bt: Type[BaseException] = self.boxed_type bt: Type[BaseException] = self.boxed_type
if bt: if bt:
return str(bt.__name__) return str(bt.__name__)
@ -821,8 +835,11 @@ class MsgTypeError(
''' '''
if ( if (
(_bad_msg := self.msgdata.get('_bad_msg')) (_bad_msg := self.msgdata.get('_bad_msg'))
and and (
isinstance(_bad_msg, PayloadMsg) isinstance(_bad_msg, PayloadMsg)
or
isinstance(_bad_msg, msgtypes.Start)
)
): ):
return _bad_msg return _bad_msg
@ -1015,18 +1032,6 @@ class MessagingError(Exception):
''' '''
class AsyncioCancelled(Exception):
'''
Asyncio cancelled translation (non-base) error
for use with the ``to_asyncio`` module
to be raised in the ``trio`` side task
NOTE: this should NOT inherit from `asyncio.CancelledError` or
tests should break!
'''
def pack_error( def pack_error(
exc: BaseException|RemoteActorError, exc: BaseException|RemoteActorError,
@ -1206,7 +1211,7 @@ def is_multi_cancelled(
trio.Cancelled in ignore_nested trio.Cancelled in ignore_nested
# XXX always count-in `trio`'s native signal # XXX always count-in `trio`'s native signal
): ):
ignore_nested |= {trio.Cancelled} ignore_nested.update({trio.Cancelled})
if isinstance(exc, BaseExceptionGroup): if isinstance(exc, BaseExceptionGroup):
matched_exc: BaseExceptionGroup|None = exc.subgroup( matched_exc: BaseExceptionGroup|None = exc.subgroup(

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

@ -23,12 +23,10 @@ import asyncio
from asyncio.exceptions import ( from asyncio.exceptions import (
CancelledError, CancelledError,
) )
from asyncio import (
QueueShutDown,
)
from contextlib import asynccontextmanager as acm from contextlib import asynccontextmanager as acm
from dataclasses import dataclass from dataclasses import dataclass
import inspect import inspect
import platform
import traceback import traceback
from typing import ( from typing import (
Any, Any,
@ -79,6 +77,20 @@ __all__ = [
'run_as_asyncio_guest', 'run_as_asyncio_guest',
] ]
if (_py_313 := (
('3', '13')
==
platform.python_version_tuple()[:-1]
)
):
# 3.13+ only.. lel.
# https://docs.python.org/3.13/library/asyncio-queue.html#asyncio.QueueShutDown
from asyncio import (
QueueShutDown,
)
else:
QueueShutDown = False
# TODO, generally speaking we can generalize this abstraction, a "SC linked # TODO, generally speaking we can generalize this abstraction, a "SC linked
# parent->child task pair", as the same "supervision scope primitive" # parent->child task pair", as the same "supervision scope primitive"
@ -348,7 +360,6 @@ def _run_asyncio_task(
trio_task: trio.Task = trio.lowlevel.current_task() trio_task: trio.Task = trio.lowlevel.current_task()
trio_cs = trio.CancelScope() trio_cs = trio.CancelScope()
aio_task_complete = trio.Event() aio_task_complete = trio.Event()
aio_err: BaseException|None = None
chan = LinkedTaskChannel( chan = LinkedTaskChannel(
_to_aio=aio_q, # asyncio.Queue _to_aio=aio_q, # asyncio.Queue
@ -392,7 +403,7 @@ def _run_asyncio_task(
if ( if (
result != orig result != orig
and and
aio_err is None chan._aio_err is None
and and
# in the `open_channel_from()` case we don't # in the `open_channel_from()` case we don't
@ -576,7 +587,11 @@ def _run_asyncio_task(
# normally suppressed unless the trio.Task also errors # normally suppressed unless the trio.Task also errors
# #
# ?TODO, is this even needed (does it happen) now? # ?TODO, is this even needed (does it happen) now?
elif isinstance(aio_err, QueueShutDown): elif (
_py_313
and
isinstance(aio_err, QueueShutDown)
):
# import pdbp; pdbp.set_trace() # import pdbp; pdbp.set_trace()
trio_err = AsyncioTaskExited( trio_err = AsyncioTaskExited(
'Task exited before `trio` side' 'Task exited before `trio` side'
@ -956,9 +971,10 @@ async def translate_aio_errors(
# or an error, we ensure the aio-side gets signalled via # or an error, we ensure the aio-side gets signalled via
# an explicit exception and its `Queue` is shutdown. # an explicit exception and its `Queue` is shutdown.
if ya_trio_exited: if ya_trio_exited:
# raise `QueueShutDown` on next `Queue.get()` call on # XXX py3.13+ ONLY..
# aio side. # raise `QueueShutDown` on next `Queue.get/put()`
chan._to_aio.shutdown() if _py_313:
chan._to_aio.shutdown()
# pump this event-loop (well `Runner` but ya) # pump this event-loop (well `Runner` but ya)
# #

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