Compare commits
	
		
			23 Commits 
		
	
	
		
			63d5e65100
			...
			ce537bc010
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | ce537bc010 | |
|  | c103a5c136 | |
|  | 3febc61e62 | |
|  | f2758a62d9 | |
|  | a163769ea6 | |
|  | bd3668e2bf | |
|  | d74dbab1be | |
|  | 9be457fcf3 | |
|  | e6f3f187b6 | |
|  | 924eff2985 | |
|  | 89fc072ca0 | |
|  | 7b8a8dcc7c | |
|  | c63b94f61f | |
|  | 0e39b3902f | |
|  | bf9689e10a | |
|  | 350a94f39e | |
|  | 0945631629 | |
|  | 0a0d30d108 | |
|  | dcb6706489 | |
|  | 170e198683 | |
|  | 840c328f19 | |
|  | 46dbe6d2fc | |
|  | f08e888138 | 
							
								
								
									
										159
									
								
								pyproject.toml
								
								
								
								
							
							
						
						
									
										159
									
								
								pyproject.toml
								
								
								
								
							|  | @ -1,71 +1,90 @@ | |||
| [build-system] | ||||
| requires = ["poetry-core"] | ||||
| build-backend = "poetry.core.masonry.api" | ||||
| requires = ["hatchling"] | ||||
| build-backend = "hatchling.build" | ||||
| 
 | ||||
| # ------ - ------ | ||||
| # ------ build-system ------ | ||||
| 
 | ||||
| [tool.poetry] | ||||
| [project] | ||||
| name = "tractor" | ||||
| version = "0.1.0a6dev0" | ||||
| description='structured concurrent `trio`-"actors"' | ||||
| authors = ["Tyler Goodlet <goodboy_foss@protonmail.com>"] | ||||
| license = "AGPlv3" | ||||
| description = 'structured concurrent `trio`-"actors"' | ||||
| authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }] | ||||
| requires-python = ">= 3.11" | ||||
| readme = "docs/README.rst" | ||||
| 
 | ||||
| # TODO: do we need this xontrib loader at all given pep420 | ||||
| # and xonsh's xontrib global-autoload-via-setuptools? | ||||
| # https://xon.sh/tutorial_xontrib.html#authoring-xontribs | ||||
| packages = [ | ||||
|   {include = 'tractor' }, | ||||
|   # {include = 'tractor.experimental' }, | ||||
|   # {include = 'tractor.trionics' }, | ||||
|   # {include = 'tractor.msg' }, | ||||
|   # {include = 'tractor.devx' }, | ||||
| license = "AGPL-3.0-or-later" | ||||
| keywords = [ | ||||
|   "trio", | ||||
|   "async", | ||||
|   "concurrency", | ||||
|   "structured concurrency", | ||||
|   "actor model", | ||||
|   "distributed", | ||||
|   "multiprocessing", | ||||
| ] | ||||
| classifiers = [ | ||||
|   "Development Status :: 3 - Alpha", | ||||
|   "Operating System :: POSIX :: Linux", | ||||
|   "Framework :: Trio", | ||||
|   "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", | ||||
|   "Programming Language :: Python :: Implementation :: CPython", | ||||
|   "Programming Language :: Python :: 3 :: Only", | ||||
|   "Programming Language :: Python :: 3.11", | ||||
|   "Topic :: System :: Distributed Computing", | ||||
| ] | ||||
| dependencies = [ | ||||
| # trio runtime and friends | ||||
|   # (poetry) proper range specs, | ||||
|   # 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", | ||||
|   "tricycle>=0.4.1,<0.5", | ||||
|   "trio-typing>=0.10.0,<0.11", | ||||
| 
 | ||||
| # ------ - ------ | ||||
| 
 | ||||
| [tool.poetry.dependencies] | ||||
| python = "^3.11" | ||||
| 
 | ||||
| # trio runtime related | ||||
| # proper range spec: | ||||
| # https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/#id5 | ||||
| trio='^0.24' | ||||
| tricycle = "^0.4.1" | ||||
| trio-typing = "^0.10.0" | ||||
| 
 | ||||
| msgspec='^0.18.5'  # interchange | ||||
| wrapt = "^1.16.0"  # decorators | ||||
| colorlog = "^6.8.2"  # logging | ||||
|   "wrapt>=1.16.0,<2", | ||||
|   "colorlog>=6.8.2,<7", | ||||
| 
 | ||||
| # built-in multi-actor `pdb` REPL | ||||
| pdbp = "^1.5.0" | ||||
|   "pdbp>=1.5.0,<2", | ||||
| 
 | ||||
| # typed IPC msging | ||||
| # TODO, get back on release once 3.13 support is out! | ||||
|   "msgspec", | ||||
| ] | ||||
| 
 | ||||
| # TODO: distributed transport using | ||||
| # ------ project ------ | ||||
| 
 | ||||
| [dependency-groups] | ||||
| 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", | ||||
|   "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", | ||||
| ] | ||||
| 
 | ||||
| # TODO, distributed (multi-host) extensions | ||||
| # linux kernel networking | ||||
| # 'pyroute2 | ||||
| 
 | ||||
| # ------ - ------ | ||||
| [tool.hatch.build.targets.sdist] | ||||
| include = ["tractor"] | ||||
| 
 | ||||
| [tool.poetry.group.dev] | ||||
| optional = false | ||||
| [tool.poetry.group.dev.dependencies] | ||||
| # testing | ||||
| pytest = "^8.2.0" | ||||
| pexpect = "^4.9.0" | ||||
| [tool.hatch.build.targets.wheel] | ||||
| include = ["tractor"] | ||||
| 
 | ||||
| # .devx tooling | ||||
| greenback = "^1.2.1" | ||||
| stackscope = "^0.2.2" | ||||
| 
 | ||||
| # (light) xonsh usage/integration | ||||
| xontrib-vox = "^0.0.1" | ||||
| prompt-toolkit = "^3.0.43" | ||||
| xonsh-vox-tabcomplete = "^0.5" | ||||
| 
 | ||||
| # ------ - ------ | ||||
| # ------ dependency-groups ------ | ||||
| 
 | ||||
| [tool.towncrier] | ||||
| package = "tractor" | ||||
|  | @ -76,28 +95,26 @@ title_format = "tractor {version} ({project_date})" | |||
| template = "nooz/_template.rst" | ||||
| all_bullets = true | ||||
| 
 | ||||
|   [[tool.towncrier.type]] | ||||
| [[tool.towncrier.type]] | ||||
|   directory = "feature" | ||||
|   name = "Features" | ||||
|   showcontent = true | ||||
| 
 | ||||
|   [[tool.towncrier.type]] | ||||
| [[tool.towncrier.type]] | ||||
|   directory = "bugfix" | ||||
|   name = "Bug Fixes" | ||||
|   showcontent = true | ||||
| 
 | ||||
|   [[tool.towncrier.type]] | ||||
| [[tool.towncrier.type]] | ||||
|   directory = "doc" | ||||
|   name = "Improved Documentation" | ||||
|   showcontent = true | ||||
| 
 | ||||
|   [[tool.towncrier.type]] | ||||
| [[tool.towncrier.type]] | ||||
|   directory = "trivial" | ||||
|   name = "Trivial/Internal Changes" | ||||
|   showcontent = true | ||||
| 
 | ||||
| # ------ - ------ | ||||
| 
 | ||||
| [tool.pytest.ini_options] | ||||
| minversion = '6.0' | ||||
| testpaths = [ | ||||
|  | @ -113,29 +130,9 @@ addopts = [ | |||
| ] | ||||
| 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.towncrier ------ | ||||
| 
 | ||||
| # ------ - ------ | ||||
| [tool.uv.sources] | ||||
| msgspec = { git = "https://github.com/jcrist/msgspec.git" } | ||||
| 
 | ||||
| [project] | ||||
| keywords = [ | ||||
|   'trio', | ||||
|   'async', | ||||
|   'concurrency', | ||||
|   'structured concurrency', | ||||
|   'actor model', | ||||
|   'distributed', | ||||
|   'multiprocessing' | ||||
| ] | ||||
| classifiers = [ | ||||
|   "Development Status :: 3 - Alpha", | ||||
|   "Operating System :: POSIX :: Linux", | ||||
|   "Framework :: Trio", | ||||
|   "License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)", | ||||
|   "Programming Language :: Python :: Implementation :: CPython", | ||||
|   "Programming Language :: Python :: 3 :: Only", | ||||
|   "Programming Language :: Python :: 3.11", | ||||
|   "Topic :: System :: Distributed Computing", | ||||
| ] | ||||
| # ------ tool.uv.sources ------ | ||||
|  |  | |||
|  | @ -0,0 +1,82 @@ | |||
| # from default `ruff.toml` @ | ||||
| # https://docs.astral.sh/ruff/configuration/ | ||||
| 
 | ||||
| # Exclude a variety of commonly ignored directories. | ||||
| exclude = [ | ||||
|     ".bzr", | ||||
|     ".direnv", | ||||
|     ".eggs", | ||||
|     ".git", | ||||
|     ".git-rewrite", | ||||
|     ".hg", | ||||
|     ".ipynb_checkpoints", | ||||
|     ".mypy_cache", | ||||
|     ".nox", | ||||
|     ".pants.d", | ||||
|     ".pyenv", | ||||
|     ".pytest_cache", | ||||
|     ".pytype", | ||||
|     ".ruff_cache", | ||||
|     ".svn", | ||||
|     ".tox", | ||||
|     ".venv", | ||||
|     ".vscode", | ||||
|     "__pypackages__", | ||||
|     "_build", | ||||
|     "buck-out", | ||||
|     "build", | ||||
|     "dist", | ||||
|     "node_modules", | ||||
|     "site-packages", | ||||
|     "venv", | ||||
| ] | ||||
| 
 | ||||
| # Same as Black. | ||||
| line-length = 88 | ||||
| indent-width = 4 | ||||
| 
 | ||||
| # Assume Python 3.9 | ||||
| target-version = "py311" | ||||
| 
 | ||||
| [lint] | ||||
| # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`)  codes by default. | ||||
| # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or | ||||
| # McCabe complexity (`C901`) by default. | ||||
| select = ["E4", "E7", "E9", "F"] | ||||
| ignore = [ | ||||
|   'E402',  # https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/ | ||||
| ] | ||||
| 
 | ||||
| # Allow fix for all enabled rules (when `--fix`) is provided. | ||||
| fixable = ["ALL"] | ||||
| unfixable = [] | ||||
| 
 | ||||
| # Allow unused variables when underscore-prefixed. | ||||
| # dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" | ||||
| 
 | ||||
| [format] | ||||
| # Use single quotes in `ruff format`. | ||||
| quote-style = "single" | ||||
| 
 | ||||
| # Like Black, indent with spaces, rather than tabs. | ||||
| indent-style = "space" | ||||
| 
 | ||||
| # Like Black, respect magic trailing commas. | ||||
| skip-magic-trailing-comma = false | ||||
| 
 | ||||
| # Like Black, automatically detect the appropriate line ending. | ||||
| line-ending = "auto" | ||||
| 
 | ||||
| # Enable auto-formatting of code examples in docstrings. Markdown, | ||||
| # reStructuredText code/literal blocks and doctests are all supported. | ||||
| # | ||||
| # This is currently disabled by default, but it is planned for this | ||||
| # to be opt-out in the future. | ||||
| docstring-code-format = false | ||||
| 
 | ||||
| # Set the line length limit used when formatting code snippets in | ||||
| # docstrings. | ||||
| # | ||||
| # This only has an effect when the `docstring-code-format` setting is | ||||
| # enabled. | ||||
| docstring-code-line-length = "dynamic" | ||||
|  | @ -5,6 +5,7 @@ The hipster way to force SC onto the stdlib's "async": 'infection mode'. | |||
| import asyncio | ||||
| import builtins | ||||
| from contextlib import ExitStack | ||||
| # from functools import partial | ||||
| import itertools | ||||
| import importlib | ||||
| import os | ||||
|  | @ -108,7 +109,9 @@ async def asyncio_actor( | |||
| 
 | ||||
|     except BaseException as err: | ||||
|         if expect_err: | ||||
|             assert isinstance(err, error_type) | ||||
|             assert isinstance(err, error_type), ( | ||||
|                 f'{type(err)} is not {error_type}?' | ||||
|             ) | ||||
| 
 | ||||
|         raise | ||||
| 
 | ||||
|  | @ -180,8 +183,8 @@ def test_trio_cancels_aio(reg_addr): | |||
|         with trio.move_on_after(1): | ||||
|             # cancel the nursery shortly after boot | ||||
| 
 | ||||
|             async with tractor.open_nursery() as n: | ||||
|                 await n.run_in_actor( | ||||
|             async with tractor.open_nursery() as tn: | ||||
|                 await tn.run_in_actor( | ||||
|                     asyncio_actor, | ||||
|                     target='aio_sleep_forever', | ||||
|                     expect_err='trio.Cancelled', | ||||
|  | @ -201,22 +204,33 @@ async def trio_ctx( | |||
|     # this will block until the ``asyncio`` task sends a "first" | ||||
|     # message. | ||||
|     with trio.fail_after(2): | ||||
|         async with ( | ||||
|             trio.open_nursery() as n, | ||||
|         try: | ||||
|             async with ( | ||||
|                 trio.open_nursery( | ||||
|                     # TODO, for new `trio` / py3.13 | ||||
|                     # strict_exception_groups=False, | ||||
|                 ) as tn, | ||||
|                 tractor.to_asyncio.open_channel_from( | ||||
|                     sleep_and_err, | ||||
|                 ) as (first, chan), | ||||
|             ): | ||||
| 
 | ||||
|             tractor.to_asyncio.open_channel_from( | ||||
|                 sleep_and_err, | ||||
|             ) as (first, chan), | ||||
|         ): | ||||
|                 assert first == 'start' | ||||
| 
 | ||||
|             assert first == 'start' | ||||
|                 # spawn another asyncio task for the cuck of it. | ||||
|                 tn.start_soon( | ||||
|                     tractor.to_asyncio.run_task, | ||||
|                     aio_sleep_forever, | ||||
|                 ) | ||||
|                 await trio.sleep_forever() | ||||
| 
 | ||||
|             # spawn another asyncio task for the cuck of it. | ||||
|             n.start_soon( | ||||
|                 tractor.to_asyncio.run_task, | ||||
|                 aio_sleep_forever, | ||||
|             ) | ||||
|             await trio.sleep_forever() | ||||
|         # TODO, factor this into a `trionics.collapse()`? | ||||
|         except* BaseException as beg: | ||||
|             # await tractor.pause(shield=True) | ||||
|             if len(excs := beg.exceptions) == 1: | ||||
|                 raise excs[0] | ||||
|             else: | ||||
|                 raise | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|  | @ -235,7 +249,6 @@ def test_context_spawns_aio_task_that_errors( | |||
| 
 | ||||
|     ''' | ||||
|     async def main(): | ||||
| 
 | ||||
|         with trio.fail_after(2): | ||||
|             async with tractor.open_nursery() as n: | ||||
|                 p = await n.start_actor( | ||||
|  | @ -307,7 +320,9 @@ async def aio_cancel(): | |||
|     await aio_sleep_forever() | ||||
| 
 | ||||
| 
 | ||||
| def test_aio_cancelled_from_aio_causes_trio_cancelled(reg_addr): | ||||
| def test_aio_cancelled_from_aio_causes_trio_cancelled( | ||||
|     reg_addr: tuple, | ||||
| ): | ||||
|     ''' | ||||
|     When the `asyncio.Task` cancels itself the `trio` side cshould | ||||
|     also cancel and teardown and relay the cancellation cross-process | ||||
|  | @ -404,6 +419,7 @@ async def stream_from_aio( | |||
|             sequence=seq, | ||||
|             expect_cancel=raise_err or exit_early, | ||||
|             fail_early=aio_raise_err, | ||||
| 
 | ||||
|         ) as (first, chan): | ||||
| 
 | ||||
|             assert first is True | ||||
|  | @ -422,10 +438,15 @@ async def stream_from_aio( | |||
|                         if raise_err: | ||||
|                             raise Exception | ||||
|                         elif exit_early: | ||||
|                             print('`consume()` breaking early!\n') | ||||
|                             break | ||||
| 
 | ||||
|                 print('returning from `consume()`..\n') | ||||
| 
 | ||||
|             # run 2 tasks each pulling from | ||||
|             # the inter-task-channel with the 2nd | ||||
|             # using a fan-out `BroadcastReceiver`. | ||||
|             if fan_out: | ||||
|                 # start second task that get's the same stream value set. | ||||
|                 async with ( | ||||
| 
 | ||||
|                     # NOTE: this has to come first to avoid | ||||
|  | @ -435,11 +456,19 @@ async def stream_from_aio( | |||
| 
 | ||||
|                     trio.open_nursery() as n, | ||||
|                 ): | ||||
|                     # start 2nd task that get's broadcast the same | ||||
|                     # value set. | ||||
|                     n.start_soon(consume, br) | ||||
|                     await consume(chan) | ||||
| 
 | ||||
|             else: | ||||
|                 await consume(chan) | ||||
|     except BaseException as err: | ||||
|         import logging | ||||
|         log = logging.getLogger() | ||||
|         log.exception('aio-subactor errored!\n') | ||||
|         raise err | ||||
| 
 | ||||
|     finally: | ||||
| 
 | ||||
|         if ( | ||||
|  | @ -460,7 +489,8 @@ async def stream_from_aio( | |||
|             assert not fan_out | ||||
|             assert pulled == expect[:51] | ||||
| 
 | ||||
|         print('trio guest mode task completed!') | ||||
|         print('trio guest-mode task completed!') | ||||
|         assert chan._aio_task.done() | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|  | @ -500,19 +530,37 @@ def test_trio_error_cancels_intertask_chan(reg_addr): | |||
|     excinfo.value.boxed_type is Exception | ||||
| 
 | ||||
| 
 | ||||
| def test_trio_closes_early_and_channel_exits(reg_addr): | ||||
| def test_trio_closes_early_and_channel_exits( | ||||
|     reg_addr: tuple[str, int], | ||||
| ): | ||||
|     ''' | ||||
|     Check that if the `trio`-task "exits early" on `async for`ing the | ||||
|     inter-task-channel (via a `break`) we exit silently from the | ||||
|     `open_channel_from()` block and get a final `Return[None]` msg. | ||||
| 
 | ||||
|     ''' | ||||
|     async def main(): | ||||
|         async with tractor.open_nursery() as n: | ||||
|             portal = await n.run_in_actor( | ||||
|                 stream_from_aio, | ||||
|                 exit_early=True, | ||||
|                 infect_asyncio=True, | ||||
|             ) | ||||
|             # should raise RAE diectly | ||||
|             await portal.result() | ||||
|         with trio.fail_after(2): | ||||
|             async with tractor.open_nursery( | ||||
|                 # debug_mode=True, | ||||
|                 # enable_stack_on_sig=True, | ||||
|             ) as n: | ||||
|                 portal = await n.run_in_actor( | ||||
|                     stream_from_aio, | ||||
|                     exit_early=True, | ||||
|                     infect_asyncio=True, | ||||
|                 ) | ||||
|                 # should raise RAE diectly | ||||
|                 print('waiting on final infected subactor result..') | ||||
|                 res: None = await portal.wait_for_result() | ||||
|                 assert res is None | ||||
|                 print('infected subactor returned result: {res!r}\n') | ||||
| 
 | ||||
|     # should be a quiet exit on a simple channel exit | ||||
|     trio.run(main) | ||||
|     trio.run( | ||||
|         main, | ||||
|         # strict_exception_groups=False, | ||||
|     ) | ||||
| 
 | ||||
| 
 | ||||
| def test_aio_errors_and_channel_propagates_and_closes(reg_addr): | ||||
|  | @ -536,41 +584,40 @@ def test_aio_errors_and_channel_propagates_and_closes(reg_addr): | |||
|     excinfo.value.boxed_type is Exception | ||||
| 
 | ||||
| 
 | ||||
| async def aio_echo_server( | ||||
|     to_trio: trio.MemorySendChannel, | ||||
|     from_trio: asyncio.Queue, | ||||
| ) -> None: | ||||
| 
 | ||||
|     to_trio.send_nowait('start') | ||||
| 
 | ||||
|     while True: | ||||
|         msg = await from_trio.get() | ||||
| 
 | ||||
|         # echo the msg back | ||||
|         to_trio.send_nowait(msg) | ||||
| 
 | ||||
|         # if we get the terminate sentinel | ||||
|         # break the echo loop | ||||
|         if msg is None: | ||||
|             print('breaking aio echo loop') | ||||
|             break | ||||
| 
 | ||||
|     print('exiting asyncio task') | ||||
| 
 | ||||
| 
 | ||||
| @tractor.context | ||||
| async def trio_to_aio_echo_server( | ||||
|     ctx: tractor.Context, | ||||
|     ctx: tractor.Context|None, | ||||
| ): | ||||
| 
 | ||||
|     async def aio_echo_server( | ||||
|         to_trio: trio.MemorySendChannel, | ||||
|         from_trio: asyncio.Queue, | ||||
|     ) -> None: | ||||
| 
 | ||||
|         to_trio.send_nowait('start') | ||||
| 
 | ||||
|         while True: | ||||
|             msg = await from_trio.get() | ||||
| 
 | ||||
|             # echo the msg back | ||||
|             to_trio.send_nowait(msg) | ||||
| 
 | ||||
|             # if we get the terminate sentinel | ||||
|             # break the echo loop | ||||
|             if msg is None: | ||||
|                 print('breaking aio echo loop') | ||||
|                 break | ||||
| 
 | ||||
|         print('exiting asyncio task') | ||||
| 
 | ||||
|     async with to_asyncio.open_channel_from( | ||||
|         aio_echo_server, | ||||
|     ) as (first, chan): | ||||
| 
 | ||||
|         assert first == 'start' | ||||
| 
 | ||||
|         await ctx.started(first) | ||||
| 
 | ||||
|         async with ctx.open_stream() as stream: | ||||
| 
 | ||||
|             async for msg in stream: | ||||
|                 print(f'asyncio echoing {msg}') | ||||
|                 await chan.send(msg) | ||||
|  | @ -649,7 +696,6 @@ def test_echoserver_detailed_mechanics( | |||
|         trio.run(main) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @tractor.context | ||||
| async def manage_file( | ||||
|     ctx: tractor.Context, | ||||
|  |  | |||
|  | @ -0,0 +1,244 @@ | |||
| ''' | ||||
| Special attention cases for using "infect `asyncio`" mode from a root | ||||
| actor; i.e. not using a std `trio.run()` bootstrap. | ||||
| 
 | ||||
| ''' | ||||
| import asyncio | ||||
| from functools import partial | ||||
| 
 | ||||
| import pytest | ||||
| import trio | ||||
| import tractor | ||||
| from tractor import ( | ||||
|     to_asyncio, | ||||
| ) | ||||
| from tests.test_infected_asyncio import ( | ||||
|     aio_echo_server, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     'raise_error_mid_stream', | ||||
|     [ | ||||
|         False, | ||||
|         Exception, | ||||
|         KeyboardInterrupt, | ||||
|     ], | ||||
|     ids='raise_error={}'.format, | ||||
| ) | ||||
| def test_infected_root_actor( | ||||
|     raise_error_mid_stream: bool|Exception, | ||||
| 
 | ||||
|     # conftest wide | ||||
|     loglevel: str, | ||||
|     debug_mode: bool, | ||||
| ): | ||||
|     ''' | ||||
|     Verify you can run the `tractor` runtime with `Actor.is_infected_aio() == True` | ||||
|     in the root actor. | ||||
| 
 | ||||
|     ''' | ||||
|     async def _trio_main(): | ||||
|         with trio.fail_after(2): | ||||
|             first: str | ||||
|             chan: to_asyncio.LinkedTaskChannel | ||||
|             async with ( | ||||
|                 tractor.open_root_actor( | ||||
|                     debug_mode=debug_mode, | ||||
|                     loglevel=loglevel, | ||||
|                 ), | ||||
|                 to_asyncio.open_channel_from( | ||||
|                     aio_echo_server, | ||||
|                 ) as (first, chan), | ||||
|             ): | ||||
|                 assert first == 'start' | ||||
| 
 | ||||
|                 for i in range(1000): | ||||
|                     await chan.send(i) | ||||
|                     out = await chan.receive() | ||||
|                     assert out == i | ||||
|                     print(f'asyncio echoing {i}') | ||||
| 
 | ||||
|                     if raise_error_mid_stream and i == 500: | ||||
|                         raise raise_error_mid_stream | ||||
| 
 | ||||
|                     if out is None: | ||||
|                         try: | ||||
|                             out = await chan.receive() | ||||
|                         except trio.EndOfChannel: | ||||
|                             break | ||||
|                         else: | ||||
|                             raise RuntimeError( | ||||
|                                 'aio channel never stopped?' | ||||
|                             ) | ||||
| 
 | ||||
|     if raise_error_mid_stream: | ||||
|         with pytest.raises(raise_error_mid_stream): | ||||
|             tractor.to_asyncio.run_as_asyncio_guest( | ||||
|                 trio_main=_trio_main, | ||||
|             ) | ||||
|     else: | ||||
|         tractor.to_asyncio.run_as_asyncio_guest( | ||||
|             trio_main=_trio_main, | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| async def sync_and_err( | ||||
|     # just signature placeholders for compat with | ||||
|     # ``to_asyncio.open_channel_from()`` | ||||
|     to_trio: trio.MemorySendChannel, | ||||
|     from_trio: asyncio.Queue, | ||||
|     ev: asyncio.Event, | ||||
| 
 | ||||
| ): | ||||
|     if to_trio: | ||||
|         to_trio.send_nowait('start') | ||||
| 
 | ||||
|     await ev.wait() | ||||
|     raise RuntimeError('asyncio-side') | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     'aio_err_trigger', | ||||
|     [ | ||||
|         'before_start_point', | ||||
|         'after_trio_task_starts', | ||||
|         'after_start_point', | ||||
|     ], | ||||
|     ids='aio_err_triggered={}'.format | ||||
| ) | ||||
| def test_trio_prestarted_task_bubbles( | ||||
|     aio_err_trigger: str, | ||||
| 
 | ||||
|     # conftest wide | ||||
|     loglevel: str, | ||||
|     debug_mode: bool, | ||||
| ): | ||||
|     async def pre_started_err( | ||||
|         raise_err: bool = False, | ||||
|         pre_sleep: float|None = None, | ||||
|         aio_trigger: asyncio.Event|None = None, | ||||
|         task_status=trio.TASK_STATUS_IGNORED, | ||||
|     ): | ||||
|         ''' | ||||
|         Maybe pre-started error then sleep. | ||||
| 
 | ||||
|         ''' | ||||
|         if pre_sleep is not None: | ||||
|             print(f'Sleeping from trio for {pre_sleep!r}s !') | ||||
|             await trio.sleep(pre_sleep) | ||||
| 
 | ||||
|         # signal aio-task to raise JUST AFTER this task | ||||
|         # starts but has not yet `.started()` | ||||
|         if aio_trigger: | ||||
|             print('Signalling aio-task to raise from `trio`!!') | ||||
|             aio_trigger.set() | ||||
| 
 | ||||
|         if raise_err: | ||||
|             print('Raising from trio!') | ||||
|             raise TypeError('trio-side') | ||||
| 
 | ||||
|         task_status.started() | ||||
|         await trio.sleep_forever() | ||||
| 
 | ||||
|     async def _trio_main(): | ||||
|         # with trio.fail_after(2): | ||||
|         with trio.fail_after(999): | ||||
|             first: str | ||||
|             chan: to_asyncio.LinkedTaskChannel | ||||
|             aio_ev = asyncio.Event() | ||||
| 
 | ||||
|             async with ( | ||||
|                 tractor.open_root_actor( | ||||
|                     debug_mode=False, | ||||
|                     loglevel=loglevel, | ||||
|                 ), | ||||
|             ): | ||||
|                 # TODO, tests for this with 3.13 egs? | ||||
|                 # from tractor.devx import open_crash_handler | ||||
|                 # with open_crash_handler(): | ||||
|                 async with ( | ||||
|                     # where we'll start a sub-task that errors BEFORE | ||||
|                     # calling `.started()` such that the error should | ||||
|                     # bubble before the guest run terminates! | ||||
|                     trio.open_nursery() as tn, | ||||
| 
 | ||||
|                     # THEN start an infect task which should error just | ||||
|                     # after the trio-side's task does. | ||||
|                     to_asyncio.open_channel_from( | ||||
|                         partial( | ||||
|                             sync_and_err, | ||||
|                             ev=aio_ev, | ||||
|                         ) | ||||
|                     ) as (first, chan), | ||||
|                 ): | ||||
| 
 | ||||
|                     for i in range(5): | ||||
|                         pre_sleep: float|None = None | ||||
|                         last_iter: bool = (i == 4) | ||||
| 
 | ||||
|                         # TODO, missing cases? | ||||
|                         # -[ ] error as well on | ||||
|                         #    'after_start_point' case as well for | ||||
|                         #    another case? | ||||
|                         raise_err: bool = False | ||||
| 
 | ||||
|                         if last_iter: | ||||
|                             raise_err: bool = True | ||||
| 
 | ||||
|                             # trigger aio task to error on next loop | ||||
|                             # tick/checkpoint | ||||
|                             if aio_err_trigger == 'before_start_point': | ||||
|                                 aio_ev.set() | ||||
| 
 | ||||
|                             pre_sleep: float = 0 | ||||
| 
 | ||||
|                         await tn.start( | ||||
|                             pre_started_err, | ||||
|                             raise_err, | ||||
|                             pre_sleep, | ||||
|                             (aio_ev if ( | ||||
|                                     aio_err_trigger == 'after_trio_task_starts' | ||||
|                                     and | ||||
|                                     last_iter | ||||
|                                 ) else None | ||||
|                             ), | ||||
|                         ) | ||||
| 
 | ||||
|                         if ( | ||||
|                             aio_err_trigger == 'after_start_point' | ||||
|                             and | ||||
|                             last_iter | ||||
|                         ): | ||||
|                             aio_ev.set() | ||||
| 
 | ||||
|     with pytest.raises( | ||||
|         expected_exception=ExceptionGroup, | ||||
|     ) as excinfo: | ||||
|         tractor.to_asyncio.run_as_asyncio_guest( | ||||
|             trio_main=_trio_main, | ||||
|         ) | ||||
| 
 | ||||
|     eg = excinfo.value | ||||
|     rte_eg, rest_eg = eg.split(RuntimeError) | ||||
| 
 | ||||
|     # ensure the trio-task's error bubbled despite the aio-side | ||||
|     # having (maybe) errored first. | ||||
|     if aio_err_trigger in ( | ||||
|         'after_trio_task_starts', | ||||
|         'after_start_point', | ||||
|     ): | ||||
|         assert len(errs := rest_eg.exceptions) == 1 | ||||
|         typerr = errs[0] | ||||
|         assert ( | ||||
|             type(typerr) is TypeError | ||||
|             and | ||||
|             'trio-side' in typerr.args | ||||
|         ) | ||||
| 
 | ||||
|     # when aio errors BEFORE (last) trio task is scheduled, we should | ||||
|     # never see anythinb but the aio-side. | ||||
|     else: | ||||
|         assert len(rtes := rte_eg.exceptions) == 1 | ||||
|         assert 'asyncio-side' in rtes[0].args[0] | ||||
|  | @ -3,6 +3,10 @@ Reminders for oddities in `trio` that we need to stay aware of and/or | |||
| want to see changed. | ||||
| 
 | ||||
| ''' | ||||
| from contextlib import ( | ||||
|     asynccontextmanager as acm, | ||||
| ) | ||||
| 
 | ||||
| import pytest | ||||
| import trio | ||||
| from trio import TaskStatus | ||||
|  | @ -80,3 +84,115 @@ def test_stashed_child_nursery(use_start_soon): | |||
| 
 | ||||
|     with pytest.raises(NameError): | ||||
|         trio.run(main) | ||||
| 
 | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     ('unmask_from_canc', 'canc_from_finally'), | ||||
|     [ | ||||
|         (True, False), | ||||
|         (True, True), | ||||
|         pytest.param(False, True, | ||||
|                      marks=pytest.mark.xfail(reason="never raises!") | ||||
|         ), | ||||
|     ], | ||||
|     # TODO, ask ronny how to impl this .. XD | ||||
|     # ids='unmask_from_canc={0}, canc_from_finally={1}',#.format, | ||||
| ) | ||||
| def test_acm_embedded_nursery_propagates_enter_err( | ||||
|     canc_from_finally: bool, | ||||
|     unmask_from_canc: bool, | ||||
| ): | ||||
|     ''' | ||||
|     Demo how a masking `trio.Cancelled` could be handled by unmasking from the | ||||
|     `.__context__` field when a user (by accident) re-raises from a `finally:`. | ||||
| 
 | ||||
|     ''' | ||||
|     import tractor | ||||
| 
 | ||||
|     @acm | ||||
|     async def maybe_raise_from_masking_exc( | ||||
|         tn: trio.Nursery, | ||||
|         unmask_from: BaseException|None = trio.Cancelled | ||||
| 
 | ||||
|         # TODO, maybe offer a collection? | ||||
|         # unmask_from: set[BaseException] = { | ||||
|         #     trio.Cancelled, | ||||
|         # }, | ||||
|     ): | ||||
|         if not unmask_from: | ||||
|             yield | ||||
|             return | ||||
| 
 | ||||
|         try: | ||||
|             yield | ||||
|         except* unmask_from as be_eg: | ||||
| 
 | ||||
|             # TODO, if we offer `unmask_from: set` | ||||
|             # for masker_exc_type in unmask_from: | ||||
| 
 | ||||
|             matches, rest = be_eg.split(unmask_from) | ||||
|             if not matches: | ||||
|                 raise | ||||
| 
 | ||||
|             for exc_match in be_eg.exceptions: | ||||
|                 if ( | ||||
|                     (exc_ctx := exc_match.__context__) | ||||
|                     and | ||||
|                     type(exc_ctx) not in { | ||||
|                         # trio.Cancelled,  # always by default? | ||||
|                         unmask_from, | ||||
|                     } | ||||
|                 ): | ||||
|                     exc_ctx.add_note( | ||||
|                         f'\n' | ||||
|                         f'WARNING: the above error was masked by a {unmask_from!r} !?!\n' | ||||
|                         f'Are you always cancelling? Say from a `finally:` ?\n\n' | ||||
| 
 | ||||
|                         f'{tn!r}' | ||||
|                     ) | ||||
|                     raise exc_ctx from exc_match | ||||
| 
 | ||||
| 
 | ||||
|     @acm | ||||
|     async def wraps_tn_that_always_cancels(): | ||||
|         async with ( | ||||
|             trio.open_nursery() as tn, | ||||
|             maybe_raise_from_masking_exc( | ||||
|                 tn=tn, | ||||
|                 unmask_from=( | ||||
|                     trio.Cancelled | ||||
|                     if unmask_from_canc | ||||
|                     else None | ||||
|                 ), | ||||
|             ) | ||||
|         ): | ||||
|             try: | ||||
|                 yield tn | ||||
|             finally: | ||||
|                 if canc_from_finally: | ||||
|                     tn.cancel_scope.cancel() | ||||
|                     await trio.lowlevel.checkpoint() | ||||
| 
 | ||||
|     async def _main(): | ||||
|         with tractor.devx.open_crash_handler() as bxerr: | ||||
|             assert not bxerr.value | ||||
| 
 | ||||
|             async with ( | ||||
|                 wraps_tn_that_always_cancels() as tn, | ||||
|             ): | ||||
|                 assert not tn.cancel_scope.cancel_called | ||||
|                 assert 0 | ||||
| 
 | ||||
|         assert ( | ||||
|             (err := bxerr.value) | ||||
|             and | ||||
|             type(err) is AssertionError | ||||
|         ) | ||||
| 
 | ||||
|     with pytest.raises(ExceptionGroup) as excinfo: | ||||
|         trio.run(_main) | ||||
| 
 | ||||
|     eg: ExceptionGroup = excinfo.value | ||||
|     assert_eg, rest_eg = eg.split(AssertionError) | ||||
| 
 | ||||
|     assert len(assert_eg.exceptions) == 1 | ||||
|  |  | |||
|  | @ -47,6 +47,9 @@ from functools import partial | |||
| import inspect | ||||
| from pprint import pformat | ||||
| import textwrap | ||||
| from types import ( | ||||
|     UnionType, | ||||
| ) | ||||
| from typing import ( | ||||
|     Any, | ||||
|     AsyncGenerator, | ||||
|  | @ -2544,7 +2547,14 @@ def context( | |||
|     name: str | ||||
|     param: Type | ||||
|     for name, param in annots.items(): | ||||
|         if param is Context: | ||||
|         if ( | ||||
|             param is Context | ||||
|             or ( | ||||
|                 isinstance(param, UnionType) | ||||
|                 and | ||||
|                 Context in param.__args__ | ||||
|             ) | ||||
|         ): | ||||
|             ctx_var_name: str = name | ||||
|             break | ||||
|     else: | ||||
|  |  | |||
|  | @ -1146,19 +1146,51 @@ def unpack_error( | |||
| 
 | ||||
| 
 | ||||
| def is_multi_cancelled( | ||||
|     exc: BaseException|BaseExceptionGroup | ||||
| ) -> bool: | ||||
|     exc: BaseException|BaseExceptionGroup, | ||||
| 
 | ||||
|     ignore_nested: set[BaseException] = set(), | ||||
| 
 | ||||
| ) -> bool|BaseExceptionGroup: | ||||
|     ''' | ||||
|     Predicate to determine if a possible ``BaseExceptionGroup`` contains | ||||
|     only ``trio.Cancelled`` sub-exceptions (and is likely the result of | ||||
|     cancelling a collection of subtasks. | ||||
|     Predicate to determine if an `BaseExceptionGroup` only contains | ||||
|     some (maybe nested) set of sub-grouped exceptions (like only | ||||
|     `trio.Cancelled`s which get swallowed silently by default) and is | ||||
|     thus the result of "gracefully cancelling" a collection of | ||||
|     sub-tasks (or other conc primitives) and receiving a "cancelled | ||||
|     ACK" from each after termination. | ||||
| 
 | ||||
|     Docs: | ||||
|     ---- | ||||
|     - https://docs.python.org/3/library/exceptions.html#exception-groups | ||||
|     - https://docs.python.org/3/library/exceptions.html#BaseExceptionGroup.subgroup | ||||
| 
 | ||||
|     ''' | ||||
| 
 | ||||
|     if ( | ||||
|         not ignore_nested | ||||
|         or | ||||
|         trio.Cancelled in ignore_nested | ||||
|         # XXX always count-in `trio`'s native signal | ||||
|     ): | ||||
|         ignore_nested |= {trio.Cancelled} | ||||
| 
 | ||||
|     if isinstance(exc, BaseExceptionGroup): | ||||
|         return exc.subgroup( | ||||
|             lambda exc: isinstance(exc, trio.Cancelled) | ||||
|         ) is not None | ||||
|         matched_exc: BaseExceptionGroup|None = exc.subgroup( | ||||
|             tuple(ignore_nested), | ||||
| 
 | ||||
|             # TODO, complain about why not allowed XD | ||||
|             # condition=tuple(ignore_nested), | ||||
|         ) | ||||
|         if matched_exc is not None: | ||||
|             return matched_exc | ||||
| 
 | ||||
|     # NOTE, IFF no excs types match (throughout the error-tree) | ||||
|     # -> return `False`, OW return the matched sub-eg. | ||||
|     # | ||||
|     # IOW, for the inverse of ^ for the purpose of | ||||
|     # maybe-enter-REPL--logic: "only debug when the err-tree contains | ||||
|     # at least one exc-type NOT in `ignore_nested`" ; i.e. the case where | ||||
|     # we fallthrough and return `False` here. | ||||
|     return False | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -95,6 +95,13 @@ async def open_root_actor( | |||
| 
 | ||||
|     hide_tb: bool = True, | ||||
| 
 | ||||
|     # XXX, proxied directly to `.devx._debug._maybe_enter_pm()` | ||||
|     # for REPL-entry logic. | ||||
|     debug_filter: Callable[ | ||||
|         [BaseException|BaseExceptionGroup], | ||||
|         bool, | ||||
|     ] = lambda err: not is_multi_cancelled(err), | ||||
| 
 | ||||
|     # TODO, a way for actors to augment passing derived | ||||
|     # read-only state to sublayers? | ||||
|     # extra_rt_vars: dict|None = None, | ||||
|  | @ -334,6 +341,10 @@ async def open_root_actor( | |||
|             loglevel=loglevel, | ||||
|             enable_modules=enable_modules, | ||||
|         ) | ||||
|         # XXX, in case the root actor runtime was actually run from | ||||
|         # `tractor.to_asyncio.run_as_asyncio_guest()` and NOt | ||||
|         # `.trio.run()`. | ||||
|         actor._infected_aio = _state._runtime_vars['_is_infected_aio'] | ||||
| 
 | ||||
|     # Start up main task set via core actor-runtime nurseries. | ||||
|     try: | ||||
|  | @ -375,6 +386,7 @@ async def open_root_actor( | |||
|                 Exception, | ||||
|                 BaseExceptionGroup, | ||||
|             ) as err: | ||||
| 
 | ||||
|                 # XXX NOTE XXX see equiv note inside | ||||
|                 # `._runtime.Actor._stream_handler()` where in the | ||||
|                 # non-root or root-that-opened-this-mahually case we | ||||
|  | @ -383,11 +395,15 @@ async def open_root_actor( | |||
|                 entered: bool = await _debug._maybe_enter_pm( | ||||
|                     err, | ||||
|                     api_frame=inspect.currentframe(), | ||||
|                     debug_filter=debug_filter, | ||||
|                 ) | ||||
| 
 | ||||
|                 if ( | ||||
|                     not entered | ||||
|                     and | ||||
|                     not is_multi_cancelled(err) | ||||
|                     not is_multi_cancelled( | ||||
|                         err, | ||||
|                     ) | ||||
|                 ): | ||||
|                     logger.exception('Root actor crashed\n') | ||||
| 
 | ||||
|  |  | |||
|  | @ -41,3 +41,38 @@ from .pformat import ( | |||
|     pformat_caller_frame as pformat_caller_frame, | ||||
|     pformat_boxed_tb as pformat_boxed_tb, | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| def _enable_readline_feats() -> str: | ||||
|     ''' | ||||
|     Handle `readline` when compiled with `libedit` to avoid breaking | ||||
|     tab completion in `pdbp` (and its dep `tabcompleter`) | ||||
|     particularly since `uv` cpython distis are compiled this way.. | ||||
| 
 | ||||
|     See docs for deats, | ||||
|     https://docs.python.org/3/library/readline.html#module-readline | ||||
| 
 | ||||
|     Originally discovered soln via SO answer, | ||||
|     https://stackoverflow.com/q/49287102 | ||||
| 
 | ||||
|     ''' | ||||
|     import readline | ||||
|     if ( | ||||
|         # 3.13+ attr | ||||
|         # https://docs.python.org/3/library/readline.html#readline.backend | ||||
|         (getattr(readline, 'backend', False) == 'libedit') | ||||
|         or | ||||
|         'libedit' in readline.__doc__ | ||||
|     ): | ||||
|         readline.parse_and_bind("python:bind -v") | ||||
|         readline.parse_and_bind("python:bind ^I rl_complete") | ||||
|         return 'libedit' | ||||
|     else: | ||||
|         readline.parse_and_bind("tab: complete") | ||||
|         readline.parse_and_bind("set editing-mode vi") | ||||
|         readline.parse_and_bind("set keymap vi") | ||||
|         return 'readline' | ||||
| 
 | ||||
| 
 | ||||
| # TODO, move this to a new `.devx._pdbp` mod? | ||||
| _enable_readline_feats() | ||||
|  |  | |||
|  | @ -75,6 +75,7 @@ from tractor import _state | |||
| from tractor._exceptions import ( | ||||
|     InternalError, | ||||
|     NoRuntime, | ||||
|     is_multi_cancelled, | ||||
| ) | ||||
| from tractor._state import ( | ||||
|     current_actor, | ||||
|  | @ -316,6 +317,7 @@ 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(): | ||||
|  | @ -443,7 +445,10 @@ class Lock: | |||
|                         f'|_{repl_task}\n' | ||||
|                     ) | ||||
| 
 | ||||
|             log.devx(message) | ||||
|             if message: | ||||
|                 log.devx(message) | ||||
|             else: | ||||
|                 import pdbp; pdbp.set_trace() | ||||
| 
 | ||||
|         return we_released | ||||
| 
 | ||||
|  | @ -1743,7 +1748,7 @@ async def _pause( | |||
|     ] = trio.TASK_STATUS_IGNORED, | ||||
|     **debug_func_kwargs, | ||||
| 
 | ||||
| ) -> tuple[PdbREPL, Task]|None: | ||||
| ) -> tuple[Task, PdbREPL]|None: | ||||
|     ''' | ||||
|     Inner impl for `pause()` to avoid the `trio.CancelScope.__exit__()` | ||||
|     stack frame when not shielded (since apparently i can't figure out | ||||
|  | @ -1929,7 +1934,7 @@ async def _pause( | |||
|                 ) | ||||
|                 with trio.CancelScope(shield=shield): | ||||
|                     await trio.lowlevel.checkpoint() | ||||
|                 return repl, task | ||||
|                 return (repl, task) | ||||
| 
 | ||||
|             # elif repl_task: | ||||
|             #     log.warning( | ||||
|  | @ -2530,26 +2535,17 @@ def pause_from_sync( | |||
|             f'{actor.uid} task called `tractor.pause_from_sync()`\n' | ||||
|         ) | ||||
| 
 | ||||
|         # TODO: once supported, remove this AND the one | ||||
|         # inside `._pause()`! | ||||
|         # outstanding impl fixes: | ||||
|         # -[ ] need to make `.shield_sigint()` below work here! | ||||
|         # -[ ] how to handle `asyncio`'s new SIGINT-handler | ||||
|         #     injection? | ||||
|         # -[ ] should `breakpoint()` work and what does it normally | ||||
|         #     do in `asyncio` ctxs? | ||||
|         # if actor.is_infected_aio(): | ||||
|         #     raise RuntimeError( | ||||
|         #         '`tractor.pause[_from_sync]()` not yet supported ' | ||||
|         #         'for infected `asyncio` mode!' | ||||
|         #     ) | ||||
| 
 | ||||
|         repl: PdbREPL = mk_pdb() | ||||
| 
 | ||||
|         # message += f'-> created local REPL {repl}\n' | ||||
|         is_trio_thread: bool = DebugStatus.is_main_trio_thread() | ||||
|         is_root: bool = is_root_process() | ||||
|         is_aio: bool = actor.is_infected_aio() | ||||
|         is_infected_aio: bool = actor.is_infected_aio() | ||||
|         thread: Thread = threading.current_thread() | ||||
| 
 | ||||
|         asyncio_task: asyncio.Task|None = None | ||||
|         if is_infected_aio: | ||||
|             asyncio_task = asyncio.current_task() | ||||
| 
 | ||||
|         # TODO: we could also check for a non-`.to_thread` context | ||||
|         # using `trio.from_thread.check_cancelled()` (says | ||||
|  | @ -2565,24 +2561,18 @@ def pause_from_sync( | |||
|         if ( | ||||
|             not is_trio_thread | ||||
|             and | ||||
|             not is_aio  # see below for this usage | ||||
|             not asyncio_task | ||||
|         ): | ||||
|             # TODO: `threading.Lock()` this so we don't get races in | ||||
|             # multi-thr cases where they're acquiring/releasing the | ||||
|             # REPL and setting request/`Lock` state, etc.. | ||||
|             thread: threading.Thread = threading.current_thread() | ||||
|             repl_owner = thread | ||||
|             repl_owner: Thread = thread | ||||
| 
 | ||||
|             # TODO: make root-actor bg thread usage work! | ||||
|             if ( | ||||
|                 is_root | ||||
|                 # or | ||||
|                 # is_aio | ||||
|             ): | ||||
|                 if is_root: | ||||
|                     message += ( | ||||
|                         f'-> called from a root-actor bg {thread}\n' | ||||
|                     ) | ||||
|             if is_root: | ||||
|                 message += ( | ||||
|                     f'-> called from a root-actor bg {thread}\n' | ||||
|                 ) | ||||
| 
 | ||||
|                 message += ( | ||||
|                     '-> scheduling `._pause_from_bg_root_thread()`..\n' | ||||
|  | @ -2637,34 +2627,95 @@ def pause_from_sync( | |||
|                 DebugStatus.shield_sigint() | ||||
|                 assert bg_task is not DebugStatus.repl_task | ||||
| 
 | ||||
|         # TODO: once supported, remove this AND the one | ||||
|         # inside `._pause()`! | ||||
|         # outstanding impl fixes: | ||||
|         # -[ ] need to make `.shield_sigint()` below work here! | ||||
|         # -[ ] how to handle `asyncio`'s new SIGINT-handler | ||||
|         #     injection? | ||||
|         # -[ ] should `breakpoint()` work and what does it normally | ||||
|         #     do in `asyncio` ctxs? | ||||
|         # if actor.is_infected_aio(): | ||||
|         #     raise RuntimeError( | ||||
|         #         '`tractor.pause[_from_sync]()` not yet supported ' | ||||
|         #         'for infected `asyncio` mode!' | ||||
|         #     ) | ||||
|         elif ( | ||||
|             not is_trio_thread | ||||
|             and | ||||
|             is_aio | ||||
|             is_infected_aio  # as in, the special actor-runtime mode | ||||
|             # ^NOTE XXX, that doesn't mean the caller is necessarily | ||||
|             # an `asyncio.Task` just that `trio` has been embedded on | ||||
|             # the `asyncio` event loop! | ||||
|             and | ||||
|             asyncio_task  # transitive caller is an actual `asyncio.Task` | ||||
|         ): | ||||
|             greenback: ModuleType = maybe_import_greenback() | ||||
|             repl_owner: Task = asyncio.current_task() | ||||
|             DebugStatus.shield_sigint() | ||||
|             fute: asyncio.Future = run_trio_task_in_future( | ||||
|                 partial( | ||||
|                     _pause, | ||||
|                     debug_func=None, | ||||
|                     repl=repl, | ||||
|                     hide_tb=hide_tb, | ||||
| 
 | ||||
|                     # XXX to prevent `._pause()` for setting | ||||
|                     # `DebugStatus.repl_task` to the gb task! | ||||
|                     called_from_sync=True, | ||||
|                     called_from_bg_thread=True, | ||||
|             if greenback.has_portal(): | ||||
|                 DebugStatus.shield_sigint() | ||||
|                 fute: asyncio.Future = run_trio_task_in_future( | ||||
|                     partial( | ||||
|                         _pause, | ||||
|                         debug_func=None, | ||||
|                         repl=repl, | ||||
|                         hide_tb=hide_tb, | ||||
| 
 | ||||
|                     **_pause_kwargs | ||||
|                         # XXX to prevent `._pause()` for setting | ||||
|                         # `DebugStatus.repl_task` to the gb task! | ||||
|                         called_from_sync=True, | ||||
|                         called_from_bg_thread=True, | ||||
| 
 | ||||
|                         **_pause_kwargs | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|                 repl_owner = asyncio_task | ||||
|                 bg_task, _ = greenback.await_(fute) | ||||
|                 # TODO: ASYNC version -> `.pause_from_aio()`? | ||||
|                 # bg_task, _ = await fute | ||||
| 
 | ||||
|             # TODO: for async version -> `.pause_from_aio()`? | ||||
|             # bg_task, _ = await fute | ||||
|             bg_task, _ = greenback.await_(fute) | ||||
|             bg_task: asyncio.Task = asyncio.current_task() | ||||
|             # handle the case where an `asyncio` task has been | ||||
|             # spawned WITHOUT enabling a `greenback` portal.. | ||||
|             # => can often happen in 3rd party libs. | ||||
|             else: | ||||
|                 bg_task = repl_owner | ||||
| 
 | ||||
|                 # TODO, ostensibly we can just acquire the | ||||
|                 # debug lock directly presuming we're the | ||||
|                 # root actor running in infected asyncio | ||||
|                 # mode? | ||||
|                 # | ||||
|                 # TODO, this would be a special case where | ||||
|                 # a `_pause_from_root()` would come in very | ||||
|                 # handy! | ||||
|                 # if is_root: | ||||
|                 #     import pdbp; pdbp.set_trace() | ||||
|                 #     log.warning( | ||||
|                 #         'Allowing `asyncio` task to acquire debug-lock in root-actor..\n' | ||||
|                 #         'This is not fully implemented yet; there may be teardown hangs!\n\n' | ||||
|                 #     ) | ||||
|                 # else: | ||||
| 
 | ||||
|                 # simply unsupported, since there exists no hack (i | ||||
|                 # can think of) to workaround this in a subactor | ||||
|                 # which needs to lock the root's REPL ow we're sure | ||||
|                 # to get prompt stdstreams clobbering.. | ||||
|                 cf_repr: str = '' | ||||
|                 if api_frame: | ||||
|                     caller_frame: FrameType = api_frame.f_back | ||||
|                     cf_repr: str = f'caller_frame: {caller_frame!r}\n' | ||||
| 
 | ||||
|                 raise RuntimeError( | ||||
|                     f"CAN'T USE `greenback._await()` without a portal !?\n\n" | ||||
|                     f'Likely this task was NOT spawned via the `tractor.to_asyncio` API..\n' | ||||
|                     f'{asyncio_task}\n' | ||||
|                     f'{cf_repr}\n' | ||||
| 
 | ||||
|                     f'Prolly the task was started out-of-band (from some lib?)\n' | ||||
|                     f'AND one of the below was never called ??\n' | ||||
|                     f'- greenback.ensure_portal()\n' | ||||
|                     f'- greenback.bestow_portal(<task>)\n' | ||||
|                 ) | ||||
| 
 | ||||
|         else:  # we are presumably the `trio.run()` + main thread | ||||
|             # raises on not-found by default | ||||
|  | @ -2915,8 +2966,14 @@ async def _maybe_enter_pm( | |||
|     tb: TracebackType|None = None, | ||||
|     api_frame: FrameType|None = None, | ||||
|     hide_tb: bool = False, | ||||
| 
 | ||||
|     # only enter debugger REPL when returns `True` | ||||
|     debug_filter: Callable[ | ||||
|         [BaseException|BaseExceptionGroup], | ||||
|         bool, | ||||
|     ] = lambda err: not is_multi_cancelled(err), | ||||
| 
 | ||||
| ): | ||||
|     from tractor._exceptions import is_multi_cancelled | ||||
|     if ( | ||||
|         debug_mode() | ||||
| 
 | ||||
|  | @ -2933,7 +2990,8 @@ async def _maybe_enter_pm( | |||
| 
 | ||||
|         # Really we just want to mostly avoid catching KBIs here so there | ||||
|         # might be a simpler check we can do? | ||||
|         and not is_multi_cancelled(err) | ||||
|         and | ||||
|         debug_filter(err) | ||||
|     ): | ||||
|         api_frame: FrameType = api_frame or inspect.currentframe() | ||||
|         tb: TracebackType = tb or sys.exc_info()[2] | ||||
|  | @ -3114,7 +3172,7 @@ async def maybe_wait_for_debugger( | |||
| @cm | ||||
| def open_crash_handler( | ||||
|     catch: set[BaseException] = { | ||||
|         Exception, | ||||
|         # Exception, | ||||
|         BaseException, | ||||
|     }, | ||||
|     ignore: set[BaseException] = { | ||||
|  | @ -3135,14 +3193,30 @@ 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..? | ||||
|     # -[ ] consider just wtv is used by `pytest.raises()`? | ||||
|     # | ||||
|     boxed_maybe_exc = BoxedMaybeException() | ||||
|     err: BaseException | ||||
|     try: | ||||
|         yield | ||||
|         yield boxed_maybe_exc | ||||
|     except tuple(catch) as err: | ||||
|         if type(err) not in ignore: | ||||
| 
 | ||||
|             # use our re-impl-ed version | ||||
|         boxed_maybe_exc.value = err | ||||
|         if ( | ||||
|             type(err) not in ignore | ||||
|             and | ||||
|             not is_multi_cancelled( | ||||
|                 err, | ||||
|                 ignore_nested=ignore | ||||
|             ) | ||||
|         ): | ||||
|             try: | ||||
|                 # use our re-impl-ed version | ||||
|                 _post_mortem( | ||||
|                     repl=mk_pdb(), | ||||
|                     tb=sys.exc_info()[2], | ||||
|  | @ -3150,13 +3224,13 @@ def open_crash_handler( | |||
|                 ) | ||||
|             except bdb.BdbQuit: | ||||
|                 __tracebackhide__: bool = False | ||||
|                 raise | ||||
|                 raise err | ||||
| 
 | ||||
|             # XXX NOTE, `pdbp`'s version seems to lose the up-stack | ||||
|             # tb-info? | ||||
|             # pdbp.xpm() | ||||
| 
 | ||||
|         raise | ||||
|         raise err | ||||
| 
 | ||||
| 
 | ||||
| @cm | ||||
|  |  | |||
|  | @ -92,7 +92,7 @@ def pformat_boxed_tb( | |||
|         f' ------ {boxer_header} ------\n' | ||||
|         f'{tb_body}' | ||||
|         f' ------ {boxer_header}- ------\n' | ||||
|         f'_|\n' | ||||
|         f'_|' | ||||
|     ) | ||||
|     tb_box_indent: str = ( | ||||
|         tb_box_indent | ||||
|  |  | |||
|  | @ -0,0 +1,26 @@ | |||
| # tractor: structured concurrent "actors". | ||||
| # Copyright 2024-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/>. | ||||
| 
 | ||||
| ''' | ||||
| High level design patterns, APIs and runtime extensions built on top | ||||
| of the `tractor` runtime core. | ||||
| 
 | ||||
| ''' | ||||
| from ._service import ( | ||||
|     open_service_mngr as open_service_mngr, | ||||
|     get_service_mngr as get_service_mngr, | ||||
|     ServiceMngr as ServiceMngr, | ||||
| ) | ||||
|  | @ -0,0 +1,592 @@ | |||
| # tractor: structured concurrent "actors". | ||||
| # Copyright 2024-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/>. | ||||
| 
 | ||||
| ''' | ||||
| Daemon subactor as service(s) management and supervision primitives | ||||
| and API. | ||||
| 
 | ||||
| ''' | ||||
| from __future__ import annotations | ||||
| from contextlib import ( | ||||
|     asynccontextmanager as acm, | ||||
|     # contextmanager as cm, | ||||
| ) | ||||
| from collections import defaultdict | ||||
| from dataclasses import ( | ||||
|     dataclass, | ||||
|     field, | ||||
| ) | ||||
| import functools | ||||
| import inspect | ||||
| from typing import ( | ||||
|     Callable, | ||||
|     Any, | ||||
| ) | ||||
| 
 | ||||
| import tractor | ||||
| import trio | ||||
| from trio import TaskStatus | ||||
| from tractor import ( | ||||
|     log, | ||||
|     ActorNursery, | ||||
|     current_actor, | ||||
|     ContextCancelled, | ||||
|     Context, | ||||
|     Portal, | ||||
| ) | ||||
| 
 | ||||
| log = log.get_logger('tractor') | ||||
| 
 | ||||
| 
 | ||||
| # TODO: implement a `@singleton` deco-API for wrapping the below | ||||
| # factory's impl for general actor-singleton use? | ||||
| # | ||||
| # -[ ] go through the options peeps on SO did? | ||||
| #  * https://stackoverflow.com/questions/6760685/what-is-the-best-way-of-implementing-singleton-in-python | ||||
| #  * including @mikenerone's answer | ||||
| #   |_https://stackoverflow.com/questions/6760685/what-is-the-best-way-of-implementing-singleton-in-python/39186313#39186313 | ||||
| # | ||||
| # -[ ] put it in `tractor.lowlevel._globals` ? | ||||
| #  * fits with our oustanding actor-local/global feat req? | ||||
| #   |_ https://github.com/goodboy/tractor/issues/55 | ||||
| #  * how can it relate to the `Actor.lifetime_stack` that was | ||||
| #    silently patched in? | ||||
| #   |_ we could implicitly call both of these in the same | ||||
| #     spot in the runtime using the lifetime stack? | ||||
| #    - `open_singleton_cm().__exit__()` | ||||
| #    -`del_singleton()` | ||||
| #   |_ gives SC fixtue semantics to sync code oriented around | ||||
| #     sub-process lifetime? | ||||
| #  * what about with `trio.RunVar`? | ||||
| #   |_https://trio.readthedocs.io/en/stable/reference-lowlevel.html#trio.lowlevel.RunVar | ||||
| #    - which we'll need for no-GIL cpython (right?) presuming | ||||
| #      multiple `trio.run()` calls in process? | ||||
| # | ||||
| # | ||||
| # @singleton | ||||
| # async def open_service_mngr( | ||||
| #     **init_kwargs, | ||||
| # ) -> ServiceMngr: | ||||
| #     ''' | ||||
| #     Note this function body is invoke IFF no existing singleton instance already | ||||
| #     exists in this proc's memory. | ||||
| 
 | ||||
| #     ''' | ||||
| #     # setup | ||||
| #     yield ServiceMngr(**init_kwargs) | ||||
| #     # teardown | ||||
| 
 | ||||
| 
 | ||||
| # a deletion API for explicit instance de-allocation? | ||||
| # @open_service_mngr.deleter | ||||
| # def del_service_mngr() -> None: | ||||
| #     mngr = open_service_mngr._singleton[0] | ||||
| #     open_service_mngr._singleton[0] = None | ||||
| #     del mngr | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| # TODO: implement a singleton deco-API for wrapping the below | ||||
| # factory's impl for general actor-singleton use? | ||||
| # | ||||
| # @singleton | ||||
| # async def open_service_mngr( | ||||
| #     **init_kwargs, | ||||
| # ) -> ServiceMngr: | ||||
| #     ''' | ||||
| #     Note this function body is invoke IFF no existing singleton instance already | ||||
| #     exists in this proc's memory. | ||||
| 
 | ||||
| #     ''' | ||||
| #     # setup | ||||
| #     yield ServiceMngr(**init_kwargs) | ||||
| #     # teardown | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| # TODO: singleton factory API instead of a class API | ||||
| @acm | ||||
| async def open_service_mngr( | ||||
|     *, | ||||
|     debug_mode: bool = False, | ||||
| 
 | ||||
|     # NOTE; since default values for keyword-args are effectively | ||||
|     # module-vars/globals as per the note from, | ||||
|     # https://docs.python.org/3/tutorial/controlflow.html#default-argument-values | ||||
|     # | ||||
|     # > "The default value is evaluated only once. This makes | ||||
|     #   a difference when the default is a mutable object such as | ||||
|     #   a list, dictionary, or instances of most classes" | ||||
|     # | ||||
|     _singleton: list[ServiceMngr|None] = [None], | ||||
|     **init_kwargs, | ||||
| 
 | ||||
| ) -> ServiceMngr: | ||||
|     ''' | ||||
|     Open an actor-global "service-manager" for supervising a tree | ||||
|     of subactors and/or actor-global tasks. | ||||
| 
 | ||||
|     The delivered `ServiceMngr` is singleton instance for each | ||||
|     actor-process, that is, allocated on first open and never | ||||
|     de-allocated unless explicitly deleted by al call to | ||||
|     `del_service_mngr()`. | ||||
| 
 | ||||
|     ''' | ||||
|     # TODO: factor this an allocation into | ||||
|     # a `._mngr.open_service_mngr()` and put in the | ||||
|     # once-n-only-once setup/`.__aenter__()` part! | ||||
|     # -[ ] how to make this only happen on the `mngr == None` case? | ||||
|     #  |_ use `.trionics.maybe_open_context()` (for generic | ||||
|     #     async-with-style-only-once of the factory impl, though | ||||
|     #     what do we do for the allocation case? | ||||
|     #    / `.maybe_open_nursery()` (since for this specific case | ||||
|     #    it's simpler?) to activate | ||||
|     async with ( | ||||
|         tractor.open_nursery() as an, | ||||
|         trio.open_nursery() as tn, | ||||
|     ): | ||||
|         # impl specific obvi.. | ||||
|         init_kwargs.update({ | ||||
|             'an': an, | ||||
|             'tn': tn, | ||||
|         }) | ||||
| 
 | ||||
|         mngr: ServiceMngr|None | ||||
|         if (mngr := _singleton[0]) is None: | ||||
| 
 | ||||
|             log.info('Allocating a new service mngr!') | ||||
|             mngr = _singleton[0] = ServiceMngr(**init_kwargs) | ||||
| 
 | ||||
|             # TODO: put into `.__aenter__()` section of | ||||
|             # eventual `@singleton_acm` API wrapper. | ||||
|             # | ||||
|             # assign globally for future daemon/task creation | ||||
|             mngr.an = an | ||||
|             mngr.tn = tn | ||||
| 
 | ||||
|         else: | ||||
|             assert (mngr.an and mngr.tn) | ||||
|             log.info( | ||||
|                 'Using extant service mngr!\n\n' | ||||
|                 f'{mngr!r}\n'  # it has a nice `.__repr__()` of services state | ||||
|             ) | ||||
| 
 | ||||
|         try: | ||||
|             # NOTE: this is a singleton factory impl specific detail | ||||
|             # which should be supported in the condensed | ||||
|             # `@singleton_acm` API? | ||||
|             mngr.debug_mode = debug_mode | ||||
| 
 | ||||
|             yield mngr | ||||
|         finally: | ||||
|             # TODO: is this more clever/efficient? | ||||
|             # if 'samplerd' in mngr.service_ctxs: | ||||
|             #     await mngr.cancel_service('samplerd') | ||||
|             tn.cancel_scope.cancel() | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| def get_service_mngr() -> ServiceMngr: | ||||
|     ''' | ||||
|     Try to get the singleton service-mngr for this actor presuming it | ||||
|     has already been allocated using, | ||||
| 
 | ||||
|     .. code:: python | ||||
| 
 | ||||
|         async with open_<@singleton_acm(func)>() as mngr` | ||||
|             ... this block kept open ... | ||||
| 
 | ||||
|     If not yet allocated raise a `ServiceError`. | ||||
| 
 | ||||
|     ''' | ||||
|     # https://stackoverflow.com/a/12627202 | ||||
|     # https://docs.python.org/3/library/inspect.html#inspect.Signature | ||||
|     maybe_mngr: ServiceMngr|None = inspect.signature( | ||||
|         open_service_mngr | ||||
|     ).parameters['_singleton'].default[0] | ||||
| 
 | ||||
|     if maybe_mngr is None: | ||||
|         raise RuntimeError( | ||||
|             'Someone must allocate a `ServiceMngr` using\n\n' | ||||
|             '`async with open_service_mngr()` beforehand!!\n' | ||||
|         ) | ||||
| 
 | ||||
|     return maybe_mngr | ||||
| 
 | ||||
| 
 | ||||
| async def _open_and_supervise_service_ctx( | ||||
|     serman: ServiceMngr, | ||||
|     name: str, | ||||
|     ctx_fn: Callable,  # TODO, type for `@tractor.context` requirement | ||||
|     portal: Portal, | ||||
| 
 | ||||
|     allow_overruns: bool = False, | ||||
|     task_status: TaskStatus[ | ||||
|         tuple[ | ||||
|             trio.CancelScope, | ||||
|             Context, | ||||
|             trio.Event, | ||||
|             Any, | ||||
|         ] | ||||
|     ] = trio.TASK_STATUS_IGNORED, | ||||
|     **ctx_kwargs, | ||||
| 
 | ||||
| ) -> Any: | ||||
|     ''' | ||||
|     Open a remote IPC-context defined by `ctx_fn` in the | ||||
|     (service) actor accessed via `portal` and supervise the | ||||
|     (local) parent task to termination at which point the remote | ||||
|     actor runtime is cancelled alongside it. | ||||
| 
 | ||||
|     The main application is for allocating long-running | ||||
|     "sub-services" in a main daemon and explicitly controlling | ||||
|     their lifetimes from an actor-global singleton. | ||||
| 
 | ||||
|     ''' | ||||
|     # TODO: use the ctx._scope directly here instead? | ||||
|     # -[ ] actually what semantics do we expect for this | ||||
|     #   usage!? | ||||
|     with trio.CancelScope() as cs: | ||||
|         try: | ||||
|             async with portal.open_context( | ||||
|                 ctx_fn, | ||||
|                 allow_overruns=allow_overruns, | ||||
|                 **ctx_kwargs, | ||||
| 
 | ||||
|             ) as (ctx, started): | ||||
| 
 | ||||
|                 # unblock once the remote context has started | ||||
|                 complete = trio.Event() | ||||
|                 task_status.started(( | ||||
|                     cs, | ||||
|                     ctx, | ||||
|                     complete, | ||||
|                     started, | ||||
|                 )) | ||||
|                 log.info( | ||||
|                     f'`pikerd` service {name} started with value {started}' | ||||
|                 ) | ||||
|                 # wait on any context's return value | ||||
|                 # and any final portal result from the | ||||
|                 # sub-actor. | ||||
|                 ctx_res: Any = await ctx.wait_for_result() | ||||
| 
 | ||||
|                 # NOTE: blocks indefinitely until cancelled | ||||
|                 # either by error from the target context | ||||
|                 # function or by being cancelled here by the | ||||
|                 # surrounding cancel scope. | ||||
|                 return ( | ||||
|                     await portal.wait_for_result(), | ||||
|                     ctx_res, | ||||
|                 ) | ||||
| 
 | ||||
|         except ContextCancelled as ctxe: | ||||
|             canceller: tuple[str, str] = ctxe.canceller | ||||
|             our_uid: tuple[str, str] = current_actor().uid | ||||
|             if ( | ||||
|                 canceller != portal.chan.uid | ||||
|                 and | ||||
|                 canceller != our_uid | ||||
|             ): | ||||
|                 log.cancel( | ||||
|                     f'Actor-service `{name}` was remotely cancelled by a peer?\n' | ||||
| 
 | ||||
|                     # TODO: this would be a good spot to use | ||||
|                     # a respawn feature Bo | ||||
|                     f'-> Keeping `pikerd` service manager alive despite this inter-peer cancel\n\n' | ||||
| 
 | ||||
|                     f'cancellee: {portal.chan.uid}\n' | ||||
|                     f'canceller: {canceller}\n' | ||||
|                 ) | ||||
|             else: | ||||
|                 raise | ||||
| 
 | ||||
|         finally: | ||||
|             # NOTE: the ctx MUST be cancelled first if we | ||||
|             # don't want the above `ctx.wait_for_result()` to | ||||
|             # raise a self-ctxc. WHY, well since from the ctx's | ||||
|             # perspective the cancel request will have | ||||
|             # arrived out-out-of-band at the `Actor.cancel()` | ||||
|             # level, thus `Context.cancel_called == False`, | ||||
|             # meaning `ctx._is_self_cancelled() == False`. | ||||
|             # with trio.CancelScope(shield=True): | ||||
|             # await ctx.cancel() | ||||
|             await portal.cancel_actor()  # terminate (remote) sub-actor | ||||
|             complete.set()  # signal caller this task is done | ||||
|             serman.service_ctxs.pop(name)  # remove mngr entry | ||||
| 
 | ||||
| 
 | ||||
| # TODO: we need remote wrapping and a general soln: | ||||
| # - factor this into a ``tractor.highlevel`` extension # pack for the | ||||
| #   library. | ||||
| # - wrap a "remote api" wherein you can get a method proxy | ||||
| #   to the pikerd actor for starting services remotely! | ||||
| # - prolly rename this to ActorServicesNursery since it spawns | ||||
| #   new actors and supervises them to completion? | ||||
| @dataclass | ||||
| class ServiceMngr: | ||||
|     ''' | ||||
|     A multi-subactor-as-service manager. | ||||
| 
 | ||||
|     Spawn, supervise and monitor service/daemon subactors in a SC | ||||
|     process tree. | ||||
| 
 | ||||
|     ''' | ||||
|     an: ActorNursery | ||||
|     tn: trio.Nursery | ||||
|     debug_mode: bool = False # tractor sub-actor debug mode flag | ||||
| 
 | ||||
|     service_tasks: dict[ | ||||
|         str, | ||||
|         tuple[ | ||||
|             trio.CancelScope, | ||||
|             trio.Event, | ||||
|         ] | ||||
|     ] = field(default_factory=dict) | ||||
| 
 | ||||
|     service_ctxs: dict[ | ||||
|         str, | ||||
|         tuple[ | ||||
|             trio.CancelScope, | ||||
|             Context, | ||||
|             Portal, | ||||
|             trio.Event, | ||||
|         ] | ||||
|     ] = field(default_factory=dict) | ||||
| 
 | ||||
|     # internal per-service task mutexs | ||||
|     _locks = defaultdict(trio.Lock) | ||||
| 
 | ||||
|     # TODO, unify this interface with our `TaskManager` PR! | ||||
|     # | ||||
|     # | ||||
|     async def start_service_task( | ||||
|         self, | ||||
|         name: str, | ||||
|         # TODO: typevar for the return type of the target and then | ||||
|         # use it below for `ctx_res`? | ||||
|         fn: Callable, | ||||
| 
 | ||||
|         allow_overruns: bool = False, | ||||
|         **ctx_kwargs, | ||||
| 
 | ||||
|     ) -> tuple[ | ||||
|         trio.CancelScope, | ||||
|         Any, | ||||
|         trio.Event, | ||||
|     ]: | ||||
|         async def _task_manager_start( | ||||
|             task_status: TaskStatus[ | ||||
|                 tuple[ | ||||
|                     trio.CancelScope, | ||||
|                     trio.Event, | ||||
|                 ] | ||||
|             ] = trio.TASK_STATUS_IGNORED, | ||||
|         ) -> Any: | ||||
| 
 | ||||
|             task_cs = trio.CancelScope() | ||||
|             task_complete = trio.Event() | ||||
| 
 | ||||
|             with task_cs as cs: | ||||
|                 task_status.started(( | ||||
|                     cs, | ||||
|                     task_complete, | ||||
|                 )) | ||||
|                 try: | ||||
|                     await fn() | ||||
|                 except trio.Cancelled as taskc: | ||||
|                     log.cancel( | ||||
|                         f'Service task for `{name}` was cancelled!\n' | ||||
|                         # TODO: this would be a good spot to use | ||||
|                         # a respawn feature Bo | ||||
|                     ) | ||||
|                     raise taskc | ||||
|                 finally: | ||||
|                     task_complete.set() | ||||
|         ( | ||||
|             cs, | ||||
|             complete, | ||||
|         ) = await self.tn.start(_task_manager_start) | ||||
| 
 | ||||
|         # store the cancel scope and portal for later cancellation or | ||||
|         # retstart if needed. | ||||
|         self.service_tasks[name] = ( | ||||
|             cs, | ||||
|             complete, | ||||
|         ) | ||||
|         return ( | ||||
|             cs, | ||||
|             complete, | ||||
|         ) | ||||
| 
 | ||||
|     async def cancel_service_task( | ||||
|         self, | ||||
|         name: str, | ||||
| 
 | ||||
|     ) -> Any: | ||||
|         log.info(f'Cancelling `pikerd` service {name}') | ||||
|         cs, complete = self.service_tasks[name] | ||||
| 
 | ||||
|         cs.cancel() | ||||
|         await complete.wait() | ||||
|         # TODO, if we use the `TaskMngr` from #346 | ||||
|         # we can also get the return value from the task! | ||||
| 
 | ||||
|         if name in self.service_tasks: | ||||
|             # TODO: custom err? | ||||
|             # raise ServiceError( | ||||
|             raise RuntimeError( | ||||
|                 f'Service task {name!r} not terminated!?\n' | ||||
|             ) | ||||
| 
 | ||||
|     async def start_service_ctx( | ||||
|         self, | ||||
|         name: str, | ||||
|         portal: Portal, | ||||
|         # TODO: typevar for the return type of the target and then | ||||
|         # use it below for `ctx_res`? | ||||
|         ctx_fn: Callable, | ||||
|         **ctx_kwargs, | ||||
| 
 | ||||
|     ) -> tuple[ | ||||
|         trio.CancelScope, | ||||
|         Context, | ||||
|         Any, | ||||
|     ]: | ||||
|         ''' | ||||
|         Start a remote IPC-context defined by `ctx_fn` in a background | ||||
|         task and immediately return supervision primitives to manage it: | ||||
| 
 | ||||
|         - a `cs: CancelScope` for the newly allocated bg task | ||||
|         - the `ipc_ctx: Context` to manage the remotely scheduled | ||||
|           `trio.Task`. | ||||
|         - the `started: Any` value returned by the remote endpoint | ||||
|           task's `Context.started(<value>)` call. | ||||
| 
 | ||||
|         The bg task supervises the ctx such that when it terminates the supporting | ||||
|         actor runtime is also cancelled, see `_open_and_supervise_service_ctx()` | ||||
|         for details. | ||||
| 
 | ||||
|         ''' | ||||
|         cs, ipc_ctx, complete, started = await self.tn.start( | ||||
|             functools.partial( | ||||
|                 _open_and_supervise_service_ctx, | ||||
|                 serman=self, | ||||
|                 name=name, | ||||
|                 ctx_fn=ctx_fn, | ||||
|                 portal=portal, | ||||
|                 **ctx_kwargs, | ||||
|             ) | ||||
|         ) | ||||
| 
 | ||||
|         # store the cancel scope and portal for later cancellation or | ||||
|         # retstart if needed. | ||||
|         self.service_ctxs[name] = (cs, ipc_ctx, portal, complete) | ||||
|         return ( | ||||
|             cs, | ||||
|             ipc_ctx, | ||||
|             started, | ||||
|         ) | ||||
| 
 | ||||
|     async def start_service( | ||||
|         self, | ||||
|         daemon_name: str, | ||||
|         ctx_ep: Callable,  # kwargs must `partial`-ed in! | ||||
|         # ^TODO, type for `@tractor.context` deco-ed funcs! | ||||
| 
 | ||||
|         debug_mode: bool = False, | ||||
|         **start_actor_kwargs, | ||||
| 
 | ||||
|     ) -> Context: | ||||
|         ''' | ||||
|         Start new subactor and schedule a supervising "service task" | ||||
|         in it which explicitly defines the sub's lifetime. | ||||
| 
 | ||||
|         "Service daemon subactors" are cancelled (and thus | ||||
|         terminated) using the paired `.cancel_service()`. | ||||
| 
 | ||||
|         Effectively this API can be used to manage "service daemons" | ||||
|         spawned under a single parent actor with supervision | ||||
|         semantics equivalent to a one-cancels-one style actor-nursery | ||||
|         or "(subactor) task manager" where each subprocess's (and | ||||
|         thus its embedded actor runtime) lifetime is synced to that | ||||
|         of the remotely spawned task defined by `ctx_ep`. | ||||
| 
 | ||||
|         The funcionality can be likened to a "daemonized" version of | ||||
|         `.hilevel.worker.run_in_actor()` but with supervision | ||||
|         controls offered by `tractor.Context` where the main/root | ||||
|         remotely scheduled `trio.Task` invoking `ctx_ep` determines | ||||
|         the underlying subactor's lifetime. | ||||
| 
 | ||||
|         ''' | ||||
|         entry: tuple|None = self.service_ctxs.get(daemon_name) | ||||
|         if entry: | ||||
|             (cs, sub_ctx, portal, complete) = entry | ||||
|             return sub_ctx | ||||
| 
 | ||||
|         if daemon_name not in self.service_ctxs: | ||||
|             portal: Portal = await self.an.start_actor( | ||||
|                 daemon_name, | ||||
|                 debug_mode=(  # maybe set globally during allocate | ||||
|                     debug_mode | ||||
|                     or | ||||
|                     self.debug_mode | ||||
|                 ), | ||||
|                 **start_actor_kwargs, | ||||
|             ) | ||||
|             ctx_kwargs: dict[str, Any] = {} | ||||
|             if isinstance(ctx_ep, functools.partial): | ||||
|                 ctx_kwargs: dict[str, Any] = ctx_ep.keywords | ||||
|                 ctx_ep: Callable = ctx_ep.func | ||||
| 
 | ||||
|             ( | ||||
|                 cs, | ||||
|                 sub_ctx, | ||||
|                 started, | ||||
|             ) = await self.start_service_ctx( | ||||
|                 name=daemon_name, | ||||
|                 portal=portal, | ||||
|                 ctx_fn=ctx_ep, | ||||
|                 **ctx_kwargs, | ||||
|             ) | ||||
| 
 | ||||
|             return sub_ctx | ||||
| 
 | ||||
|     async def cancel_service( | ||||
|         self, | ||||
|         name: str, | ||||
| 
 | ||||
|     ) -> Any: | ||||
|         ''' | ||||
|         Cancel the service task and actor for the given ``name``. | ||||
| 
 | ||||
|         ''' | ||||
|         log.info(f'Cancelling `pikerd` service {name}') | ||||
|         cs, sub_ctx, portal, complete = self.service_ctxs[name] | ||||
| 
 | ||||
|         # cs.cancel() | ||||
|         await sub_ctx.cancel() | ||||
|         await complete.wait() | ||||
| 
 | ||||
|         if name in self.service_ctxs: | ||||
|             # TODO: custom err? | ||||
|             # raise ServiceError( | ||||
|             raise RuntimeError( | ||||
|                 f'Service actor for {name} not terminated and/or unknown?' | ||||
|             ) | ||||
| 
 | ||||
|         # assert name not in self.service_ctxs, \ | ||||
|         #     f'Serice task for {name} not terminated?' | ||||
|  | @ -258,20 +258,28 @@ class ActorContextInfo(Mapping): | |||
| 
 | ||||
| 
 | ||||
| def get_logger( | ||||
| 
 | ||||
|     name: str | None = None, | ||||
|     name: str|None = None, | ||||
|     _root_name: str = _proj_name, | ||||
| 
 | ||||
|     logger: Logger|None = None, | ||||
| 
 | ||||
|     # TODO, using `.config.dictConfig()` api? | ||||
|     # -[ ] SO answer with docs links | ||||
|     #  |_https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig | ||||
|     #  |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema | ||||
|     subsys_spec: str|None = None, | ||||
| 
 | ||||
| ) -> StackLevelAdapter: | ||||
|     '''Return the package log or a sub-logger for ``name`` if provided. | ||||
| 
 | ||||
|     ''' | ||||
|     log: Logger | ||||
|     log = rlog = logging.getLogger(_root_name) | ||||
|     log = rlog = logger or logging.getLogger(_root_name) | ||||
| 
 | ||||
|     if ( | ||||
|         name | ||||
|         and name != _proj_name | ||||
|         and | ||||
|         name != _proj_name | ||||
|     ): | ||||
| 
 | ||||
|         # NOTE: for handling for modules that use ``get_logger(__name__)`` | ||||
|  | @ -283,7 +291,7 @@ def get_logger( | |||
|         #   since in python the {filename} is always this same | ||||
|         #   module-file. | ||||
| 
 | ||||
|         sub_name: None | str = None | ||||
|         sub_name: None|str = None | ||||
|         rname, _, sub_name = name.partition('.') | ||||
|         pkgpath, _, modfilename = sub_name.rpartition('.') | ||||
| 
 | ||||
|  | @ -306,7 +314,10 @@ def get_logger( | |||
| 
 | ||||
|     # add our actor-task aware adapter which will dynamically look up | ||||
|     # the actor and task names at each log emit | ||||
|     logger = StackLevelAdapter(log, ActorContextInfo()) | ||||
|     logger = StackLevelAdapter( | ||||
|         log, | ||||
|         ActorContextInfo(), | ||||
|     ) | ||||
| 
 | ||||
|     # additional levels | ||||
|     for name, val in CUSTOM_LEVELS.items(): | ||||
|  | @ -319,15 +330,25 @@ def get_logger( | |||
| 
 | ||||
| 
 | ||||
| def get_console_log( | ||||
|     level: str | None = None, | ||||
|     level: str|None = None, | ||||
|     logger: Logger|None = None, | ||||
|     **kwargs, | ||||
| ) -> LoggerAdapter: | ||||
|     '''Get the package logger and enable a handler which writes to stderr. | ||||
| 
 | ||||
|     Yeah yeah, i know we can use ``DictConfig``. You do it. | ||||
| ) -> LoggerAdapter: | ||||
|     ''' | ||||
|     log = get_logger(**kwargs)  # our root logger | ||||
|     logger = log.logger | ||||
|     Get a `tractor`-style logging instance: a `Logger` wrapped in | ||||
|     a `StackLevelAdapter` which injects various concurrency-primitive | ||||
|     (process, thread, task) fields and enables a `StreamHandler` that | ||||
|     writes on stderr using `colorlog` formatting. | ||||
| 
 | ||||
|     Yeah yeah, i know we can use `logging.config.dictConfig()`. You do it. | ||||
| 
 | ||||
|     ''' | ||||
|     log = get_logger( | ||||
|         logger=logger, | ||||
|         **kwargs | ||||
|     )  # set a root logger | ||||
|     logger: Logger = log.logger | ||||
| 
 | ||||
|     if not level: | ||||
|         return log | ||||
|  | @ -346,9 +367,13 @@ def get_console_log( | |||
|             None, | ||||
|         ) | ||||
|     ): | ||||
|         fmt = LOG_FORMAT | ||||
|         # if logger: | ||||
|         #     fmt = None | ||||
| 
 | ||||
|         handler = StreamHandler() | ||||
|         formatter = colorlog.ColoredFormatter( | ||||
|             LOG_FORMAT, | ||||
|             fmt=fmt, | ||||
|             datefmt=DATE_FORMAT, | ||||
|             log_colors=STD_PALETTE, | ||||
|             secondary_log_colors=BOLD_PALETTE, | ||||
|  | @ -365,7 +390,7 @@ def get_loglevel() -> str: | |||
| 
 | ||||
| 
 | ||||
| # global module logger for tractor itself | ||||
| log = get_logger('tractor') | ||||
| log: StackLevelAdapter = get_logger('tractor') | ||||
| 
 | ||||
| 
 | ||||
| def at_least_level( | ||||
|  |  | |||
|  | @ -33,12 +33,19 @@ from typing import ( | |||
| ) | ||||
| 
 | ||||
| import tractor | ||||
| from tractor._exceptions import AsyncioCancelled | ||||
| from tractor._exceptions import ( | ||||
|     AsyncioCancelled, | ||||
|     is_multi_cancelled, | ||||
| ) | ||||
| from tractor._state import ( | ||||
|     debug_mode, | ||||
|     _runtime_vars, | ||||
| ) | ||||
| from tractor.devx import _debug | ||||
| from tractor.log import get_logger | ||||
| from tractor.log import ( | ||||
|     get_logger, | ||||
|     StackLevelAdapter, | ||||
| ) | ||||
| from tractor.trionics._broadcast import ( | ||||
|     broadcast_receiver, | ||||
|     BroadcastReceiver, | ||||
|  | @ -49,7 +56,7 @@ from outcome import ( | |||
|     Outcome, | ||||
| ) | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
| log: StackLevelAdapter = get_logger(__name__) | ||||
| 
 | ||||
| 
 | ||||
| __all__ = [ | ||||
|  | @ -69,9 +76,10 @@ class LinkedTaskChannel(trio.abc.Channel): | |||
|     _to_aio: asyncio.Queue | ||||
|     _from_aio: trio.MemoryReceiveChannel | ||||
|     _to_trio: trio.MemorySendChannel | ||||
| 
 | ||||
|     _trio_cs: trio.CancelScope | ||||
|     _aio_task_complete: trio.Event | ||||
| 
 | ||||
|     _trio_err: BaseException|None = None | ||||
|     _trio_exited: bool = False | ||||
| 
 | ||||
|     # set after ``asyncio.create_task()`` | ||||
|  | @ -83,28 +91,40 @@ class LinkedTaskChannel(trio.abc.Channel): | |||
|         await self._from_aio.aclose() | ||||
| 
 | ||||
|     async def receive(self) -> Any: | ||||
|         async with translate_aio_errors( | ||||
|             self, | ||||
| 
 | ||||
|             # XXX: obviously this will deadlock if an on-going stream is | ||||
|             # being procesed. | ||||
|             # wait_on_aio_task=False, | ||||
|         ): | ||||
|         ''' | ||||
|         Receive a value from the paired `asyncio.Task` with | ||||
|         exception/cancel handling to teardown both sides on any | ||||
|         unexpected error. | ||||
| 
 | ||||
|         ''' | ||||
|         try: | ||||
|             # TODO: do we need this to guarantee asyncio code get's | ||||
|             # cancelled in the case where the trio side somehow creates | ||||
|             # a state where the asyncio cycle-task isn't getting the | ||||
|             # cancel request sent by (in theory) the last checkpoint | ||||
|             # cycle on the trio side? | ||||
|             # await trio.lowlevel.checkpoint() | ||||
| 
 | ||||
|             return await self._from_aio.receive() | ||||
|         except BaseException as err: | ||||
|             async with translate_aio_errors( | ||||
|                 self, | ||||
| 
 | ||||
|                 # XXX: obviously this will deadlock if an on-going stream is | ||||
|                 # being procesed. | ||||
|                 # wait_on_aio_task=False, | ||||
|             ): | ||||
|                 raise err | ||||
| 
 | ||||
|     async def wait_asyncio_complete(self) -> None: | ||||
|         await self._aio_task_complete.wait() | ||||
| 
 | ||||
|     # def cancel_asyncio_task(self) -> None: | ||||
|     #     self._aio_task.cancel() | ||||
|     def cancel_asyncio_task( | ||||
|         self, | ||||
|         msg: str = '', | ||||
|     ) -> None: | ||||
|         self._aio_task.cancel( | ||||
|             msg=msg, | ||||
|         ) | ||||
| 
 | ||||
|     async def send(self, item: Any) -> None: | ||||
|         ''' | ||||
|  | @ -154,7 +174,6 @@ class LinkedTaskChannel(trio.abc.Channel): | |||
| 
 | ||||
| 
 | ||||
| def _run_asyncio_task( | ||||
| 
 | ||||
|     func: Callable, | ||||
|     *, | ||||
|     qsize: int = 1, | ||||
|  | @ -164,8 +183,9 @@ def _run_asyncio_task( | |||
| 
 | ||||
| ) -> LinkedTaskChannel: | ||||
|     ''' | ||||
|     Run an ``asyncio`` async function or generator in a task, return | ||||
|     or stream the result back to the caller `trio.lowleve.Task`. | ||||
|     Run an `asyncio`-compat async function or generator in a task, | ||||
|     return or stream the result back to the caller | ||||
|     `trio.lowleve.Task`. | ||||
| 
 | ||||
|     ''' | ||||
|     __tracebackhide__: bool = hide_tb | ||||
|  | @ -203,23 +223,23 @@ def _run_asyncio_task( | |||
|     aio_err: BaseException|None = None | ||||
| 
 | ||||
|     chan = LinkedTaskChannel( | ||||
|         aio_q,  # asyncio.Queue | ||||
|         from_aio,  # recv chan | ||||
|         to_trio,  # send chan | ||||
| 
 | ||||
|         cancel_scope, | ||||
|         aio_task_complete, | ||||
|         _to_aio=aio_q,  # asyncio.Queue | ||||
|         _from_aio=from_aio,  # recv chan | ||||
|         _to_trio=to_trio,  # send chan | ||||
|         _trio_cs=cancel_scope, | ||||
|         _aio_task_complete=aio_task_complete, | ||||
|     ) | ||||
| 
 | ||||
|     async def wait_on_coro_final_result( | ||||
| 
 | ||||
|         to_trio: trio.MemorySendChannel, | ||||
|         coro: Awaitable, | ||||
|         aio_task_complete: trio.Event, | ||||
| 
 | ||||
|     ) -> None: | ||||
|         ''' | ||||
|         Await ``coro`` and relay result back to ``trio``. | ||||
|         Await `coro` and relay result back to `trio`. | ||||
| 
 | ||||
|         This can only be run as an `asyncio.Task`! | ||||
| 
 | ||||
|         ''' | ||||
|         nonlocal aio_err | ||||
|  | @ -242,8 +262,10 @@ def _run_asyncio_task( | |||
| 
 | ||||
|         else: | ||||
|             if ( | ||||
|                 result != orig and | ||||
|                 aio_err is None and | ||||
|                 result != orig | ||||
|                 and | ||||
|                 aio_err is None | ||||
|                 and | ||||
| 
 | ||||
|                 # in the `open_channel_from()` case we don't | ||||
|                 # relay through the "return value". | ||||
|  | @ -259,12 +281,21 @@ def _run_asyncio_task( | |||
|                 # a ``trio.EndOfChannel`` to the trio (consumer) side. | ||||
|                 to_trio.close() | ||||
| 
 | ||||
|             # import pdbp; pdbp.set_trace() | ||||
|             aio_task_complete.set() | ||||
|             log.runtime(f'`asyncio` task: {task.get_name()} is complete') | ||||
|             # await asyncio.sleep(0.1) | ||||
|             log.info( | ||||
|                 f'`asyncio` task terminated\n' | ||||
|                 f'x)>\n' | ||||
|                 f'  |_{task}\n' | ||||
|             ) | ||||
| 
 | ||||
|     # start the asyncio task we submitted from trio | ||||
|     if not inspect.isawaitable(coro): | ||||
|         raise TypeError(f"No support for invoking {coro}") | ||||
|         raise TypeError( | ||||
|             f'Pass the async-fn NOT a coroutine\n' | ||||
|             f'{coro!r}' | ||||
|         ) | ||||
| 
 | ||||
|     task: asyncio.Task = asyncio.create_task( | ||||
|         wait_on_coro_final_result( | ||||
|  | @ -288,6 +319,10 @@ def _run_asyncio_task( | |||
|             raise_not_found=False, | ||||
|         )) | ||||
|     ): | ||||
|         log.info( | ||||
|             f'Bestowing `greenback` portal for `asyncio`-task\n' | ||||
|             f'{task}\n' | ||||
|         ) | ||||
|         greenback.bestow_portal(task) | ||||
| 
 | ||||
|     def cancel_trio(task: asyncio.Task) -> None: | ||||
|  | @ -303,13 +338,33 @@ def _run_asyncio_task( | |||
|         # task exceptions | ||||
|         try: | ||||
|             res: Any = task.result() | ||||
|         except BaseException as terr: | ||||
|             task_err: BaseException = terr | ||||
|             log.info( | ||||
|                 '`trio` received final result from {task}\n' | ||||
|                 f'|_{res}\n' | ||||
|             ) | ||||
|         except BaseException as _aio_err: | ||||
|             task_err: BaseException = _aio_err | ||||
| 
 | ||||
|             # read again AFTER the `asyncio` side errors in case | ||||
|             # it was cancelled due to an error from `trio` (or | ||||
|             # some other out of band exc). | ||||
|             aio_err: BaseException|None = chan._aio_err | ||||
| 
 | ||||
|             # always true right? | ||||
|             assert ( | ||||
|                 type(_aio_err) is type(aio_err) | ||||
|             ), ( | ||||
|                 f'`asyncio`-side task errors mismatch?!?\n\n' | ||||
|                 f'caught: {_aio_err}\n' | ||||
|                 f'chan._aio_err: {aio_err}\n' | ||||
|             ) | ||||
| 
 | ||||
|             msg: str = ( | ||||
|                 'Infected `asyncio` task {etype_str}\n' | ||||
|                 '`trio`-side reports that the `asyncio`-side ' | ||||
|                 '{etype_str}\n' | ||||
|                 # ^NOTE filled in below | ||||
|             ) | ||||
|             if isinstance(terr, CancelledError): | ||||
|             if isinstance(_aio_err, CancelledError): | ||||
|                 msg += ( | ||||
|                     f'c)>\n' | ||||
|                     f' |_{task}\n' | ||||
|  | @ -326,17 +381,15 @@ def _run_asyncio_task( | |||
|                     msg.format(etype_str='errored') | ||||
|                 ) | ||||
| 
 | ||||
|             assert type(terr) is type(aio_err), ( | ||||
|                 '`asyncio` task error mismatch?!?' | ||||
|             ) | ||||
| 
 | ||||
|         if aio_err is not None: | ||||
|             # import pdbp; pdbp.set_trace() | ||||
|             # XXX: uhh is this true? | ||||
|             # assert task_err, f'Asyncio task {task.get_name()} discrepancy!?' | ||||
| 
 | ||||
|             # NOTE: currently mem chan closure may act as a form | ||||
|             # of error relay (at least in the ``asyncio.CancelledError`` | ||||
|             # case) since we have no way to directly trigger a ``trio`` | ||||
|             # of error relay (at least in the `asyncio.CancelledError` | ||||
|             # case) since we have no way to directly trigger a `trio` | ||||
|             # task error without creating a nursery to throw one. | ||||
|             # We might want to change this in the future though. | ||||
|             from_aio.close() | ||||
|  | @ -347,7 +400,7 @@ def _run_asyncio_task( | |||
|                 # aio_err.with_traceback(aio_err.__traceback__) | ||||
| 
 | ||||
|             # TODO: show when cancellation originated | ||||
|             # from each side more pedantically? | ||||
|             # from each side more pedantically in log-msg? | ||||
|             # elif ( | ||||
|             #     type(aio_err) is CancelledError | ||||
|             #     and  # trio was the cause? | ||||
|  | @ -358,43 +411,57 @@ def _run_asyncio_task( | |||
|             #     ) | ||||
|             #     raise aio_err from task_err | ||||
| 
 | ||||
|             # XXX: if not already, alway cancel the scope | ||||
|             # on a task error in case the trio task is blocking on | ||||
|             # XXX: if not already, alway cancel the scope on a task | ||||
|             # error in case the trio task is blocking on | ||||
|             # a checkpoint. | ||||
|             cancel_scope.cancel() | ||||
| 
 | ||||
|             if ( | ||||
|                 task_err | ||||
|                 and | ||||
|                 aio_err is not task_err | ||||
|                 not cancel_scope.cancelled_caught | ||||
|                 or | ||||
|                 not cancel_scope.cancel_called | ||||
|             ): | ||||
|                 raise aio_err from task_err | ||||
|                 # import pdbp; pdbp.set_trace() | ||||
|                 cancel_scope.cancel() | ||||
| 
 | ||||
|             # raise any `asyncio` side error. | ||||
|             raise aio_err | ||||
| 
 | ||||
|         log.info( | ||||
|             '`trio` received final result from {task}\n' | ||||
|             f'|_{res}\n' | ||||
|         ) | ||||
|         # TODO: do we need this? | ||||
|         # if task_err: | ||||
|         #     cancel_scope.cancel() | ||||
|         #     raise task_err | ||||
|             if task_err: | ||||
|                 # XXX raise any `asyncio` side error IFF it doesn't | ||||
|                 # match the one we just caught from the task above! | ||||
|                 # (that would indicate something weird/very-wrong | ||||
|                 # going on?) | ||||
|                 if aio_err is not task_err: | ||||
|                     # import pdbp; pdbp.set_trace() | ||||
|                     raise aio_err from task_err | ||||
| 
 | ||||
|     task.add_done_callback(cancel_trio) | ||||
|     return chan | ||||
| 
 | ||||
| 
 | ||||
| class TrioTaskExited(AsyncioCancelled): | ||||
|     ''' | ||||
|     The `trio`-side task exited without explicitly cancelling the | ||||
|     `asyncio.Task` peer. | ||||
| 
 | ||||
|     This is very similar to how `trio.ClosedResource` acts as | ||||
|     a "clean shutdown" signal to the consumer side of a mem-chan, | ||||
| 
 | ||||
|     https://trio.readthedocs.io/en/stable/reference-core.html#clean-shutdown-with-channels | ||||
| 
 | ||||
|     ''' | ||||
| 
 | ||||
| 
 | ||||
| @acm | ||||
| async def translate_aio_errors( | ||||
| 
 | ||||
|     chan: LinkedTaskChannel, | ||||
|     wait_on_aio_task: bool = False, | ||||
|     cancel_aio_task_on_trio_exit: bool = True, | ||||
| 
 | ||||
| ) -> AsyncIterator[None]: | ||||
|     ''' | ||||
|     Error handling context around ``asyncio`` task spawns which | ||||
|     An error handling to cross-loop propagation context around | ||||
|     `asyncio.Task` spawns via one of this module's APIs: | ||||
| 
 | ||||
|     - `open_channel_from()` | ||||
|     - `run_task()` | ||||
| 
 | ||||
|     appropriately translates errors and cancels into ``trio`` land. | ||||
| 
 | ||||
|     ''' | ||||
|  | @ -402,88 +469,247 @@ async def translate_aio_errors( | |||
| 
 | ||||
|     aio_err: BaseException|None = None | ||||
| 
 | ||||
|     # TODO: make thisi a channel method? | ||||
|     def maybe_raise_aio_err( | ||||
|         err: Exception|None = None | ||||
|     ) -> None: | ||||
|         aio_err = chan._aio_err | ||||
|         if ( | ||||
|             aio_err is not None | ||||
|             and | ||||
|             # not isinstance(aio_err, CancelledError) | ||||
|             type(aio_err) != CancelledError | ||||
|         ): | ||||
|             # always raise from any captured asyncio error | ||||
|             if err: | ||||
|                 raise aio_err from err | ||||
|             else: | ||||
|                 raise aio_err | ||||
| 
 | ||||
|     task = chan._aio_task | ||||
|     assert task | ||||
|     aio_task: asyncio.Task = chan._aio_task | ||||
|     assert aio_task | ||||
|     trio_err: BaseException|None = None | ||||
|     try: | ||||
|         yield | ||||
|         yield  # back to one of the cross-loop apis | ||||
|     except trio.Cancelled as taskc: | ||||
|         trio_err = taskc | ||||
| 
 | ||||
|     except ( | ||||
|         trio.Cancelled, | ||||
|     ): | ||||
|         # relay cancel through to called ``asyncio`` task | ||||
|         # should NEVER be the case that `trio` is cancel-handling | ||||
|         # BEFORE the other side's task-ref was set!? | ||||
|         assert chan._aio_task | ||||
|         chan._aio_task.cancel( | ||||
|             msg=f'the `trio` caller task was cancelled: {trio_task.name}' | ||||
|         ) | ||||
|         raise | ||||
| 
 | ||||
|         # import pdbp; pdbp.set_trace()  # lolevel-debug | ||||
| 
 | ||||
|         # relay cancel through to called ``asyncio`` task | ||||
|         chan._aio_err = AsyncioCancelled( | ||||
|             f'trio`-side cancelled the `asyncio`-side,\n' | ||||
|             f'c)>\n' | ||||
|             f'  |_{trio_task}\n\n' | ||||
| 
 | ||||
| 
 | ||||
|             f'{trio_err!r}\n' | ||||
|         ) | ||||
| 
 | ||||
|         # XXX NOTE XXX seems like we can get all sorts of unreliable | ||||
|         # behaviour from `asyncio` under various cancellation | ||||
|         # conditions (like SIGINT/kbi) when this is used.. | ||||
|         # SO FOR NOW, try to avoid it at most costs! | ||||
|         # | ||||
|         # aio_task.cancel( | ||||
|         #     msg=f'the `trio` parent task was cancelled: {trio_task.name}' | ||||
|         # ) | ||||
|         # raise | ||||
| 
 | ||||
|     # NOTE ALSO SEE the matching note in the `cancel_trio()` asyncio | ||||
|     # task-done-callback. | ||||
|     except ( | ||||
|         # NOTE: see the note in the ``cancel_trio()`` asyncio task | ||||
|         # termination callback | ||||
|         trio.ClosedResourceError, | ||||
|         # trio.BrokenResourceError, | ||||
|     ): | ||||
|     ) as cre: | ||||
|         trio_err = cre | ||||
|         aio_err = chan._aio_err | ||||
|         # import pdbp; pdbp.set_trace() | ||||
| 
 | ||||
|         # XXX if an underlying `asyncio.CancelledError` triggered | ||||
|         # this channel close, raise our (non-`BaseException`) wrapper | ||||
|         # exception (`AsyncioCancelled`) from that source error. | ||||
|         if ( | ||||
|             task.cancelled() | ||||
|             # aio-side is cancelled? | ||||
|             aio_task.cancelled()  # not set until it terminates?? | ||||
|             and | ||||
|             type(aio_err) is CancelledError | ||||
| 
 | ||||
|             # TODO, if we want suppression of the | ||||
|             # silent-exit-by-`trio` case? | ||||
|             # -[ ] the parent task can also just catch it though? | ||||
|             # -[ ] OR, offer a `signal_aio_side_on_exit=True` ?? | ||||
|             # | ||||
|             # or | ||||
|             # aio_err is None | ||||
|             # and | ||||
|             # chan._trio_exited | ||||
| 
 | ||||
|         ): | ||||
|             # if an underlying `asyncio.CancelledError` triggered this | ||||
|             # channel close, raise our (non-``BaseException``) wrapper | ||||
|             # error: ``AsyncioCancelled`` from that source error. | ||||
|             raise AsyncioCancelled( | ||||
|                 f'Task cancelled\n' | ||||
|                 f'|_{task}\n' | ||||
|                 f'asyncio`-side cancelled the `trio`-side,\n' | ||||
|                 f'c(>\n' | ||||
|                 f'  |_{aio_task}\n\n' | ||||
| 
 | ||||
|                 f'{trio_err!r}\n' | ||||
|             ) from aio_err | ||||
| 
 | ||||
|         # maybe the chan-closure is due to something else? | ||||
|         else: | ||||
|             raise | ||||
| 
 | ||||
|     finally: | ||||
|     except BaseException as _trio_err: | ||||
|         trio_err = _trio_err | ||||
|         log.exception( | ||||
|             '`trio`-side task errored?' | ||||
|         ) | ||||
| 
 | ||||
|         entered: bool = await _debug._maybe_enter_pm( | ||||
|             trio_err, | ||||
|             api_frame=inspect.currentframe(), | ||||
|         ) | ||||
|         if ( | ||||
|             # NOTE: always cancel the ``asyncio`` task if we've made it | ||||
|             # this far and it's not done. | ||||
|             not task.done() and aio_err | ||||
|             not entered | ||||
|             and | ||||
|             not is_multi_cancelled(trio_err) | ||||
|         ): | ||||
|             log.exception('actor crashed\n') | ||||
| 
 | ||||
|         aio_taskc = AsyncioCancelled( | ||||
|             f'`trio`-side task errored!\n' | ||||
|             f'{trio_err}' | ||||
|         ) #from trio_err | ||||
| 
 | ||||
|         try: | ||||
|             aio_task.set_exception(aio_taskc) | ||||
|         except ( | ||||
|             asyncio.InvalidStateError, | ||||
|             RuntimeError, | ||||
|             # ^XXX, uhh bc apparently we can't use `.set_exception()` | ||||
|             # any more XD .. ?? | ||||
|         ): | ||||
|             wait_on_aio_task = False | ||||
| 
 | ||||
|         # import pdbp; pdbp.set_trace() | ||||
|         # raise aio_taskc from trio_err | ||||
| 
 | ||||
|     finally: | ||||
|         # record wtv `trio`-side error transpired | ||||
|         chan._trio_err = trio_err | ||||
|         ya_trio_exited: bool = chan._trio_exited | ||||
| 
 | ||||
|         # NOTE! by default always cancel the `asyncio` task if | ||||
|         # we've made it this far and it's not done. | ||||
|         # TODO, how to detect if there's an out-of-band error that | ||||
|         # caused the exit? | ||||
|         if ( | ||||
|             cancel_aio_task_on_trio_exit | ||||
|             and | ||||
|             not aio_task.done() | ||||
|             and | ||||
|             aio_err | ||||
| 
 | ||||
|             # or the trio side has exited it's surrounding cancel scope | ||||
|             # indicating the lifetime of the ``asyncio``-side task | ||||
|             # should also be terminated. | ||||
|             or chan._trio_exited | ||||
|         ): | ||||
|             log.runtime( | ||||
|                 f'Cancelling `asyncio`-task: {task.get_name()}' | ||||
|             or ( | ||||
|                 ya_trio_exited | ||||
|                 and | ||||
|                 not chan._trio_err   # XXX CRITICAL, `asyncio.Task.cancel()` is cucked man.. | ||||
|             ) | ||||
|             # assert not aio_err, 'WTF how did asyncio do this?!' | ||||
|             task.cancel() | ||||
|         ): | ||||
|             report: str = ( | ||||
|                 'trio-side exited silently!' | ||||
|             ) | ||||
|             assert not aio_err, 'WTF how did asyncio do this?!' | ||||
| 
 | ||||
|         # Required to sync with the far end ``asyncio``-task to ensure | ||||
|             # if the `trio.Task` already exited the `open_channel_from()` | ||||
|             # block we ensure the asyncio-side gets signalled via an | ||||
|             # explicit exception and its `Queue` is shutdown. | ||||
|             if ya_trio_exited: | ||||
|                 chan._to_aio.shutdown() | ||||
| 
 | ||||
|                 # pump the other side's task? needed? | ||||
|                 await trio.lowlevel.checkpoint() | ||||
| 
 | ||||
|                 if ( | ||||
|                     not chan._trio_err | ||||
|                     and | ||||
|                     (fut := aio_task._fut_waiter) | ||||
|                 ): | ||||
|                     fut.set_exception( | ||||
|                         TrioTaskExited( | ||||
|                             f'The peer `asyncio` task is still blocking/running?\n' | ||||
|                             f'>>\n' | ||||
|                             f'|_{aio_task!r}\n' | ||||
|                         ) | ||||
|                     ) | ||||
|                 else: | ||||
|                     # from tractor._state import is_root_process | ||||
|                     # if is_root_process(): | ||||
|                     #     breakpoint() | ||||
|                     #     import pdbp; pdbp.set_trace() | ||||
| 
 | ||||
|                     aio_taskc_warn: str = ( | ||||
|                         f'\n' | ||||
|                         f'MANUALLY Cancelling `asyncio`-task: {aio_task.get_name()}!\n\n' | ||||
|                         f'**THIS CAN SILENTLY SUPPRESS ERRORS FYI\n\n' | ||||
|                     ) | ||||
|                     report += aio_taskc_warn | ||||
|                     # TODO XXX, figure out the case where calling this makes the | ||||
|                     # `test_infected_asyncio.py::test_trio_closes_early_and_channel_exits` | ||||
|                     # hang and then don't call it in that case! | ||||
|                     # | ||||
|                     aio_task.cancel(msg=aio_taskc_warn) | ||||
| 
 | ||||
|             log.warning(report) | ||||
| 
 | ||||
|         # Required to sync with the far end `asyncio`-task to ensure | ||||
|         # any error is captured (via monkeypatching the | ||||
|         # ``channel._aio_err``) before calling ``maybe_raise_aio_err()`` | ||||
|         # `channel._aio_err`) before calling ``maybe_raise_aio_err()`` | ||||
|         # below! | ||||
|         # | ||||
|         # XXX NOTE XXX the `task.set_exception(aio_taskc)` call above | ||||
|         # MUST NOT EXCEPT or this WILL HANG!! | ||||
|         # | ||||
|         # so if you get a hang maybe step through and figure out why | ||||
|         # it erroed out up there! | ||||
|         # | ||||
|         if wait_on_aio_task: | ||||
|             # await chan.wait_asyncio_complete() | ||||
|             await chan._aio_task_complete.wait() | ||||
|             log.info( | ||||
|                 'asyncio-task is done and unblocked trio-side!\n' | ||||
|             ) | ||||
| 
 | ||||
|         # TODO? | ||||
|         # -[ ] make this a channel method, OR | ||||
|         # -[ ] just put back inline below? | ||||
|         # | ||||
|         def maybe_raise_aio_side_err( | ||||
|             trio_err: Exception, | ||||
|         ) -> None: | ||||
|             ''' | ||||
|             Raise any `trio`-side-caused cancellation or legit task | ||||
|             error normally propagated from the caller of either, | ||||
|               - `open_channel_from()` | ||||
|               - `run_task()` | ||||
| 
 | ||||
|             ''' | ||||
|             aio_err: BaseException|None = chan._aio_err | ||||
| 
 | ||||
|             # Check if the asyncio-side is the cause of the trio-side | ||||
|             # error. | ||||
|             if ( | ||||
|                 aio_err is not None | ||||
|                 and | ||||
|                 type(aio_err) is not AsyncioCancelled | ||||
| 
 | ||||
|                 # not isinstance(aio_err, CancelledError) | ||||
|                 # type(aio_err) is not CancelledError | ||||
|             ): | ||||
|                 # always raise from any captured asyncio error | ||||
|                 if trio_err: | ||||
|                     raise trio_err from aio_err | ||||
| 
 | ||||
|                 raise aio_err | ||||
| 
 | ||||
|             if trio_err: | ||||
|                 raise trio_err | ||||
| 
 | ||||
|         # NOTE: if any ``asyncio`` error was caught, raise it here inline | ||||
|         # here in the ``trio`` task | ||||
|         maybe_raise_aio_err() | ||||
|         # if trio_err: | ||||
|         maybe_raise_aio_side_err( | ||||
|             trio_err=trio_err | ||||
|         ) | ||||
| 
 | ||||
| 
 | ||||
| async def run_task( | ||||
|  | @ -495,8 +721,8 @@ async def run_task( | |||
| 
 | ||||
| ) -> Any: | ||||
|     ''' | ||||
|     Run an `asyncio` async function or generator in a task, return | ||||
|     or stream the result back to `trio`. | ||||
|     Run an `asyncio`-compat async function or generator in a task, | ||||
|     return or stream the result back to `trio`. | ||||
| 
 | ||||
|     ''' | ||||
|     # simple async func | ||||
|  | @ -536,6 +762,7 @@ async def open_channel_from( | |||
|         provide_channels=True, | ||||
|         **kwargs, | ||||
|     ) | ||||
|     # TODO, tuple form here? | ||||
|     async with chan._from_aio: | ||||
|         async with translate_aio_errors( | ||||
|             chan, | ||||
|  | @ -684,18 +911,21 @@ def run_as_asyncio_guest( | |||
|     # Uh, oh. | ||||
|     # | ||||
|     # :o | ||||
| 
 | ||||
|     # It looks like your event loop has caught a case of the ``trio``s. | ||||
| 
 | ||||
|     # :() | ||||
| 
 | ||||
|     # Don't worry, we've heard you'll barely notice. You might | ||||
|     # hallucinate a few more propagating errors and feel like your | ||||
|     # digestion has slowed but if anything get's too bad your parents | ||||
|     # will know about it. | ||||
| 
 | ||||
|     # | ||||
|     # looks like your stdlib event loop has caught a case of "the trios" ! | ||||
|     # | ||||
|     # :O | ||||
|     # | ||||
|     # Don't worry, we've heard you'll barely notice. | ||||
|     # | ||||
|     # :) | ||||
| 
 | ||||
|     # | ||||
|     # You might hallucinate a few more propagating errors and feel | ||||
|     # like your digestion has slowed, but if anything get's too bad | ||||
|     # your parents will know about it. | ||||
|     # | ||||
|     # B) | ||||
|     # | ||||
|     async def aio_main(trio_main): | ||||
|         ''' | ||||
|         Main `asyncio.Task` which calls | ||||
|  | @ -712,16 +942,20 @@ def run_as_asyncio_guest( | |||
|             '-> built a `trio`-done future\n' | ||||
|         ) | ||||
| 
 | ||||
|         # TODO: shoudn't this be done in the guest-run trio task? | ||||
|         # if debug_mode(): | ||||
|         #     # XXX make it obvi we know this isn't supported yet! | ||||
|         #     log.error( | ||||
|         #         'Attempting to enter unsupported `greenback` init ' | ||||
|         #         'from `asyncio` task..' | ||||
|         #     ) | ||||
|         #     await _debug.maybe_init_greenback( | ||||
|         #         force_reload=True, | ||||
|         #     ) | ||||
|         # TODO: is this evern run or needed? | ||||
|         # -[ ] pretty sure it never gets run for root-infected-aio | ||||
|         #     since this main task is always the parent of any | ||||
|         #     eventual `open_root_actor()` call? | ||||
|         if debug_mode(): | ||||
|             log.error( | ||||
|                 'Attempting to enter non-required `greenback` init ' | ||||
|                 'from `asyncio` task ???' | ||||
|             ) | ||||
|             # XXX make it obvi we know this isn't supported yet! | ||||
|             assert 0 | ||||
|             # await _debug.maybe_init_greenback( | ||||
|             #     force_reload=True, | ||||
|             # ) | ||||
| 
 | ||||
|         def trio_done_callback(main_outcome): | ||||
|             log.runtime( | ||||
|  | @ -731,6 +965,7 @@ def run_as_asyncio_guest( | |||
|             ) | ||||
| 
 | ||||
|             if isinstance(main_outcome, Error): | ||||
|                 # import pdbp; pdbp.set_trace() | ||||
|                 error: BaseException = main_outcome.error | ||||
| 
 | ||||
|                 # show an dedicated `asyncio`-side tb from the error | ||||
|  | @ -750,7 +985,7 @@ def run_as_asyncio_guest( | |||
|                 trio_done_fute.set_result(main_outcome) | ||||
| 
 | ||||
|             log.info( | ||||
|                 f'`trio` guest-run finished with outcome\n' | ||||
|                 f'`trio` guest-run finished with,\n' | ||||
|                 f')>\n' | ||||
|                 f'|_{trio_done_fute}\n' | ||||
|             ) | ||||
|  | @ -767,6 +1002,9 @@ def run_as_asyncio_guest( | |||
|             'Infecting `asyncio`-process with a `trio` guest-run!\n' | ||||
|         ) | ||||
| 
 | ||||
|         # TODO, somehow bootstrap this! | ||||
|         _runtime_vars['_is_infected_aio'] = True | ||||
| 
 | ||||
|         trio.lowlevel.start_guest_run( | ||||
|             trio_main, | ||||
|             run_sync_soon_threadsafe=loop.call_soon_threadsafe, | ||||
|  | @ -775,6 +1013,18 @@ def run_as_asyncio_guest( | |||
|         fute_err: BaseException|None = None | ||||
|         try: | ||||
|             out: Outcome = await asyncio.shield(trio_done_fute) | ||||
|             # ^TODO still don't really understand why the `.shield()` | ||||
|             # is required ... ?? | ||||
|             # https://docs.python.org/3/library/asyncio-task.html#asyncio.shield | ||||
|             # ^ seems as though in combo with the try/except here | ||||
|             # we're BOLDLY INGORING cancel of the trio fute? | ||||
|             # | ||||
|             # I guess it makes sense bc we don't want `asyncio` to | ||||
|             # cancel trio just because they can't handle SIGINT | ||||
|             # sanely? XD .. kk | ||||
| 
 | ||||
|             # XXX, sin-shield causes guest-run abandons on SIGINT.. | ||||
|             # out: Outcome = await trio_done_fute | ||||
| 
 | ||||
|             # NOTE will raise (via `Error.unwrap()`) from any | ||||
|             # exception packed into the guest-run's `main_outcome`. | ||||
|  | @ -797,27 +1047,32 @@ def run_as_asyncio_guest( | |||
|             fute_err = _fute_err | ||||
|             err_message: str = ( | ||||
|                 'main `asyncio` task ' | ||||
|                 'was cancelled!\n' | ||||
|             ) | ||||
|             if isinstance(fute_err, asyncio.CancelledError): | ||||
|                 err_message += 'was cancelled!\n' | ||||
|             else: | ||||
|                 err_message += f'errored with {out.error!r}\n' | ||||
| 
 | ||||
|             # TODO, handle possible edge cases with | ||||
|             # `open_root_actor()` closing before this is run! | ||||
|             # | ||||
|             actor: tractor.Actor = tractor.current_actor() | ||||
| 
 | ||||
|             log.exception( | ||||
|                 err_message | ||||
|                 + | ||||
|                 'Cancelling `trio`-side `tractor`-runtime..\n' | ||||
|                 f'c)>\n' | ||||
|                 f'c(>\n' | ||||
|                 f'  |_{actor}.cancel_soon()\n' | ||||
|             ) | ||||
| 
 | ||||
|             # XXX WARNING XXX the next LOCs are super important, since | ||||
|             # without them, we can get guest-run abandonment cases | ||||
|             # where `asyncio` will not schedule or wait on the `trio` | ||||
|             # guest-run task before final shutdown! This is | ||||
|             # particularly true if the `trio` side has tasks doing | ||||
|             # shielded work when a SIGINT condition occurs. | ||||
|             # XXX WARNING XXX the next LOCs are super important! | ||||
|             # | ||||
|             # SINCE without them, we can get guest-run ABANDONMENT | ||||
|             # cases where `asyncio` will not schedule or wait on the | ||||
|             # guest-run `trio.Task` nor invoke its registered | ||||
|             # `trio_done_callback()` before final shutdown! | ||||
|             # | ||||
|             # This is particularly true if the `trio` side has tasks | ||||
|             # in shielded sections when an OC-cancel (SIGINT) | ||||
|             # condition occurs! | ||||
|             # | ||||
|             # We now have the | ||||
|             # `test_infected_asyncio.test_sigint_closes_lifetime_stack()` | ||||
|  | @ -881,7 +1136,12 @@ def run_as_asyncio_guest( | |||
| 
 | ||||
|             try: | ||||
|                 return trio_done_fute.result() | ||||
|             except asyncio.exceptions.InvalidStateError as state_err: | ||||
|             except ( | ||||
|                 asyncio.InvalidStateError, | ||||
|                 # asyncio.CancelledError, | ||||
|                 # ^^XXX `.shield()` call above prevents this?? | ||||
| 
 | ||||
|             )as state_err: | ||||
| 
 | ||||
|                 # XXX be super dupere noisy about abandonment issues! | ||||
|                 aio_task: asyncio.Task = asyncio.current_task() | ||||
|  |  | |||
|  | @ -0,0 +1,533 @@ | |||
| version = 1 | ||||
| revision = 1 | ||||
| requires-python = ">=3.11" | ||||
| 
 | ||||
| [[package]] | ||||
| name = "async-generator" | ||||
| version = "1.10" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "attrs" | ||||
| version = "24.3.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "cffi" | ||||
| version = "1.17.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "pycparser" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "colorama" | ||||
| version = "0.4.6" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "colorlog" | ||||
| version = "6.9.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "colorama", marker = "sys_platform == 'win32'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "greenback" | ||||
| version = "1.2.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "greenlet" }, | ||||
|     { name = "outcome" }, | ||||
|     { name = "sniffio" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/ab3a42c0f3ed56df9cd33de1539b3198d98c6ccbaf88a73d6be0b72d85e0/greenback-1.2.1.tar.gz", hash = "sha256:de3ca656885c03b96dab36079f3de74bb5ba061da9bfe3bb69dccc866ef95ea3", size = 42597 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/71/d0/b8dc79d5ecfffacad9c844b6ae76b9c6259935796d3c561deccbf8fa421d/greenback-1.2.1-py3-none-any.whl", hash = "sha256:98768edbbe4340091a9730cf64a683fcbaa3f2cb81e4ac41d7ed28d3b6f74b79", size = 28062 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "greenlet" | ||||
| version = "3.1.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "idna" | ||||
| version = "3.10" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "importlib-metadata" | ||||
| version = "8.6.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "zipp" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "iniconfig" | ||||
| version = "2.0.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "msgspec" | ||||
| version = "0.19.0" | ||||
| source = { git = "https://github.com/jcrist/msgspec.git#dd965dce22e5278d4935bea923441ecde31b5325" } | ||||
| 
 | ||||
| [[package]] | ||||
| name = "mypy-extensions" | ||||
| version = "1.0.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "outcome" | ||||
| version = "1.3.0.post0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "attrs" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "packaging" | ||||
| version = "24.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pdbp" | ||||
| version = "1.6.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "colorama", marker = "sys_platform == 'win32'" }, | ||||
|     { name = "pygments" }, | ||||
|     { name = "tabcompleter" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/69/13/80da03638f62facbee76312ca9ee5941c017b080f2e4c6919fd4e87e16e3/pdbp-1.6.1.tar.gz", hash = "sha256:f4041642952a05df89664e166d5bd379607a0866ddd753c06874f65552bdf40b", size = 25322 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/29/93/d56fb9ba5569dc29d8263c72e46d21a2fd38741339ebf03f54cf7561828c/pdbp-1.6.1-py3-none-any.whl", hash = "sha256:f10bad2ee044c0e5c168cb0825abfdbdc01c50013e9755df5261b060bdd35c22", size = 21495 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pexpect" | ||||
| version = "4.9.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "ptyprocess" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pluggy" | ||||
| version = "1.5.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "prompt-toolkit" | ||||
| version = "3.0.50" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "wcwidth" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "ptyprocess" | ||||
| version = "0.7.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pycparser" | ||||
| version = "2.22" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pygments" | ||||
| version = "2.19.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pyperclip" | ||||
| version = "1.9.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961 } | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pyreadline3" | ||||
| version = "3.5.4" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "pytest" | ||||
| version = "8.3.4" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "colorama", marker = "sys_platform == 'win32'" }, | ||||
|     { name = "iniconfig" }, | ||||
|     { 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 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "sniffio" | ||||
| version = "1.3.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "sortedcontainers" | ||||
| version = "2.4.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "stackscope" | ||||
| version = "0.2.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/4a/fc/20dbb993353f31230138f3c63f3f0c881d1853e70d7a30cd68d2ba4cf1e2/stackscope-0.2.2.tar.gz", hash = "sha256:f508c93eb4861ada466dd3ff613ca203962ceb7587ad013759f15394e6a4e619", size = 90479 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/f1/5f/0a674fcafa03528089badb46419413f342537b5b57d2fefc9900fb8ee4e4/stackscope-0.2.2-py3-none-any.whl", hash = "sha256:c199b0cda738d39c993ee04eb01961b06b7e9aeb43ebf9fd6226cdd72ea9faf6", size = 80807 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "tabcompleter" | ||||
| version = "1.4.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "pyreadline3", marker = "sys_platform == 'win32'" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/73/1a/ed3544579628c5709bae6fae2255e94c6982a9ff77d42d8ba59fd2f3b21a/tabcompleter-1.4.0.tar.gz", hash = "sha256:7562a9938e62f8e7c3be612c3ac4e14c5ec4307b58ba9031c148260e866e8814", size = 10431 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/65/44/bb509c3d2c0b5a87e7a5af1d5917a402a32ff026f777a6d7cb6990746cbb/tabcompleter-1.4.0-py3-none-any.whl", hash = "sha256:d744aa735b49c0a6cc2fb8fcd40077fec47425e4388301010b14e6ce3311368b", size = 6725 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "tractor" | ||||
| version = "0.1.0a6.dev0" | ||||
| source = { editable = "." } | ||||
| dependencies = [ | ||||
|     { name = "colorlog" }, | ||||
|     { name = "msgspec" }, | ||||
|     { name = "pdbp" }, | ||||
|     { name = "tricycle" }, | ||||
|     { name = "trio" }, | ||||
|     { name = "trio-typing" }, | ||||
|     { name = "wrapt" }, | ||||
| ] | ||||
| 
 | ||||
| [package.dev-dependencies] | ||||
| dev = [ | ||||
|     { name = "greenback" }, | ||||
|     { name = "pexpect" }, | ||||
|     { name = "prompt-toolkit" }, | ||||
|     { name = "pyperclip" }, | ||||
|     { 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 = "tricycle", specifier = ">=0.4.1,<0.5" }, | ||||
|     { name = "trio", specifier = ">=0.24,<0.25" }, | ||||
|     { name = "trio-typing", specifier = ">=0.10.0,<0.11" }, | ||||
|     { name = "wrapt", specifier = ">=1.16.0,<2" }, | ||||
| ] | ||||
| 
 | ||||
| [package.metadata.requires-dev] | ||||
| dev = [ | ||||
|     { name = "greenback", specifier = ">=1.2.1,<2" }, | ||||
|     { name = "pexpect", specifier = ">=4.9.0,<5" }, | ||||
|     { name = "prompt-toolkit", specifier = ">=3.0.43,<4" }, | ||||
|     { name = "pyperclip", specifier = ">=1.9.0" }, | ||||
|     { name = "pytest", specifier = ">=8.2.0,<9" }, | ||||
|     { 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" }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "tricycle" | ||||
| version = "0.4.1" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "trio" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/f8/8e/fdd7bc467b40eedd0a5f2ed36b0d692c6e6f2473be00c8160e2e9f53adc1/tricycle-0.4.1.tar.gz", hash = "sha256:f56edb4b3e1bed3e2552b1b499b24a2dab47741e92e9b4d806acc5c35c9e6066", size = 41551 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/d7/c6/7cc05d60e21c683df99167db071ce5d848f5063c2a63971a8443466f603e/tricycle-0.4.1-py3-none-any.whl", hash = "sha256:67900995a73e7445e2c70250cdca04a778d9c3923dd960a97ad4569085e0fb3f", size = 35316 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "trio" | ||||
| version = "0.24.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "attrs" }, | ||||
|     { name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" }, | ||||
|     { name = "idna" }, | ||||
|     { name = "outcome" }, | ||||
|     { 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 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/14/fb/9299cf74953f473a15accfdbe2c15218e766bae8c796f2567c83bae03e98/trio-0.24.0-py3-none-any.whl", hash = "sha256:c3bd3a4e3e3025cd9a2241eae75637c43fe0b9e88b4c97b9161a55b9e54cd72c", size = 460205 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "trio-typing" | ||||
| version = "0.10.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| dependencies = [ | ||||
|     { name = "async-generator" }, | ||||
|     { name = "importlib-metadata" }, | ||||
|     { name = "mypy-extensions" }, | ||||
|     { name = "packaging" }, | ||||
|     { name = "trio" }, | ||||
|     { name = "typing-extensions" }, | ||||
| ] | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/b5/74/a87aafa40ec3a37089148b859892cbe2eef08d132c816d58a60459be5337/trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", size = 38747 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/89/ff/9bd795273eb14fac7f6a59d16cc8c4d0948a619a1193d375437c7f50f3eb/trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264", size = 42224 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "typing-extensions" | ||||
| version = "4.12.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wcwidth" | ||||
| version = "0.2.13" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "wrapt" | ||||
| version = "1.17.2" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 }, | ||||
|     { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "xonsh" | ||||
| version = "0.19.1" | ||||
| 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 } | ||||
| 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 }, | ||||
| ] | ||||
| 
 | ||||
| [[package]] | ||||
| name = "zipp" | ||||
| version = "3.21.0" | ||||
| source = { registry = "https://pypi.org/simple" } | ||||
| sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } | ||||
| wheels = [ | ||||
|     { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, | ||||
| ] | ||||
		Loading…
	
		Reference in New Issue