Polish the debugger examples

Finish the Python style, typing and docstring pass across the
debugger examples without changing their intentional breakpoints,
failures, cancellation races or timeout reproducers.

Restore full child command lines in the documented process trees
and keep the examples within the 69-column source limit.

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
wkt/big_boi_docs_472_follow_ups
Gud Boi 2026-08-29 19:01:34 -04:00
parent 96a1838210
commit 97aff52050
16 changed files with 233 additions and 88 deletions

View File

@ -13,7 +13,11 @@ import tractor
@tractor.context @tractor.context
async def sleep( async def sleep(
ctx: tractor.Context, ctx: tractor.Context,
): ) -> None:
'''
Start a context after a brief initialization delay.
'''
await trio.sleep(0.5) await trio.sleep(0.5)
await ctx.started() await ctx.started()
await trio.sleep_forever() await trio.sleep_forever()
@ -21,10 +25,13 @@ async def sleep(
async def open_ctx( async def open_ctx(
n: tractor.runtime._supervise.ActorNursery n: tractor.runtime._supervise.ActorNursery
): ) -> None:
'''
Spawn a sleeper and open a context with it.
'''
# spawn both actors # spawn both actors
portal = await n.start_actor( portal: tractor.Portal = await n.start_actor(
name='sleeper', name='sleeper',
enable_modules=[__name__], enable_modules=[__name__],
) )
@ -36,7 +43,10 @@ async def open_ctx(
async def main() -> None: async def main() -> None:
'''
Fail the root while a subactor context is still starting.
'''
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
loglevel='runtime', loglevel='runtime',

View File

@ -1,9 +1,14 @@
from collections.abc import AsyncIterator
import tractor import tractor
import trio import trio
async def breakpoint_forever(): async def breakpoint_forever() -> AsyncIterator[str]:
"Indefinitely re-enter debugger in child actor." '''
Indefinitely re-enter debugger in child actor.
'''
try: try:
while True: while True:
yield 'yo' yield 'yo'
@ -15,8 +20,11 @@ async def breakpoint_forever():
raise raise
async def name_error(): async def name_error() -> None:
"Raise a ``NameError``" '''
Raise a ``NameError``.
'''
getattr(doggypants) # noqa getattr(doggypants) # noqa
@ -28,8 +36,14 @@ async def main() -> None:
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
) as an: ) as an:
p0 = await an.start_actor('bp_forever', enable_modules=[__name__]) p0: tractor.Portal = await an.start_actor(
p1 = await an.start_actor('name_error', enable_modules=[__name__]) 'bp_forever',
enable_modules=[__name__],
)
p1: tractor.Portal = await an.start_actor(
'name_error',
enable_modules=[__name__],
)
# retreive results # retreive results
async with p0.open_stream_from(breakpoint_forever) as stream: async with p0.open_stream_from(breakpoint_forever) as stream:
@ -40,6 +54,7 @@ async def main() -> None:
except tractor.RemoteActorError as rae: except tractor.RemoteActorError as rae:
assert rae.boxed_type is NameError assert rae.boxed_type is NameError
i: str
async for i in stream: async for i in stream:
# a second time try the failing subactor and this tie # a second time try the failing subactor and this tie

View File

@ -4,13 +4,19 @@ import trio
import tractor import tractor
async def name_error(): async def name_error() -> None:
"Raise a ``NameError``" '''
Raise a ``NameError``.
'''
getattr(doggypants) # noqa getattr(doggypants) # noqa
async def breakpoint_forever(): async def breakpoint_forever() -> None:
"Indefinitely re-enter debugger in child actor." '''
Indefinitely re-enter debugger in child actor.
'''
while True: while True:
await tractor.pause() await tractor.pause()
@ -20,9 +26,13 @@ async def breakpoint_forever():
# await trio.sleep(0) # await trio.sleep(0)
async def spawn_until(depth=0): async def spawn_until(
""""A nested nursery that triggers another ``NameError``. depth: int = 0,
""" ) -> None:
'''
A nested nursery that triggers another ``NameError``.
'''
async with ( async with (
tractor.open_nursery() as an, tractor.open_nursery() as an,
trio.open_nursery() as tn, trio.open_nursery() as tn,
@ -37,7 +47,8 @@ async def spawn_until(depth=0):
) )
) )
# Let the background one-shot enter `breakpoint_forever()` # Let the background one-shot enter
# `breakpoint_forever()`
# before its sibling raises and cancellation propagates. # before its sibling raises and cancellation propagates.
await trio.sleep(0.5) await trio.sleep(0.5)
# rx and propagate error from child # rx and propagate error from child
@ -48,9 +59,9 @@ async def spawn_until(depth=0):
) )
else: else:
# recusrive call to spawn another process branching layer of # recusrive call to spawn another process branching
# the tree; blocks (up) each level until the leaf's # layer of the tree; blocks (up) each level until the
# `name_error` relays through. # leaf's `name_error` relays through.
depth -= 1 depth -= 1
await tractor.to_actor.run( await tractor.to_actor.run(
partial( partial(
@ -64,24 +75,34 @@ async def spawn_until(depth=0):
# TODO: notes on the new boxed-relayed errors through proxy actors # TODO: notes on the new boxed-relayed errors through proxy actors
async def main() -> None: async def main() -> None:
"""The main ``tractor`` routine. '''
The main ``tractor`` routine.
The process tree should look as approximately as follows when the debugger The process tree should look approximately as follows when the
first engages: debugger first engages:
python examples/debugging/multi_nested_subactors_bp_forever.py python examples/debugging/multi_nested_subactors_bp_forever.py
python -m tractor._child --uid ('spawner1', '7eab8462 ...) python -m tractor._child --uid
python -m tractor._child --uid ('spawn_until_3', 'afcba7a8 ...) ('spawner1', '7eab8462 ...')
python -m tractor._child --uid ('spawn_until_2', 'd2433d13 ...) python -m tractor._child --uid
python -m tractor._child --uid ('spawn_until_1', '1df589de ...) ('spawn_until_3', 'afcba7a8 ...')
python -m tractor._child --uid ('spawn_until_0', '3720602b ...) python -m tractor._child --uid
('spawn_until_2', 'd2433d13 ...')
python -m tractor._child --uid
('spawn_until_1', '1df589de ...')
python -m tractor._child --uid
('spawn_until_0', '3720602b ...')
python -m tractor._child --uid ('spawner0', '1d42012b ...) python -m tractor._child --uid
python -m tractor._child --uid ('spawn_until_2', '2877e155 ...) ('spawner0', '1d42012b ...')
python -m tractor._child --uid ('spawn_until_1', '0502d786 ...) python -m tractor._child --uid
python -m tractor._child --uid ('spawn_until_0', 'de918e6d ...) ('spawn_until_2', '2877e155 ...')
python -m tractor._child --uid
('spawn_until_1', '0502d786 ...')
python -m tractor._child --uid
('spawn_until_0', 'de918e6d ...')
""" '''
async with ( async with (
tractor.open_nursery( tractor.open_nursery(
debug_mode=True, debug_mode=True,

View File

@ -7,14 +7,19 @@ import trio
import tractor import tractor
async def name_error(): async def name_error() -> None:
"Raise a ``NameError``" '''
Raise a ``NameError``.
'''
getattr(doggypants) # noqa getattr(doggypants) # noqa
async def spawn_error(): async def spawn_error() -> None:
""""A nested nursery that triggers another ``NameError``. '''
""" A nested nursery that triggers another ``NameError``.
'''
async with tractor.open_nursery() as an: async with tractor.open_nursery() as an:
return await tractor.to_actor.run( return await tractor.to_actor.run(
name_error, name_error,
@ -24,7 +29,8 @@ async def spawn_error():
async def main() -> None: async def main() -> None:
"""The main ``tractor`` routine. '''
The main ``tractor`` routine.
The process tree should look as approximately as follows: The process tree should look as approximately as follows:
@ -37,7 +43,8 @@ async def main() -> None:
- nested name_error sub-sub-actor - nested name_error sub-sub-actor
- root actor should then fail on assert - root actor should then fail on assert
- program termination - program termination
"""
'''
async with ( async with (
tractor.open_nursery( tractor.open_nursery(
debug_mode=True, debug_mode=True,
@ -46,11 +53,11 @@ async def main() -> None:
trio.open_nursery() as tn, trio.open_nursery() as tn,
): ):
# spawn both actors.. # spawn both actors..
portal = await an.start_actor( portal: tractor.Portal = await an.start_actor(
'name_error', 'name_error',
enable_modules=[__name__], enable_modules=[__name__],
) )
portal1 = await an.start_actor( portal1: tractor.Portal = await an.start_actor(
'spawn_error', 'spawn_error',
enable_modules=[__name__], enable_modules=[__name__],
) )

View File

@ -1,22 +1,32 @@
from collections.abc import Awaitable, Callable
import tractor import tractor
import trio import trio
async def breakpoint_forever(): async def breakpoint_forever() -> None:
"Indefinitely re-enter debugger in child actor." '''
Indefinitely re-enter debugger in child actor.
'''
while True: while True:
await trio.sleep(0.1) await trio.sleep(0.1)
await tractor.pause() await tractor.pause()
async def name_error(): async def name_error() -> None:
"Raise a ``NameError``" '''
Raise a ``NameError``.
'''
getattr(doggypants) # noqa getattr(doggypants) # noqa
async def spawn_error(): async def spawn_error() -> None:
""""A nested nursery that triggers another ``NameError``. '''
""" A nested nursery that triggers another ``NameError``.
'''
async with tractor.open_nursery() as an: async with tractor.open_nursery() as an:
return await tractor.to_actor.run( return await tractor.to_actor.run(
name_error, name_error,
@ -26,7 +36,8 @@ async def spawn_error():
async def main() -> None: async def main() -> None:
"""The main ``tractor`` routine. '''
The main ``tractor`` routine.
The process tree should look as approximately as follows: The process tree should look as approximately as follows:
@ -35,7 +46,8 @@ async def main() -> None:
|-python -m tractor._child --uid ('bp_forever', '1f787a7e ...) |-python -m tractor._child --uid ('bp_forever', '1f787a7e ...)
`-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...) `-python -m tractor._child --uid ('spawn_error', '52ee14a5 ...)
`-python -m tractor._child --uid ('name_error', '3391222c ...) `-python -m tractor._child --uid ('name_error', '3391222c ...)
"""
'''
errors: list[BaseException] = [] errors: list[BaseException] = []
async with tractor.open_nursery( async with tractor.open_nursery(
@ -43,12 +55,14 @@ async def main() -> None:
# loglevel='runtime', # loglevel='runtime',
) as an: ) as an:
async def run_and_collect(fn): async def run_and_collect(
fn: Callable[[], Awaitable[object]],
) -> None:
''' '''
One-shot whose (boxed) error is stashed instead of One-shot whose (boxed) error is stashed instead of
raised so a sibling's crash never cancels the others raised so a sibling's crash never cancels the others
before they've had their own debugger sessions (the before they've had their own debugger sessions (the
"collect all errors" the legacy `run_in_actor()` API 'collect all errors' the legacy `run_in_actor()` API
did implicitly at nursery teardown). did implicitly at nursery teardown).
''' '''

View File

@ -1,19 +1,28 @@
import trio import trio
import tractor import tractor
async def die():
async def die() -> None:
'''
Deliberately crash the calling actor.
'''
raise RuntimeError raise RuntimeError
async def main() -> None: async def main() -> None:
'''
Crash actors with different debugger settings concurrently.
'''
async with tractor.open_nursery() as an: async with tractor.open_nursery() as an:
debug_actor = await an.start_actor( debug_actor: tractor.Portal = await an.start_actor(
'debugged_boi', 'debugged_boi',
enable_modules=[__name__], enable_modules=[__name__],
debug_mode=True, debug_mode=True,
) )
crash_boi = await an.start_actor( crash_boi: tractor.Portal = await an.start_actor(
'crash_boi', 'crash_boi',
enable_modules=[__name__], enable_modules=[__name__],
# debug_mode=True, # debug_mode=True,

View File

@ -5,7 +5,7 @@ import tractor
@tractor.context @tractor.context
async def name_error( async def name_error(
ctx: tractor.Context, ctx: tractor.Context,
): ) -> None:
''' '''
Raise a `NameError`, catch it and enter `.post_mortem()`, then Raise a `NameError`, catch it and enter `.post_mortem()`, then
expect the `._rpc._invoke()` crash handler to also engage. expect the `._rpc._invoke()` crash handler to also engage.
@ -49,7 +49,9 @@ async def main() -> None:
await tractor.post_mortem() await tractor.post_mortem()
raise raise
else: else:
raise RuntimeError('IPC ctx should have remote errored!?') raise RuntimeError(
'IPC ctx should have remote errored!?'
)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -3,7 +3,10 @@ import tractor
async def main() -> None: async def main() -> None:
'''
Pause in the root actor to exercise its debugger REPL.
'''
async with tractor.open_root_actor( async with tractor.open_root_actor(
debug_mode=True, debug_mode=True,
): ):

View File

@ -3,6 +3,10 @@ import tractor
async def main() -> None: async def main() -> None:
'''
Raise an assertion error from the debug-enabled root actor.
'''
async with tractor.open_root_actor( async with tractor.open_root_actor(
debug_mode=True, debug_mode=True,
): ):

View File

@ -4,14 +4,21 @@ import trio
import tractor import tractor
async def name_error(): async def name_error() -> None:
"Raise a ``NameError``" '''
Raise a ``NameError``.
'''
getattr(doggypants) # noqa getattr(doggypants) # noqa
async def spawn_until(depth=0): async def spawn_until(
""""A nested nursery that triggers another ``NameError``. depth: int = 0,
""" ) -> None:
'''
A nested nursery that triggers another ``NameError``.
'''
async with tractor.open_nursery() as an: async with tractor.open_nursery() as an:
if depth < 1: if depth < 1:
await tractor.to_actor.run(name_error, an=an) await tractor.to_actor.run(name_error, an=an)
@ -33,12 +40,17 @@ async def main() -> None:
debugger first engages: debugger first engages:
python examples/debugging/multi_nested_subactors_bp_forever.py python examples/debugging/multi_nested_subactors_bp_forever.py
python -m tractor._child --uid ('spawner1', '7eab8462 ...) python -m tractor._child --uid
python -m tractor._child --uid ('spawn_until_0', '3720602b ...) ('spawner1', '7eab8462 ...')
python -m tractor._child --uid ('name_error', '505bf71d ...) python -m tractor._child --uid
('spawn_until_0', '3720602b ...')
python -m tractor._child --uid
('name_error', '505bf71d ...')
python -m tractor._child --uid ('spawner0', '1d42012b ...) python -m tractor._child --uid
python -m tractor._child --uid ('name_error', '6c2733b8 ...) ('spawner0', '1d42012b ...')
python -m tractor._child --uid
('name_error', '6c2733b8 ...')
''' '''
async with ( async with (

View File

@ -3,6 +3,10 @@ import tractor
async def main() -> None: async def main() -> None:
'''
Enter shielded debugging after root cancellation, then fail.
'''
async with tractor.open_root_actor( async with tractor.open_root_actor(
debug_mode=True, debug_mode=True,
loglevel='cancel', loglevel='cancel',
@ -18,16 +22,19 @@ async def main() -> None:
try: try:
await tractor.pause() await tractor.pause()
except trio.Cancelled as _taskc: except trio.Cancelled as _taskc:
assert (root_cs := _root._root_tn.cancel_scope).cancel_called root_cs: trio.CancelScope
assert (
root_cs := _root._root_tn.cancel_scope
).cancel_called
# NOTE^^ above logic but inside `open_root_actor()` and # NOTE^^ above logic but inside `open_root_actor()` and
# passed to the `shield=` expression is effectively what # passed to the `shield=` expression is effectively what
# we're testing here! # we're testing here!
await tractor.pause(shield=root_cs.cancel_called) await tractor.pause(shield=root_cs.cancel_called)
# XXX, if shield logic *is wrong* inside `open_root_actor()`'s # XXX, if shield logic *is wrong* inside
# crash-handler block this should never be interacted, # `open_root_actor()`'s crash-handler block this should never
# instead `trio.Cancelled` would be bubbled up: the original # be interacted, instead `trio.Cancelled` would be bubbled
# BUG. # up: the original BUG.
assert 0 assert 0

View File

@ -2,8 +2,11 @@ import trio
import tractor import tractor
async def key_error(): async def key_error() -> None:
"Raise a ``NameError``" '''
Raise a ``KeyError``.
'''
return {}['doggy'] return {}['doggy']
@ -21,7 +24,7 @@ async def main() -> None:
trio.open_nursery() as tn, trio.open_nursery() as tn,
): ):
# spawn the actor.. # spawn the actor..
portal = await an.start_actor( portal: tractor.Portal = await an.start_actor(
'key_error', 'key_error',
enable_modules=[__name__], enable_modules=[__name__],
) )
@ -32,9 +35,9 @@ async def main() -> None:
# root blocks below. # root blocks below.
tn.start_soon(portal.run, key_error) tn.start_soon(portal.run, key_error)
# XXX: originally a bug caused by this is where root would enter # XXX: originally a bug caused by this is where root would
# the debugger and clobber the tty used by the repl even though # enter the debugger and clobber the tty used by the repl
# child should have it locked. # even though child should have it locked.
with trio.fail_after(1): with trio.fail_after(1):
await trio.Event().wait() await trio.Event().wait()

View File

@ -3,8 +3,15 @@ import tractor
async def cancellable_pause_loop( async def cancellable_pause_loop(
task_status: trio.TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED task_status: trio.TaskStatus[
): trio.CancelScope
] = trio.TASK_STATUS_IGNORED,
) -> None:
'''
Exercise shielded debugger pauses under cancellation.
'''
cs: trio.CancelScope
with trio.CancelScope() as cs: with trio.CancelScope() as cs:
task_status.started(cs) task_status.started(cs)
for _ in range(3): for _ in range(3):
@ -30,7 +37,11 @@ async def cancellable_pause_loop(
await trio.lowlevel.checkpoint() await trio.lowlevel.checkpoint()
async def pm_on_cancelled(): async def pm_on_cancelled() -> None:
'''
Compare shielded and unshielded post-mortem entry.
'''
async with trio.open_nursery() as tn: async with trio.open_nursery() as tn:
tn.cancel_scope.cancel() tn.cancel_scope.cancel()
try: try:
@ -56,7 +67,7 @@ async def pm_on_cancelled():
async def cancelled_before_pause( async def cancelled_before_pause(
): ) -> None:
''' '''
Verify that using a shielded pause works despite surrounding Verify that using a shielded pause works despite surrounding
cancellation called state in the calling task. cancellation called state in the calling task.
@ -72,6 +83,10 @@ async def cancelled_before_pause(
async def main() -> None: async def main() -> None:
'''
Exercise shielded debugger entry in subactor and root tasks.
'''
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
) as an: ) as an:

View File

@ -1,10 +1,15 @@
import platform import platform
from collections.abc import AsyncIterator
import tractor import tractor
import trio import trio
async def gen(): async def gen() -> AsyncIterator[str]:
'''
Yield values around debugger pauses.
'''
yield 'yo' yield 'yo'
await tractor.pause() await tractor.pause()
yield 'yo' yield 'yo'
@ -15,11 +20,15 @@ async def gen():
async def just_bp( async def just_bp(
ctx: tractor.Context, ctx: tractor.Context,
) -> None: ) -> None:
'''
Pause repeatedly before deliberately breaking the context.
'''
await ctx.started() await ctx.started()
await tractor.pause() await tractor.pause()
# TODO: bps and errors in this call.. # TODO: bps and errors in this call..
val: str
async for val in gen(): async for val in gen():
print(val) print(val)
@ -35,14 +44,17 @@ async def just_bp(
async def main() -> None: async def main() -> None:
'''
Run the breakpoint context over a supported transport.
'''
# !TODO, parametrize the --tpt-proto={key} with osenv vars just # !TODO, parametrize the --tpt-proto={key} with osenv vars just
# like we do for loglevel/spawn-backend! # like we do for loglevel/spawn-backend!
# - [ ] run on both tpts for all such debugger tests? # - [ ] run on both tpts for all such debugger tests?
# - [ ] special skip for macos! # - [ ] special skip for macos!
# #
if platform.system() != 'Darwin': if platform.system() != 'Darwin':
tpt = 'uds' tpt: str = 'uds'
else: else:
# XXX, precisely we can't use pytest's tmp-path generation # XXX, precisely we can't use pytest's tmp-path generation
# for tests.. apparently because: # for tests.. apparently because:
@ -59,7 +71,7 @@ async def main() -> None:
enable_transports=[tpt], enable_transports=[tpt],
loglevel='devx', loglevel='devx',
) as an: ) as an:
p = await an.start_actor( p: tractor.Portal = await an.start_actor(
'bp_boi', 'bp_boi',
enable_modules=[__name__], enable_modules=[__name__],
) )

View File

@ -2,7 +2,7 @@ import trio
import tractor import tractor
async def breakpoint_forever(): async def breakpoint_forever() -> None:
''' '''
Indefinitely re-enter debugger in child actor. Indefinitely re-enter debugger in child actor.
@ -13,7 +13,10 @@ async def breakpoint_forever():
async def main() -> None: async def main() -> None:
'''
Run a subactor that repeatedly pauses in the debugger.
'''
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
loglevel='cancel', loglevel='cancel',

View File

@ -2,11 +2,19 @@ import trio
import tractor import tractor
async def name_error(): async def name_error() -> None:
'''
Deliberately raise a ``NameError`` in a subactor.
'''
getattr(doggypants) # noqa (on purpose) getattr(doggypants) # noqa (on purpose)
async def main() -> None: async def main() -> None:
'''
Surface a subactor `NameError` at the waiting root task.
'''
async with tractor.open_nursery( async with tractor.open_nursery(
debug_mode=True, debug_mode=True,
) as an: ) as an: