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

View File

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

View File

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

View File

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

View File

@ -1,22 +1,32 @@
from collections.abc import Awaitable, Callable
import tractor
import trio
async def breakpoint_forever():
"Indefinitely re-enter debugger in child actor."
async def breakpoint_forever() -> None:
'''
Indefinitely re-enter debugger in child actor.
'''
while True:
await trio.sleep(0.1)
await tractor.pause()
async def name_error():
"Raise a ``NameError``"
async def name_error() -> None:
'''
Raise a ``NameError``.
'''
getattr(doggypants) # noqa
async def spawn_error():
""""A nested nursery that triggers another ``NameError``.
"""
async def spawn_error() -> None:
'''
A nested nursery that triggers another ``NameError``.
'''
async with tractor.open_nursery() as an:
return await tractor.to_actor.run(
name_error,
@ -26,7 +36,8 @@ async def spawn_error():
async def main() -> None:
"""The main ``tractor`` routine.
'''
The main ``tractor`` routine.
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 ('spawn_error', '52ee14a5 ...)
`-python -m tractor._child --uid ('name_error', '3391222c ...)
"""
'''
errors: list[BaseException] = []
async with tractor.open_nursery(
@ -43,12 +55,14 @@ async def main() -> None:
# loglevel='runtime',
) 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
raised so a sibling's crash never cancels the others
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).
'''

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,8 +3,15 @@ import tractor
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:
task_status.started(cs)
for _ in range(3):
@ -30,7 +37,11 @@ async def cancellable_pause_loop(
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:
tn.cancel_scope.cancel()
try:
@ -56,7 +67,7 @@ async def pm_on_cancelled():
async def cancelled_before_pause(
):
) -> None:
'''
Verify that using a shielded pause works despite surrounding
cancellation called state in the calling task.
@ -72,6 +83,10 @@ async def cancelled_before_pause(
async def main() -> None:
'''
Exercise shielded debugger entry in subactor and root tasks.
'''
async with tractor.open_nursery(
debug_mode=True,
) as an:

View File

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

View File

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

View File

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