Python 3.13 support #18
			
				
			
		
		
		
	|  | @ -62,7 +62,9 @@ async def recv_and_spawn_net_killers( | |||
|     await ctx.started() | ||||
|     async with ( | ||||
|         ctx.open_stream() as stream, | ||||
|         trio.open_nursery() as n, | ||||
|         trio.open_nursery( | ||||
|             strict_exception_groups=False, | ||||
|         ) as tn, | ||||
|     ): | ||||
|         async for i in stream: | ||||
|             print(f'child echoing {i}') | ||||
|  | @ -77,11 +79,11 @@ async def recv_and_spawn_net_killers( | |||
|                 i >= break_ipc_after | ||||
|             ): | ||||
|                 broke_ipc = True | ||||
|                 n.start_soon( | ||||
|                 tn.start_soon( | ||||
|                     iter_ipc_stream, | ||||
|                     stream, | ||||
|                 ) | ||||
|                 n.start_soon( | ||||
|                 tn.start_soon( | ||||
|                     partial( | ||||
|                         break_ipc_then_error, | ||||
|                         stream=stream, | ||||
|  |  | |||
|  | @ -21,11 +21,13 @@ async def name_error(): | |||
| 
 | ||||
| 
 | ||||
| async def main(): | ||||
|     """Test breakpoint in a streaming actor. | ||||
|     """ | ||||
|     ''' | ||||
|     Test breakpoint in a streaming actor. | ||||
| 
 | ||||
|     ''' | ||||
|     async with tractor.open_nursery( | ||||
|         debug_mode=True, | ||||
|         # loglevel='cancel', | ||||
|         loglevel='cancel', | ||||
|         # loglevel='devx', | ||||
|     ) as n: | ||||
| 
 | ||||
|  |  | |||
|  | @ -40,7 +40,7 @@ async def main(): | |||
|     """ | ||||
|     async with tractor.open_nursery( | ||||
|         debug_mode=True, | ||||
|         # loglevel='cancel', | ||||
|         loglevel='devx', | ||||
|     ) as n: | ||||
| 
 | ||||
|         # spawn both actors | ||||
|  |  | |||
|  | @ -32,8 +32,7 @@ async def main() -> None: | |||
|             f'$PYTHONOBREAKPOINT: {pybp_var!r}\n' | ||||
|             f'`sys.breakpointhook`: {pybp_hook!r}\n' | ||||
|         ) | ||||
|         breakpoint() | ||||
|         pass  # first bp, tractor hook set. | ||||
|         breakpoint()  # first bp, tractor hook set. | ||||
| 
 | ||||
|     # XXX AFTER EXIT (of actor-runtime) verify the hook is unset.. | ||||
|     # | ||||
|  | @ -43,8 +42,7 @@ async def main() -> None: | |||
|     assert sys.breakpointhook | ||||
| 
 | ||||
|     # now ensure a regular builtin pause still works | ||||
|     breakpoint() | ||||
|     pass  # last bp, stdlib hook restored | ||||
|     breakpoint()  # last bp, stdlib hook restored | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|  |  | |||
|  | @ -91,7 +91,7 @@ async def main() -> list[int]: | |||
|     an: ActorNursery | ||||
|     async with tractor.open_nursery( | ||||
|         loglevel='cancel', | ||||
|         debug_mode=True, | ||||
|         # debug_mode=True, | ||||
|     ) as an: | ||||
| 
 | ||||
|         seed = int(1e3) | ||||
|  |  | |||
|  | @ -3,20 +3,18 @@ import trio | |||
| import tractor | ||||
| 
 | ||||
| 
 | ||||
| async def sleepy_jane(): | ||||
|     uid = tractor.current_actor().uid | ||||
| async def sleepy_jane() -> None: | ||||
|     uid: tuple = tractor.current_actor().uid | ||||
|     print(f'Yo i am actor {uid}') | ||||
|     await trio.sleep_forever() | ||||
| 
 | ||||
| 
 | ||||
| async def main(): | ||||
|     ''' | ||||
|     Spawn a flat actor cluster, with one process per | ||||
|     detected core. | ||||
|     Spawn a flat actor cluster, with one process per detected core. | ||||
| 
 | ||||
|     ''' | ||||
|     portal_map: dict[str, tractor.Portal] | ||||
|     results: dict[str, str] | ||||
| 
 | ||||
|     # look at this hip new syntax! | ||||
|     async with ( | ||||
|  | @ -25,11 +23,16 @@ async def main(): | |||
|             modules=[__name__] | ||||
|         ) as portal_map, | ||||
| 
 | ||||
|         trio.open_nursery() as n, | ||||
|         trio.open_nursery( | ||||
|             strict_exception_groups=False, | ||||
|         ) as tn, | ||||
|     ): | ||||
| 
 | ||||
|         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) | ||||
| 
 | ||||
|  | @ -41,4 +44,4 @@ if __name__ == '__main__': | |||
|     try: | ||||
|         trio.run(main) | ||||
|     except KeyboardInterrupt: | ||||
|         pass | ||||
|         print('trio cancelled by KBI') | ||||
|  |  | |||
|  | @ -37,16 +37,14 @@ dependencies = [ | |||
|   # 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 | ||||
|   # disable strict egs or port to handling them internally! | ||||
|   # trio='^0.27' | ||||
|   "trio>=0.24,<0.25", | ||||
|   "trio>0.27", | ||||
|   "tricycle>=0.4.1,<0.5", | ||||
|   "wrapt>=1.16.0,<2", | ||||
|   "colorlog>=6.8.2,<7", | ||||
|   # built-in multi-actor `pdb` REPL | ||||
|   "pdbp>=1.5.0,<2", | ||||
|   "pdbp>=1.6,<2", # windows only (from `pdbp`) | ||||
|   # typed IPC msging | ||||
|   # TODO, get back on release once 3.13 support is out! | ||||
|   "msgspec", | ||||
|   "msgspec>=0.19.0", | ||||
| ] | ||||
| 
 | ||||
| # ------ project ------ | ||||
|  | @ -56,18 +54,14 @@ dev = [ | |||
|   # test suite | ||||
|   # TODO: maybe some of these layout choices? | ||||
|   # 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", | ||||
|   # `tractor.devx` tooling | ||||
|   "greenback>=1.2.1,<2", | ||||
|   "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", | ||||
|   "prompt-toolkit>=3.0.50", | ||||
|   "xonsh>=0.19.2", | ||||
| ] | ||||
| # TODO, add these with sane versions; were originally in | ||||
| # `requirements-docs.txt`.. | ||||
|  | @ -78,21 +72,39 @@ dev = [ | |||
| 
 | ||||
| # ------ dependency-groups ------ | ||||
| 
 | ||||
| # ------ dependency-groups ------ | ||||
| 
 | ||||
| [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 ------ | ||||
| # TODO, distributed (multi-host) extensions | ||||
| # linux kernel networking | ||||
| # '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] | ||||
| include = ["tractor"] | ||||
| 
 | ||||
| [tool.hatch.build.targets.wheel] | ||||
| include = ["tractor"] | ||||
| 
 | ||||
| # ------ dependency-groups ------ | ||||
| # ------ tool.hatch ------ | ||||
| 
 | ||||
| [tool.towncrier] | ||||
| package = "tractor" | ||||
|  | @ -142,3 +154,5 @@ log_cli = false | |||
| # TODO: maybe some of these layout choices? | ||||
| # https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules | ||||
| # pythonpath = "src" | ||||
| 
 | ||||
| # ------ tool.pytest ------ | ||||
|  |  | |||
|  | @ -75,7 +75,10 @@ def pytest_configure(config): | |||
| 
 | ||||
| @pytest.fixture(scope='session') | ||||
| 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) | ||||
|  | @ -92,6 +95,12 @@ def spawn_backend(request) -> str: | |||
|     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) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -309,10 +309,13 @@ def test_subactor_breakpoint( | |||
|     child.expect(EOF) | ||||
| 
 | ||||
|     assert in_prompt_msg( | ||||
|         child, | ||||
|         ['RemoteActorError:', | ||||
|         child, [ | ||||
|         'MessagingError:', | ||||
|         'RemoteActorError:', | ||||
|          "('breakpoint_forever'", | ||||
|          'bdb.BdbQuit',] | ||||
|          'bdb.BdbQuit', | ||||
|         ], | ||||
|         pause_on_false=True, | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ Sketchy network blackoutz, ugly byzantine gens, puedes eschuchar la | |||
| cancelacion?.. | ||||
| 
 | ||||
| ''' | ||||
| import itertools | ||||
| from functools import partial | ||||
| from types import ModuleType | ||||
| 
 | ||||
|  | @ -230,13 +229,10 @@ def test_ipc_channel_break_during_stream( | |||
|     # get raw instance from pytest wrapper | ||||
|     value = excinfo.value | ||||
|     if isinstance(value, ExceptionGroup): | ||||
|         value = next( | ||||
|             itertools.dropwhile( | ||||
|                 lambda exc: not isinstance(exc, expect_final_exc), | ||||
|                 value.exceptions, | ||||
|             ) | ||||
|         ) | ||||
|         assert value | ||||
|         excs = value.exceptions | ||||
|         assert len(excs) == 1 | ||||
|         final_exc = excs[0] | ||||
|         assert isinstance(final_exc, expect_final_exc) | ||||
| 
 | ||||
| 
 | ||||
| @tractor.context | ||||
|  | @ -259,15 +255,16 @@ async def break_ipc_after_started( | |||
| 
 | ||||
| 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 | ||||
|     the parent can trigger a SIGINT and the child will be reaped out-of-IPC by | ||||
|     the localhost process supervision machinery: aka "zombie lord". | ||||
|     Verify that is a subactor's IPC goes down just after bringing up | ||||
|     a stream the parent can trigger a SIGINT and the child will be | ||||
|     reaped out-of-IPC by the localhost process supervision machinery: | ||||
|     aka "zombie lord". | ||||
| 
 | ||||
|     ''' | ||||
|     async def main(): | ||||
|         with trio.fail_after(3): | ||||
|             async with tractor.open_nursery() as n: | ||||
|                 portal = await n.start_actor( | ||||
|             async with tractor.open_nursery() as an: | ||||
|                 portal = await an.start_actor( | ||||
|                     'ipc_breaker', | ||||
|                     enable_modules=[__name__], | ||||
|                 ) | ||||
|  |  | |||
|  | @ -307,7 +307,15 @@ async def inf_streamer( | |||
| 
 | ||||
|     async with ( | ||||
|         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 for msg in stream: | ||||
|  |  | |||
|  | @ -519,7 +519,9 @@ def test_cancel_via_SIGINT_other_task( | |||
|     async def main(): | ||||
|         # should never timeout since SIGINT should cancel the current program | ||||
|         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) | ||||
|                 if 'mp' in spawn_backend: | ||||
|                     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) | ||||
| 
 | ||||
|                     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: | ||||
|             duration = time.time() - start | ||||
|             if duration > timeout: | ||||
|  |  | |||
|  | @ -874,13 +874,13 @@ def chk_pld_type( | |||
|     return roundtrip | ||||
| 
 | ||||
| 
 | ||||
| def test_limit_msgspec(): | ||||
| 
 | ||||
| def test_limit_msgspec( | ||||
|     debug_mode: bool, | ||||
| ): | ||||
|     async def main(): | ||||
|         async with tractor.open_root_actor( | ||||
|             debug_mode=True | ||||
|             debug_mode=debug_mode, | ||||
|         ): | ||||
| 
 | ||||
|             # ensure we can round-trip a boxing `PayloadMsg` | ||||
|             assert chk_pld_type( | ||||
|                 payload_spec=Any, | ||||
|  |  | |||
|  | @ -95,8 +95,8 @@ async def trio_main( | |||
| 
 | ||||
|     # stash a "service nursery" as "actor local" (aka a Python global) | ||||
|     global _nursery | ||||
|     n = _nursery | ||||
|     assert n | ||||
|     tn = _nursery | ||||
|     assert tn | ||||
| 
 | ||||
|     async def consume_stream(): | ||||
|         async with wrapper_mngr() as stream: | ||||
|  | @ -104,10 +104,10 @@ async def trio_main( | |||
|                 print(msg) | ||||
| 
 | ||||
|     # run 2 tasks to ensure broadcaster chan use | ||||
|     n.start_soon(consume_stream) | ||||
|     n.start_soon(consume_stream) | ||||
|     tn.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() | ||||
| 
 | ||||
|  | @ -117,8 +117,10 @@ async def open_actor_local_nursery( | |||
|     ctx: tractor.Context, | ||||
| ): | ||||
|     global _nursery | ||||
|     async with trio.open_nursery() as n: | ||||
|         _nursery = n | ||||
|     async with trio.open_nursery( | ||||
|         strict_exception_groups=False, | ||||
|     ) as tn: | ||||
|         _nursery = tn | ||||
|         await ctx.started() | ||||
|         await trio.sleep(10) | ||||
|         # await trio.sleep(1) | ||||
|  | @ -132,7 +134,7 @@ async def open_actor_local_nursery( | |||
|         # never yields back.. aka a scenario where the | ||||
|         # ``tractor.context`` task IS NOT in the service n's cancel | ||||
|         # scope. | ||||
|         n.cancel_scope.cancel() | ||||
|         tn.cancel_scope.cancel() | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|  | @ -157,7 +159,7 @@ def test_actor_managed_trio_nursery_task_error_cancels_aio( | |||
|         async with tractor.open_nursery() as n: | ||||
|             p = await n.start_actor( | ||||
|                 'nursery_mngr', | ||||
|                 infect_asyncio=asyncio_mode, | ||||
|                 infect_asyncio=asyncio_mode,  # TODO, is this enabling debug mode? | ||||
|                 enable_modules=[__name__], | ||||
|             ) | ||||
|             async with ( | ||||
|  |  | |||
|  | @ -181,7 +181,9 @@ async def spawn_and_check_registry( | |||
| 
 | ||||
|             try: | ||||
|                 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 = {} | ||||
|                         for i in range(3): | ||||
|  | @ -316,7 +318,9 @@ async def close_chans_before_nursery( | |||
|                         async with portal2.open_stream_from( | ||||
|                             stream_forever | ||||
|                         ) 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(cancel, use_signal, .5) | ||||
|                                 try: | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ from tractor._testing import ( | |||
| @pytest.fixture | ||||
| def run_example_in_subproc( | ||||
|     loglevel: str, | ||||
|     testdir: pytest.Testdir, | ||||
|     testdir: pytest.Pytester, | ||||
|     reg_addr: tuple[str, int], | ||||
| ): | ||||
| 
 | ||||
|  | @ -81,28 +81,36 @@ def run_example_in_subproc( | |||
| 
 | ||||
|     # 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 | ||||
|         and f[0] != '_' | ||||
|         and 'debugging' not in p[0] | ||||
|         and 'integration' not in p[0] | ||||
|         and 'advanced_faults' not in p[0] | ||||
|         and 'multihost' not in p[0] | ||||
|         if ( | ||||
|             '__' not in f | ||||
|             and f[0] != '_' | ||||
|             and 'debugging' not in p[0] | ||||
|             and 'integration' not in p[0] | ||||
|             and 'advanced_faults' not in p[0] | ||||
|             and 'multihost' not in p[0] | ||||
|         ) | ||||
|     ], | ||||
| 
 | ||||
|     ids=lambda t: t[1], | ||||
| ) | ||||
| def test_example(run_example_in_subproc, example_script): | ||||
|     """Load and run scripts from this repo's ``examples/`` dir as a user | ||||
| def test_example( | ||||
|     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. | ||||
| 
 | ||||
|     On windows a little more "finessing" is done to make | ||||
|     ``multiprocessing`` play nice: we copy the ``__main__.py`` into the | ||||
|     test directory and invoke the script as a module with ``python -m | ||||
|     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): | ||||
|         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 | ||||
|                     # a better way to be explicit about aio side | ||||
|                     # cancels? | ||||
|                     and 'asyncio.exceptions.CancelledError' not in last_error | ||||
|                     and | ||||
|                     'asyncio.exceptions.CancelledError' not in last_error | ||||
|                 ): | ||||
|                     raise Exception(errmsg) | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,7 +2,9 @@ | |||
| Broadcast channels for fan-out to local tasks. | ||||
| 
 | ||||
| """ | ||||
| from contextlib import asynccontextmanager | ||||
| from contextlib import ( | ||||
|     asynccontextmanager as acm, | ||||
| ) | ||||
| from functools import partial | ||||
| from itertools import cycle | ||||
| import time | ||||
|  | @ -15,6 +17,7 @@ import tractor | |||
| from tractor.trionics import ( | ||||
|     broadcast_receiver, | ||||
|     Lagged, | ||||
|     collapse_eg, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -62,7 +65,7 @@ async def ensure_sequence( | |||
|                 break | ||||
| 
 | ||||
| 
 | ||||
| @asynccontextmanager | ||||
| @acm | ||||
| async def open_sequence_streamer( | ||||
| 
 | ||||
|     sequence: list[int], | ||||
|  | @ -74,9 +77,9 @@ async def open_sequence_streamer( | |||
|     async with tractor.open_nursery( | ||||
|         arbiter_addr=reg_addr, | ||||
|         start_method=start_method, | ||||
|     ) as tn: | ||||
|     ) as an: | ||||
| 
 | ||||
|         portal = await tn.start_actor( | ||||
|         portal = await an.start_actor( | ||||
|             'sequence_echoer', | ||||
|             enable_modules=[__name__], | ||||
|         ) | ||||
|  | @ -155,9 +158,12 @@ def test_consumer_and_parent_maybe_lag( | |||
|         ) as stream: | ||||
| 
 | ||||
|             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, | ||||
|                         stream, | ||||
|                         sequence.copy(), | ||||
|  | @ -230,8 +236,8 @@ def test_faster_task_to_recv_is_cancelled_by_slower( | |||
| 
 | ||||
|         ) as stream: | ||||
| 
 | ||||
|             async with trio.open_nursery() as n: | ||||
|                 n.start_soon( | ||||
|             async with trio.open_nursery() as tn: | ||||
|                 tn.start_soon( | ||||
|                     ensure_sequence, | ||||
|                     stream, | ||||
|                     sequence.copy(), | ||||
|  | @ -253,7 +259,7 @@ def test_faster_task_to_recv_is_cancelled_by_slower( | |||
|                         continue | ||||
| 
 | ||||
|                 print('cancelling faster subtask') | ||||
|                 n.cancel_scope.cancel() | ||||
|                 tn.cancel_scope.cancel() | ||||
| 
 | ||||
|             try: | ||||
|                 value = await stream.receive() | ||||
|  | @ -371,13 +377,13 @@ def test_ensure_slow_consumers_lag_out( | |||
|                                     f'on {lags}:{value}') | ||||
|                                 return | ||||
| 
 | ||||
|             async with trio.open_nursery() as nursery: | ||||
|             async with trio.open_nursery() as tn: | ||||
| 
 | ||||
|                 for i in range(1, num_laggers): | ||||
| 
 | ||||
|                     task_name = f'sub_{i}' | ||||
|                     laggers[task_name] = 0 | ||||
|                     nursery.start_soon( | ||||
|                     tn.start_soon( | ||||
|                         partial( | ||||
|                             sub_and_print, | ||||
|                             delay=i*0.001, | ||||
|  | @ -497,6 +503,7 @@ def test_no_raise_on_lag(): | |||
|                 # internals when the no raise flag is set. | ||||
|                 loglevel='warning', | ||||
|             ), | ||||
|             collapse_eg(), | ||||
|             trio.open_nursery() as n, | ||||
|         ): | ||||
|             n.start_soon(slow) | ||||
|  |  | |||
|  | @ -64,7 +64,9 @@ def test_stashed_child_nursery(use_start_soon): | |||
|     async def main(): | ||||
| 
 | ||||
|         async with ( | ||||
|             trio.open_nursery() as pn, | ||||
|             trio.open_nursery( | ||||
|                 strict_exception_groups=False, | ||||
|             ) as pn, | ||||
|         ): | ||||
|             cn = await pn.start(mk_child_nursery) | ||||
|             assert cn | ||||
|  | @ -101,6 +103,7 @@ def test_stashed_child_nursery(use_start_soon): | |||
| def test_acm_embedded_nursery_propagates_enter_err( | ||||
|     canc_from_finally: bool, | ||||
|     unmask_from_canc: bool, | ||||
|     debug_mode: bool, | ||||
| ): | ||||
|     ''' | ||||
|     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() | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
|             async with ( | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ from ._state import ( | |||
|     current_actor as current_actor, | ||||
|     is_root_process as is_root_process, | ||||
|     current_ipc_ctx as current_ipc_ctx, | ||||
|     debug_mode as debug_mode | ||||
| ) | ||||
| from ._exceptions import ( | ||||
|     ContextCancelled as ContextCancelled, | ||||
|  | @ -66,3 +67,4 @@ from ._root import ( | |||
| from ._ipc import Channel as Channel | ||||
| from ._portal import Portal as Portal | ||||
| from ._runtime import Actor as Actor | ||||
| from . import hilevel as hilevel | ||||
|  |  | |||
|  | @ -19,10 +19,13 @@ Actor cluster helpers. | |||
| 
 | ||||
| ''' | ||||
| from __future__ import annotations | ||||
| 
 | ||||
| from contextlib import asynccontextmanager as acm | ||||
| from contextlib import ( | ||||
|     asynccontextmanager as acm, | ||||
| ) | ||||
| from multiprocessing import cpu_count | ||||
| from typing import AsyncGenerator, Optional | ||||
| from typing import ( | ||||
|     AsyncGenerator, | ||||
| ) | ||||
| 
 | ||||
| import trio | ||||
| import tractor | ||||
|  |  | |||
|  | @ -950,7 +950,7 @@ class Context: | |||
|             # f'Context.cancel() => {self.chan.uid}\n' | ||||
|             f'c)=> {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._nsf}() -> {codec}[dict]:\n\n' | ||||
|             # TODO: pull msg-type from spec re #320 | ||||
|  | @ -1003,7 +1003,8 @@ class Context: | |||
|                     ) | ||||
|                 else: | ||||
|                     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}' | ||||
|                     ) | ||||
| 
 | ||||
|  | @ -1560,12 +1561,12 @@ class Context: | |||
|                     strict_pld_parity=strict_pld_parity, | ||||
|                     hide_tb=hide_tb, | ||||
|                 ) | ||||
|             except BaseException as err: | ||||
|             except BaseException as _bexc: | ||||
|                 err = _bexc | ||||
|                 if not isinstance(err, MsgTypeError): | ||||
|                     __tracebackhide__: bool = False | ||||
| 
 | ||||
|                 raise | ||||
| 
 | ||||
|                 raise err | ||||
| 
 | ||||
|         # TODO: maybe a flag to by-pass encode op if already done | ||||
|         # here in caller? | ||||
|  | @ -1982,7 +1983,10 @@ async def open_context_from_portal( | |||
|     ctxc_from_callee: ContextCancelled|None = None | ||||
|     try: | ||||
|         async with ( | ||||
|             trio.open_nursery() as tn, | ||||
|             trio.open_nursery( | ||||
|                 strict_exception_groups=False, | ||||
|             ) as tn, | ||||
| 
 | ||||
|             msgops.maybe_limit_plds( | ||||
|                 ctx=ctx, | ||||
|                 spec=ctx_meta.get('pld_spec'), | ||||
|  |  | |||
|  | @ -238,7 +238,7 @@ def _trio_main( | |||
|             nest_from_op( | ||||
|                 input_op='>(',  # see syntax ideas above | ||||
|                 tree_str=actor_info, | ||||
|                 back_from_op=1, | ||||
|                 back_from_op=2,  # since "complete" | ||||
|             ) | ||||
|         ) | ||||
|     logmeth = log.info | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ from __future__ import annotations | |||
| import builtins | ||||
| import importlib | ||||
| from pprint import pformat | ||||
| from pdb import bdb | ||||
| import sys | ||||
| from types import ( | ||||
|     TracebackType, | ||||
|  | @ -181,6 +182,7 @@ def get_err_type(type_name: str) -> BaseException|None: | |||
|         builtins, | ||||
|         _this_mod, | ||||
|         trio, | ||||
|         bdb, | ||||
|     ]: | ||||
|         if type_ref := getattr( | ||||
|             ns, | ||||
|  |  | |||
|  | @ -255,8 +255,8 @@ class MsgpackTCPStream(MsgTransport): | |||
|                 raise TransportClosed( | ||||
|                     message=( | ||||
|                         f'IPC transport already closed by peer\n' | ||||
|                         f'x)> {type(trans_err)}\n' | ||||
|                         f' |_{self}\n' | ||||
|                         f'x]> {type(trans_err)}\n' | ||||
|                         f'  |_{self}\n' | ||||
|                     ), | ||||
|                     loglevel=loglevel, | ||||
|                 ) from trans_err | ||||
|  | @ -273,8 +273,8 @@ class MsgpackTCPStream(MsgTransport): | |||
|                 raise TransportClosed( | ||||
|                     message=( | ||||
|                         f'IPC transport already manually closed locally?\n' | ||||
|                         f'x)> {type(closure_err)} \n' | ||||
|                         f' |_{self}\n' | ||||
|                         f'x]> {type(closure_err)} \n' | ||||
|                         f'  |_{self}\n' | ||||
|                     ), | ||||
|                     loglevel='error', | ||||
|                     raise_on_report=( | ||||
|  | @ -289,8 +289,8 @@ class MsgpackTCPStream(MsgTransport): | |||
|                 raise TransportClosed( | ||||
|                     message=( | ||||
|                         f'IPC transport already gracefully closed\n' | ||||
|                         f')>\n' | ||||
|                         f'|_{self}\n' | ||||
|                         f']>\n' | ||||
|                         f' |_{self}\n' | ||||
|                     ), | ||||
|                     loglevel='transport', | ||||
|                     # cause=???  # handy or no? | ||||
|  |  | |||
|  | @ -533,6 +533,10 @@ async def open_portal( | |||
|     async with maybe_open_nursery( | ||||
|         tn, | ||||
|         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: | ||||
| 
 | ||||
|         if not channel.connected(): | ||||
|  |  | |||
|  | @ -111,8 +111,8 @@ async def open_root_actor( | |||
|     Runtime init entry point for ``tractor``. | ||||
| 
 | ||||
|     ''' | ||||
|     __tracebackhide__: bool = hide_tb | ||||
|     _debug.hide_runtime_frames() | ||||
|     __tracebackhide__: bool = hide_tb | ||||
| 
 | ||||
|     # 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 | ||||
|         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 | ||||
|             # and blocks here until any underlying actor(-process) | ||||
|  | @ -387,6 +390,12 @@ async def open_root_actor( | |||
|                 BaseExceptionGroup, | ||||
|             ) 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 | ||||
|                 # `._runtime.Actor._stream_handler()` where in the | ||||
|                 # non-root or root-that-opened-this-mahually case we | ||||
|  | @ -457,12 +466,19 @@ def run_daemon( | |||
| 
 | ||||
|     start_method: str | None = None, | ||||
|     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 | ||||
| 
 | ||||
| ) -> None: | ||||
|     ''' | ||||
|     Spawn daemon actor which will respond to RPC; the main task simply | ||||
|     starts the runtime and then sleeps forever. | ||||
|     Spawn a root (daemon) actor which will respond to RPC; the main | ||||
|     task simply starts the runtime and then blocks via embedded | ||||
|     `trio.sleep_forever()`. | ||||
| 
 | ||||
|     This is a very minimal convenience wrapper around starting | ||||
|     a "run-until-cancelled" root actor which can be started with a set | ||||
|  | @ -475,7 +491,6 @@ def run_daemon( | |||
|         importlib.import_module(path) | ||||
| 
 | ||||
|     async def _main(): | ||||
| 
 | ||||
|         async with open_root_actor( | ||||
|             registry_addrs=registry_addrs, | ||||
|             name=name, | ||||
|  |  | |||
|  | @ -620,7 +620,11 @@ async def _invoke( | |||
|             tn: trio.Nursery | ||||
|             rpc_ctx_cs: CancelScope | ||||
|             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( | ||||
|                     ctx=ctx, | ||||
|                     spec=ctx_meta.get('pld_spec'), | ||||
|  | @ -733,8 +737,8 @@ async def _invoke( | |||
|         # XXX: do we ever trigger this block any more? | ||||
|         except ( | ||||
|             BaseExceptionGroup, | ||||
|             trio.Cancelled, | ||||
|             BaseException, | ||||
|             trio.Cancelled, | ||||
| 
 | ||||
|         ) as scope_error: | ||||
|             if ( | ||||
|  | @ -847,8 +851,8 @@ async def try_ship_error_to_remote( | |||
|             log.critical( | ||||
|                 'IPC transport failure -> ' | ||||
|                 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! | ||||
|                 f'{msg}\n' | ||||
|             ) | ||||
|  |  | |||
|  | @ -1283,7 +1283,8 @@ class Actor: | |||
|         msg: str = ( | ||||
|             f'Actor-runtime cancel request from {requester_type}\n\n' | ||||
|             f'<=c) {requesting_uid}\n' | ||||
|             f' |_{self}\n' | ||||
|             f'  |_{self}\n' | ||||
|             f'\n' | ||||
|         ) | ||||
| 
 | ||||
|         # TODO: what happens here when we self-cancel tho? | ||||
|  | @ -1303,13 +1304,15 @@ class Actor: | |||
|                 lock_req_ctx.has_outcome | ||||
|             ): | ||||
|                 msg += ( | ||||
|                     '-> Cancelling active debugger request..\n' | ||||
|                     f'\n' | ||||
|                     f'-> Cancelling active debugger request..\n' | ||||
|                     f'|_{_debug.Lock.repr()}\n\n' | ||||
|                     f'|_{lock_req_ctx}\n\n' | ||||
|                 ) | ||||
|                 # lock_req_ctx._scope.cancel() | ||||
|                 # TODO: wrap this in a method-API.. | ||||
|                 debug_req.req_cs.cancel() | ||||
|                 # if lock_req_ctx: | ||||
| 
 | ||||
|             # self-cancel **all** ongoing RPC tasks | ||||
|             await self.cancel_rpc_tasks( | ||||
|  | @ -1718,11 +1721,15 @@ async def async_main( | |||
|         # parent is kept alive as a resilient service until | ||||
|         # cancellation steps have (mostly) occurred in | ||||
|         # 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 | ||||
|             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 | ||||
|                 # connections to us such that if the TCP server | ||||
|                 # is killed, connections can continue to process | ||||
|  |  | |||
|  | @ -327,9 +327,10 @@ async def soft_kill( | |||
|     uid: tuple[str, str] = portal.channel.uid | ||||
|     try: | ||||
|         log.cancel( | ||||
|             'Soft killing sub-actor via portal request\n' | ||||
|             f'c)> {portal.chan.uid}\n' | ||||
|             f' |_{proc}\n' | ||||
|             f'Soft killing sub-actor via portal request\n' | ||||
|             f'\n' | ||||
|             f'(c=> {portal.chan.uid}\n' | ||||
|             f'  |_{proc}\n' | ||||
|         ) | ||||
|         # wait on sub-proc to signal termination | ||||
|         await wait_func(proc) | ||||
|  |  | |||
|  | @ -108,6 +108,7 @@ def is_main_process() -> bool: | |||
|     return mp.current_process().name == 'MainProcess' | ||||
| 
 | ||||
| 
 | ||||
| # TODO, more verby name? | ||||
| def debug_mode() -> bool: | ||||
|     ''' | ||||
|     Bool determining if "debug mode" is on which enables | ||||
|  |  | |||
|  | @ -376,7 +376,7 @@ class MsgStream(trio.abc.Channel): | |||
|                 f'Stream self-closed by {self._ctx.side!r}-side before EoC\n' | ||||
|                 # } bc a stream is a "scope"/msging-phase inside an IPC | ||||
|                 f'x}}>\n' | ||||
|                 f'|_{self}\n' | ||||
|                 f'  |_{self}\n' | ||||
|             ) | ||||
|             log.cancel(message) | ||||
|             self._eoc = trio.EndOfChannel(message) | ||||
|  |  | |||
|  | @ -395,17 +395,23 @@ async def _open_and_supervise_one_cancels_all_nursery( | |||
|     # `ActorNursery.start_actor()`). | ||||
| 
 | ||||
|     # 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: | ||||
|             # This is the inner level "run in actor" nursery. It is | ||||
|             # 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 | ||||
|             # gracefully). Errors collected from these actors are | ||||
|             # immediately raised for handling by a supervisor strategy. | ||||
|             # As such if the strategy propagates any error(s) upwards | ||||
|             # 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( | ||||
|                     actor, | ||||
|  | @ -472,8 +478,8 @@ async def _open_and_supervise_one_cancels_all_nursery( | |||
|                             ContextCancelled, | ||||
|                         }: | ||||
|                             log.cancel( | ||||
|                                 'Actor-nursery caught remote cancellation\n\n' | ||||
| 
 | ||||
|                                 'Actor-nursery caught remote cancellation\n' | ||||
|                                 '\n' | ||||
|                                 f'{inner_err.tb_str}' | ||||
|                             ) | ||||
|                         else: | ||||
|  | @ -565,7 +571,9 @@ async def _open_and_supervise_one_cancels_all_nursery( | |||
| @acm | ||||
| # @api_frame | ||||
| async def open_nursery( | ||||
|     hide_tb: bool = True, | ||||
|     **kwargs, | ||||
|     # ^TODO, paramspec for `open_root_actor()` | ||||
| 
 | ||||
| ) -> typing.AsyncGenerator[ActorNursery, None]: | ||||
|     ''' | ||||
|  | @ -583,7 +591,7 @@ async def open_nursery( | |||
|     which cancellation scopes correspond to each spawned subactor set. | ||||
| 
 | ||||
|     ''' | ||||
|     __tracebackhide__: bool = True | ||||
|     __tracebackhide__: bool = hide_tb | ||||
|     implicit_runtime: bool = False | ||||
|     actor: Actor = current_actor(err_on_no_runtime=False) | ||||
|     an: ActorNursery|None = None | ||||
|  | @ -599,7 +607,10 @@ async def open_nursery( | |||
|             # mark us for teardown on exit | ||||
|             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() | ||||
| 
 | ||||
|                 try: | ||||
|  | @ -637,8 +648,10 @@ async def open_nursery( | |||
|         # show frame on any internal runtime-scope error | ||||
|         if ( | ||||
|             an | ||||
|             and not an.cancelled | ||||
|             and an._scope_error | ||||
|             and | ||||
|             not an.cancelled | ||||
|             and | ||||
|             an._scope_error | ||||
|         ): | ||||
|             __tracebackhide__: bool = False | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,7 +19,10 @@ Various helpers/utils for auditing your `tractor` app and/or the | |||
| core runtime. | ||||
| 
 | ||||
| ''' | ||||
| from contextlib import asynccontextmanager as acm | ||||
| from contextlib import ( | ||||
|     asynccontextmanager as acm, | ||||
| ) | ||||
| import os | ||||
| import pathlib | ||||
| 
 | ||||
| import tractor | ||||
|  | @ -59,7 +62,12 @@ def mk_cmd( | |||
|     exs_subpath: str = 'debugging', | ||||
| ) -> 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 = ( | ||||
|  | @ -67,10 +75,15 @@ def mk_cmd( | |||
|         / exs_subpath | ||||
|         / f'{ex_name}.py' | ||||
|     ) | ||||
|     return ' '.join([ | ||||
|     py_cmd: str = ' '.join([ | ||||
|         'python', | ||||
|         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 | ||||
|  |  | |||
|  | @ -317,8 +317,6 @@ class Lock: | |||
|         we_released: bool = False | ||||
|         ctx_in_debug: Context|None = cls.ctx_in_debug | ||||
|         repl_task: Task|Thread|None = DebugStatus.repl_task | ||||
|         message: str = '' | ||||
| 
 | ||||
|         try: | ||||
|             if not DebugStatus.is_main_trio_thread(): | ||||
|                 thread: threading.Thread = threading.current_thread() | ||||
|  | @ -333,6 +331,10 @@ class Lock: | |||
|                 return False | ||||
| 
 | ||||
|             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 | ||||
|             # the lock is marked as such. | ||||
|  | @ -347,11 +349,6 @@ class Lock: | |||
|             else: | ||||
|                 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 | ||||
|             owner: Task = lock.statistics().owner | ||||
|             if ( | ||||
|  | @ -366,23 +363,21 @@ class Lock: | |||
|                 # correct task, greenback-spawned-task and/or thread | ||||
|                 # being set to the `.repl_task` such that the above | ||||
|                 # condition matches and we actually release the lock. | ||||
|                 # | ||||
|                 # This is particular of note from `.pause_from_sync()`! | ||||
| 
 | ||||
|             ): | ||||
|                 cls._debug_lock.release() | ||||
|                 we_released: bool = True | ||||
|                 if repl_task: | ||||
|                     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' | ||||
|                     ) | ||||
|                 else: | ||||
|                     message: str = ( | ||||
|                         'TTY lock released by us on behalf of remote peer?\n' | ||||
|                         f'|_ctx_in_debug: {ctx_in_debug}\n\n' | ||||
|                         'TTY released by us on behalf of remote peer?\n' | ||||
|                         f'{ctx_in_debug}\n' | ||||
|                     ) | ||||
|                     # mk_pdb().set_trace() | ||||
|                 # elif owner: | ||||
| 
 | ||||
|         except RuntimeError as rte: | ||||
|             log.exception( | ||||
|  | @ -400,7 +395,8 @@ class Lock: | |||
|             req_handler_finished: trio.Event|None = Lock.req_handler_finished | ||||
|             if ( | ||||
|                 not lock_stats.owner | ||||
|                 and req_handler_finished is None | ||||
|                 and | ||||
|                 req_handler_finished is None | ||||
|             ): | ||||
|                 message += ( | ||||
|                     '-> No new task holds the TTY lock!\n\n' | ||||
|  | @ -418,8 +414,8 @@ class Lock: | |||
|                     repl_task | ||||
|                 ) | ||||
|                 message += ( | ||||
|                     f'A non-caller task still owns this lock on behalf of ' | ||||
|                     f'`{behalf_of_task}`\n' | ||||
|                     f'A non-caller task still owns this lock on behalf of\n' | ||||
|                     f'{behalf_of_task}\n' | ||||
|                     f'lock owner task: {lock_stats.owner}\n' | ||||
|                 ) | ||||
| 
 | ||||
|  | @ -447,8 +443,6 @@ class Lock: | |||
| 
 | ||||
|             if message: | ||||
|                 log.devx(message) | ||||
|             else: | ||||
|                 import pdbp; pdbp.set_trace() | ||||
| 
 | ||||
|         return we_released | ||||
| 
 | ||||
|  | @ -668,10 +662,11 @@ async def lock_stdio_for_peer( | |||
|         fail_reason: str = ( | ||||
|             f'on behalf of peer\n\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' | ||||
|             f'x)=> {ctx}\n' | ||||
|             f'x)=>\n' | ||||
|             f'   {ctx}' | ||||
|         ) | ||||
|         if isinstance(req_err, trio.Cancelled): | ||||
|             fail_reason = ( | ||||
|  | @ -1179,7 +1174,7 @@ async def request_root_stdio_lock( | |||
|     log.devx( | ||||
|         '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`? | ||||
|     # -[ ] simply add the wrapping needed for the debugger specifics? | ||||
|     #   - 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 | ||||
|     DebugStatus.req_finished = trio.Event() | ||||
|     DebugStatus.req_task = current_task() | ||||
|     req_err: BaseException|None = None | ||||
|     try: | ||||
|         from tractor._discovery import get_root | ||||
|         # 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 | ||||
|             req_ctx: Context|None = None | ||||
|             ctx_eg: BaseExceptionGroup|None = None | ||||
|             try: | ||||
|                 # TODO: merge into single async with ? | ||||
|                 async with get_root() as portal: | ||||
|  | @ -1242,7 +1239,12 @@ async def request_root_stdio_lock( | |||
|                         ) | ||||
| 
 | ||||
|                         # 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 | ||||
|                         # except AttributeError: | ||||
|                         #     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' | ||||
|                     ) | ||||
| 
 | ||||
|             except ( | ||||
|             except* ( | ||||
|                 tractor.ContextCancelled, | ||||
|                 trio.Cancelled, | ||||
|             ): | ||||
|             ) as _taskc_eg: | ||||
|                 ctx_eg = _taskc_eg | ||||
|                 log.cancel( | ||||
|                     'Debug lock request was CANCELLED?\n\n' | ||||
|                     f'<=c) {req_ctx}\n' | ||||
|  | @ -1291,21 +1294,23 @@ async def request_root_stdio_lock( | |||
|                 ) | ||||
|                 raise | ||||
| 
 | ||||
|             except ( | ||||
|             except* ( | ||||
|                 BaseException, | ||||
|             ) as ctx_err: | ||||
|             ) as _ctx_eg: | ||||
|                 ctx_eg = _ctx_eg | ||||
|                 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): | ||||
|                     message += ( | ||||
|                         f'<=x) {req_ctx}\n\n' | ||||
|                         f'<=x)\n' | ||||
|                         f' |_{req_ctx}\n' | ||||
|                         f'Cancelling IPC ctx!\n' | ||||
|                     ) | ||||
|                     try: | ||||
|                         await req_ctx.cancel() | ||||
|                     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 `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' | ||||
| 
 | ||||
|                 log.exception(message) | ||||
|                 ctx_err.add_note(message) | ||||
|                 raise ctx_err | ||||
|                 ctx_eg.add_note(message) | ||||
|                 raise ctx_eg | ||||
| 
 | ||||
|     except ( | ||||
|         tractor.ContextCancelled, | ||||
|         trio.Cancelled, | ||||
|     ): | ||||
|         log.cancel( | ||||
|             'Debug lock request CANCELLED?\n' | ||||
|             f'{req_ctx}\n' | ||||
|         ) | ||||
|         raise | ||||
|     except BaseException as _req_err: | ||||
|         req_err = _req_err | ||||
| 
 | ||||
|         # XXX NOTE, since new `trio` enforces strict egs by default | ||||
|         # we have to always handle the eg explicitly given the | ||||
|         # `Portal.open_context()` call above (which implicitly opens | ||||
|         # a nursery). | ||||
|         match req_err: | ||||
|             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.release() | ||||
| 
 | ||||
|  | @ -1343,7 +1372,7 @@ async def request_root_stdio_lock( | |||
|             'Failed during stdio-locking dialog from root actor\n\n' | ||||
| 
 | ||||
|             f'<=x)\n' | ||||
|             f'|_{DebugStatus.req_ctx}\n' | ||||
|             f'  |_{DebugStatus.req_ctx}\n' | ||||
|         ) from req_err | ||||
| 
 | ||||
|     finally: | ||||
|  | @ -1406,7 +1435,7 @@ def any_connected_locker_child() -> bool: | |||
|     actor: Actor = current_actor() | ||||
| 
 | ||||
|     if not is_root_process(): | ||||
|         raise RuntimeError('This is a root-actor only API!') | ||||
|         raise InternalError('This is a root-actor only API!') | ||||
| 
 | ||||
|     if ( | ||||
|         (ctx := Lock.ctx_in_debug) | ||||
|  | @ -2143,11 +2172,12 @@ async def _pause( | |||
|     # `_enter_repl_sync()` into a common @cm? | ||||
|     except BaseException as _pause_err: | ||||
|         pause_err: BaseException = _pause_err | ||||
|         _repl_fail_report: str|None = _repl_fail_msg | ||||
|         if isinstance(pause_err, bdb.BdbQuit): | ||||
|             log.devx( | ||||
|                 'REPL for pdb was explicitly quit!\n' | ||||
|             ) | ||||
|             _repl_fail_msg = None | ||||
|             _repl_fail_report = None | ||||
| 
 | ||||
|         # when the actor is mid-runtime cancellation the | ||||
|         # `Actor._service_n` might get closed before we can spawn | ||||
|  | @ -2167,16 +2197,16 @@ async def _pause( | |||
|             return | ||||
| 
 | ||||
|         elif isinstance(pause_err, trio.Cancelled): | ||||
|             _repl_fail_msg = ( | ||||
|             _repl_fail_report += ( | ||||
|                 'You called `tractor.pause()` from an already cancelled scope!\n\n' | ||||
|                 'Consider `await tractor.pause(shield=True)` to make it work B)\n' | ||||
|             ) | ||||
| 
 | ||||
|         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: | ||||
|             log.exception(_repl_fail_msg) | ||||
|         if _repl_fail_report: | ||||
|             log.exception(_repl_fail_report) | ||||
| 
 | ||||
|         if not actor.is_infected_aio(): | ||||
|             DebugStatus.release(cancel_req_task=True) | ||||
|  | @ -2257,6 +2287,13 @@ def _set_trace( | |||
|     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( | ||||
|     *, | ||||
|     hide_tb: bool = True, | ||||
|  | @ -3051,7 +3088,8 @@ async def maybe_wait_for_debugger( | |||
| 
 | ||||
|     if ( | ||||
|         not debug_mode() | ||||
|         and not child_in_debug | ||||
|         and | ||||
|         not child_in_debug | ||||
|     ): | ||||
|         return False | ||||
| 
 | ||||
|  | @ -3109,7 +3147,7 @@ async def maybe_wait_for_debugger( | |||
|                 logmeth( | ||||
|                     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' | ||||
|                 ) | ||||
| 
 | ||||
|  | @ -3163,6 +3201,15 @@ async def maybe_wait_for_debugger( | |||
|     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? | ||||
| # - [ ] optional runtime plugging? | ||||
| # - [ ] detection for sync vs. async code? | ||||
|  | @ -3172,11 +3219,11 @@ async def maybe_wait_for_debugger( | |||
| @cm | ||||
| def open_crash_handler( | ||||
|     catch: set[BaseException] = { | ||||
|         # Exception, | ||||
|         BaseException, | ||||
|     }, | ||||
|     ignore: set[BaseException] = { | ||||
|         KeyboardInterrupt, | ||||
|         trio.Cancelled, | ||||
|     }, | ||||
|     tb_hide: bool = True, | ||||
| ): | ||||
|  | @ -3193,9 +3240,6 @@ def open_crash_handler( | |||
|     ''' | ||||
|     __tracebackhide__: bool = tb_hide | ||||
| 
 | ||||
|     class BoxedMaybeException(Struct): | ||||
|         value: BaseException|None = None | ||||
| 
 | ||||
|     # TODO, yield a `outcome.Error`-like boxed type? | ||||
|     # -[~] use `outcome.Value/Error` X-> frozen! | ||||
|     # -[x] write our own..? | ||||
|  | @ -3237,6 +3281,8 @@ def open_crash_handler( | |||
| def maybe_open_crash_handler( | ||||
|     pdb: bool = False, | ||||
|     tb_hide: bool = True, | ||||
| 
 | ||||
|     **kwargs, | ||||
| ): | ||||
|     ''' | ||||
|     Same as `open_crash_handler()` but with bool input flag | ||||
|  | @ -3247,9 +3293,11 @@ def maybe_open_crash_handler( | |||
|     ''' | ||||
|     __tracebackhide__: bool = tb_hide | ||||
| 
 | ||||
|     rtctx = nullcontext | ||||
|     rtctx = nullcontext( | ||||
|         enter_result=BoxedMaybeException() | ||||
|     ) | ||||
|     if pdb: | ||||
|         rtctx = open_crash_handler | ||||
|         rtctx = open_crash_handler(**kwargs) | ||||
| 
 | ||||
|     with rtctx(): | ||||
|         yield | ||||
|     with rtctx as boxed_maybe_exc: | ||||
|         yield boxed_maybe_exc | ||||
|  |  | |||
|  | @ -258,6 +258,9 @@ class PldRx(Struct): | |||
|                         f'|_pld={pld!r}\n' | ||||
|                     ) | ||||
|                     return pld | ||||
|                 except TypeError as typerr: | ||||
|                     __tracebackhide__: bool = False | ||||
|                     raise typerr | ||||
| 
 | ||||
|                 # XXX pld-value type failure | ||||
|                 except ValidationError as valerr: | ||||
|  | @ -796,8 +799,14 @@ def validate_payload_msg( | |||
|     __tracebackhide__: bool = hide_tb | ||||
|     codec: MsgCodec = current_codec() | ||||
|     msg_bytes: bytes = codec.encode(pld_msg) | ||||
|     roundtripped: Started|None = None | ||||
|     try: | ||||
|         roundtripped: Started = codec.decode(msg_bytes) | ||||
|     except TypeError as typerr: | ||||
|         __tracebackhide__: bool = False | ||||
|         raise typerr | ||||
| 
 | ||||
|     try: | ||||
|         ctx: Context = getattr(ipc, 'ctx', ipc) | ||||
|         pld: PayloadT = ctx.pld_rx.decode_pld( | ||||
|             msg=roundtripped, | ||||
|  | @ -822,6 +831,11 @@ def validate_payload_msg( | |||
|             ) | ||||
|             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! | ||||
|     except ValidationError as verr: | ||||
|         try: | ||||
|  | @ -832,9 +846,13 @@ def validate_payload_msg( | |||
|                 verb_header='Trying to send ', | ||||
|                 is_invalid_payload=True, | ||||
|             ) | ||||
|         except BaseException: | ||||
|         except BaseException as _be: | ||||
|             if not roundtripped: | ||||
|                 raise verr | ||||
| 
 | ||||
|             be = _be | ||||
|             __tracebackhide__: bool = False | ||||
|             raise | ||||
|             raise be | ||||
| 
 | ||||
|         if not raise_mte: | ||||
|             return mte | ||||
|  |  | |||
|  | @ -29,3 +29,6 @@ from ._broadcast import ( | |||
|     BroadcastReceiver as BroadcastReceiver, | ||||
|     Lagged as Lagged, | ||||
| ) | ||||
| from ._beg import ( | ||||
|     collapse_eg as collapse_eg, | ||||
| ) | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -15,7 +15,7 @@ | |||
| # 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 | ||||
| 
 | ||||
| ''' | ||||
|  |  | |||
|  | @ -57,6 +57,8 @@ async def maybe_open_nursery( | |||
|     shield: bool = False, | ||||
|     lib: ModuleType = trio, | ||||
| 
 | ||||
|     **kwargs,  # proxy thru | ||||
| 
 | ||||
| ) -> AsyncGenerator[trio.Nursery, Any]: | ||||
|     ''' | ||||
|     Create a new nursery if None provided. | ||||
|  | @ -67,7 +69,7 @@ async def maybe_open_nursery( | |||
|     if nursery is not None: | ||||
|         yield nursery | ||||
|     else: | ||||
|         async with lib.open_nursery() as nursery: | ||||
|         async with lib.open_nursery(**kwargs) as nursery: | ||||
|             nursery.cancel_scope.shield = shield | ||||
|             yield nursery | ||||
| 
 | ||||
|  | @ -143,9 +145,14 @@ async def gather_contexts( | |||
|             '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: | ||||
|             n.start_soon( | ||||
|             tn.start_soon( | ||||
|                 _enter_and_wait, | ||||
|                 mngr, | ||||
|                 unwrapped, | ||||
|  |  | |||
							
								
								
									
										88
									
								
								uv.lock
								
								
								
								
							
							
						
						
									
										88
									
								
								uv.lock
								
								
								
								
							|  | @ -126,7 +126,31 @@ wheels = [ | |||
| [[package]] | ||||
| name = "msgspec" | ||||
| 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]] | ||||
| name = "outcome" | ||||
|  | @ -240,7 +264,7 @@ wheels = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "pytest" | ||||
| version = "8.3.4" | ||||
| version = "8.3.5" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "colorama", marker = "sys_platform == 'win32'" }, | ||||
|  | @ -248,9 +272,9 @@ dependencies = [ | |||
|     { name = "packaging" }, | ||||
|     { 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 = [ | ||||
|     { 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]] | ||||
|  | @ -314,17 +338,15 @@ dev = [ | |||
|     { name = "pytest" }, | ||||
|     { name = "stackscope" }, | ||||
|     { name = "xonsh" }, | ||||
|     { name = "xonsh-vox-tabcomplete" }, | ||||
|     { name = "xontrib-vox" }, | ||||
| ] | ||||
| 
 | ||||
| [package.metadata] | ||||
| requires-dist = [ | ||||
|     { name = "colorlog", specifier = ">=6.8.2,<7" }, | ||||
|     { name = "msgspec", git = "https://github.com/jcrist/msgspec.git" }, | ||||
|     { name = "pdbp", specifier = ">=1.5.0,<2" }, | ||||
|     { name = "msgspec", specifier = ">=0.19.0" }, | ||||
|     { name = "pdbp", specifier = ">=1.6,<2" }, | ||||
|     { 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" }, | ||||
| ] | ||||
| 
 | ||||
|  | @ -332,13 +354,11 @@ requires-dist = [ | |||
| dev = [ | ||||
|     { name = "greenback", specifier = ">=1.2.1,<2" }, | ||||
|     { 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 = "pytest", specifier = ">=8.2.0,<9" }, | ||||
|     { name = "pytest", specifier = ">=8.3.5" }, | ||||
|     { name = "stackscope", specifier = ">=0.2.2,<0.3" }, | ||||
|     { name = "xonsh", specifier = ">=0.19.1" }, | ||||
|     { name = "xonsh-vox-tabcomplete", specifier = ">=0.5,<0.6" }, | ||||
|     { name = "xontrib-vox", specifier = ">=0.0.1,<0.0.2" }, | ||||
|     { name = "xonsh", specifier = ">=0.19.2" }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
|  | @ -355,7 +375,7 @@ wheels = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "trio" | ||||
| version = "0.24.0" | ||||
| version = "0.29.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "attrs" }, | ||||
|  | @ -365,9 +385,9 @@ dependencies = [ | |||
|     { name = "sniffio" }, | ||||
|     { 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 = [ | ||||
|     { 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]] | ||||
|  | @ -434,33 +454,13 @@ wheels = [ | |||
| 
 | ||||
| [[package]] | ||||
| name = "xonsh" | ||||
| version = "0.19.1" | ||||
| version = "0.19.2" | ||||
| 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 = [ | ||||
|     { 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/77/4e/e487e82349866b245c559433c9ba626026a2e66bd17d7f9ac1045082f146/xonsh-0.19.1-py311-none-any.whl", hash = "sha256:c176e515b0260ab803963d1f0924f1e32f1064aa6fd5d791aa0cf6cda3a924ae", size = 640680 }, | ||||
|     { 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/83/ff/7873cb8184cffeafddbf861712831c2baa2e9dbecdbfd33b1228f0db0019/xonsh-0.19.1-py313-none-any.whl", hash = "sha256:3f158b6fc0bba954e0b989004d4261bafc4bd94c68c2abd75b825da23e5a869c", size = 641166 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/cc/03/b9f8dd338df0a330011d104e63d4d0acd8bbbc1e990ff049487b6bdf585d/xonsh-0.19.1-py39-none-any.whl", hash = "sha256:a900a6eb87d881a7ef90b1ac8522ba3699582f0bcb1e9abd863d32f6d63faf04", size = 632912 }, | ||||
| ] | ||||
| 
 | ||||
| [[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 }, | ||||
|     { 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/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286 }, | ||||
|     { 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/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602 }, | ||||
| ] | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue