Compare commits
	
		
			250 Commits 
		
	
	
		
			05a02d97b4
			...
			23809b8468
		
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | 23809b8468 | |
|  | 60427329ee | |
|  | f946041d44 | |
|  | a4339d6ac6 | |
|  | 9123fbdbfa | |
|  | e7b3254b7b | |
|  | e468f62c26 | |
|  | 6c65729c20 | |
|  | 94fbbe0b05 | |
|  | d5b54f3f5e | |
|  | fd314deecb | |
|  | dd011c0b2f | |
|  | 087aaa1c36 | |
|  | 66b7410eab | |
|  | c9a55c2d46 | |
|  | 548855b4f5 | |
|  | 5322861d6d | |
|  | 46a2fa7074 | |
|  | bfe5b2dde6 | |
|  | a9f06df3fb | |
|  | ee32bc433c | |
|  | 561954594e | |
|  | 28a6354e81 | |
|  | d1599449e7 | |
|  | 2d27c94dec | |
|  | 6e4c76245b | |
|  | a6f599901c | |
|  | 0fafd25f0d | |
|  | b74e93ee55 | |
|  | 961504b657 | |
|  | bd148300c5 | |
|  | 4a7491bda4 | |
|  | 62415518fc | |
|  | 5c7d930a9a | |
|  | c46986504d | |
|  | e05a4d3cac | |
|  | a9aa5ec04e | |
|  | 5021514a6a | |
|  | 79f502034f | |
|  | 331921f612 | |
|  | df0d00abf4 | |
|  | a72d1e6c48 | |
|  | 5931c59aef | |
|  | ba08052ddf | |
|  | 00112edd58 | |
|  | 1d706bddda | |
|  | 3c30c559d5 | |
|  | 599020c2c5 | |
|  | 50f6543ee7 | |
|  | c0854fd221 | |
|  | e875b62869 | |
|  | 3ab7498893 | |
|  | dd041b0a01 | |
|  | 4e252526b5 | |
|  | 4ba3590450 | |
|  | f1ff79a4e6 | |
|  | 70664b98de | |
|  | 1c425cbd22 | |
|  | edc2211444 | |
|  | b05abea51e | |
|  | 88c1c083bd | |
|  | b096867d40 | |
|  | a3c9822602 | |
|  | e3a542f2b5 | |
|  | 0ffcea1033 | |
|  | a7bdf0486c | |
|  | d2ac9ecf95 | |
|  | dcb1062bb8 | |
|  | 05d865c0f1 | |
|  | 8218f0f51f | |
|  | 8f19f5d3a8 | |
|  | 64c27a914b | |
|  | d9c8d543b3 | |
|  | 048b154f00 | |
|  | 88828e9f99 | |
|  | 25ff195c17 | |
|  | f60cc646ff | |
|  | a2b754b5f5 | |
|  | 5e13588aed | |
|  | 0a56f40bab | |
|  | f776c47cb4 | |
|  | 7f584d4f54 | |
|  | d650dda0fa | |
|  | f6598e8400 | |
|  | 59822ff093 | |
|  | ca427aec7e | |
|  | f53aa992af | |
|  | 69e0afccf0 | |
|  | e275c49b23 | |
|  | 48fbf38c1d | |
|  | defd6e28d2 | |
|  | 414b0e2bae | |
|  | d34fb54f7c | |
|  | 5d87f63377 | |
|  | 0ca3d50602 | |
|  | 8880a80e3e | |
|  | 7be713ee1e | |
|  | 4bd8211abb | |
|  | a23a98886c | |
|  | 31544c862c | |
|  | 7d320c4e1e | |
|  | 38944ad1d2 | |
|  | 9260909fe1 | |
|  | c00b3c86ea | |
|  | 808a336508 | |
|  | 679d999185 | |
|  | a8428d7de3 | |
|  | e9f2fecd66 | |
|  | 547cf5a210 | |
|  | b5e3fa7370 | |
|  | cd16748598 | |
|  | 1af35f8170 | |
|  | 4569d11052 | |
|  | 6ba76ab700 | |
|  | 734dda35e9 | |
|  | b7e04525cc | |
|  | 35977dcebb | |
|  | e1f26f9611 | |
|  | 63c5b7696a | |
|  | 5f94f52226 | |
|  | 6bf571a124 | |
|  | f5056cdd02 | |
|  | 9ff448faa3 | |
|  | 760b9890c4 | |
|  | d000642462 | |
|  | dd69948744 | |
|  | 5b69975f81 | |
|  | 6b474743f9 | |
|  | 5ac229244a | |
|  | bbd2ea3e4f | |
|  | 6b903f7746 | |
|  | 2280bad135 | |
|  | 8d506796ec | |
|  | 02d03ce700 | |
|  | 9786e2c404 | |
|  | 116137d066 | |
|  | 7f87b4e717 | |
|  | bb17d39c4e | |
|  | fba6edfe9a | |
|  | e4758550f7 | |
|  | a7efbfdbc2 | |
|  | 1c6660c497 | |
|  | 202befa360 | |
|  | c24708b273 | |
|  | 3aee702733 | |
|  | a573c3c9a8 | |
|  | 6a352fee87 | |
|  | 6cb361352c | |
|  | 7807ffaabe | |
|  | 65b795612c | |
|  | a42c1761a8 | |
|  | 359d732633 | |
|  | b09e35f3dc | |
|  | 6618b004f4 | |
|  | fc57a4d639 | |
|  | 2248ffb74f | |
|  | 1eb0d785a8 | |
|  | 98d0ca88e5 | |
|  | 37f843a128 | |
|  | 29cd2ddbac | |
|  | 295b06511b | |
|  | 1e6b5b3f0a | |
|  | 36ddb85197 | |
|  | d6b0ddecd7 | |
|  | 9e5475391c | |
|  | ef7ed7ac6f | |
|  | d8094f4420 | |
|  | d7b12735a8 | |
|  | 47107e44ed | |
|  | ba384ca83d | |
|  | ad9833a73a | |
|  | 161884fbf1 | |
|  | c2e7dc7407 | |
|  | 309360daa2 | |
|  | cbfb0d0144 | |
|  | c0eef3bac3 | |
|  | 27e6ad18ee | |
|  | 28e32b8f85 | |
|  | 05df634d62 | |
|  | 6d2f4d108d | |
|  | ae2687b381 | |
|  | a331f6dab3 | |
|  | 9c0de24899 | |
|  | 1f3cef5ed6 | |
|  | 8538a9c591 | |
|  | 7533e93b0f | |
|  | f67b0639b8 | |
|  | 26fedec6a1 | |
|  | 0711576678 | |
|  | 0477a62ac3 | |
|  | 01d6f111f6 | |
|  | 56ef4cba23 | |
|  | 52b5efd78d | |
|  | a7d4bcdfb9 | |
|  | 79d0c17f6b | |
|  | 98c4614a36 | |
|  | 61df10b333 | |
|  | 094447787e | |
|  | ba45c03e14 | |
|  | 00d8a2a099 | |
|  | bedde076d9 | |
|  | be1d8bf6fa | |
|  | d9aee98db2 | |
|  | 708ce4a051 | |
|  | d6d0112d95 | |
|  | 0fcbedd2be | |
|  | 412c66d000 | |
|  | 3cc835c215 | |
|  | f15bbb30cc | |
|  | ad211f8c2c | |
|  | acac605c37 | |
|  | 078e507774 | |
|  | 81bf810fbb | |
|  | 7d1512e03a | |
|  | 1c85338ff8 | |
|  | 7a3c9d0458 | |
|  | 31196b9cb4 | |
|  | 44c9da1c91 | |
|  | b4ce618e33 | |
|  | a504d92536 | |
|  | 8c0d9614bc | |
|  | a6fefcc2a8 | |
|  | abdaf7bf1f | |
|  | 7b3324b240 | |
|  | bbae2c91fd | |
|  | 2540d1f9e0 | |
|  | 63fac5a809 | |
|  | 568fb18d01 | |
|  | f67e19a852 | |
|  | 0be9f5f907 | |
|  | 5e2d456029 | |
|  | c7d5b021db | |
|  | 6f1f198fb1 | |
|  | 26fef82d33 | |
|  | 84d25b5727 | |
|  | 1ed0c861b5 | |
|  | 2dd3a682c8 | |
|  | 881813e61e | |
|  | 566a11c00d | |
|  | af69272d16 | |
|  | 8e3f581d3f | |
|  | eceb292415 | |
|  | 9921ea3cae | |
|  | 414a8c5b75 | |
|  | eeb0516017 | |
|  | d6eeddef4e | |
|  | d478dbfcfe | |
|  | ef6094a650 | |
|  | 4e8404bb09 | |
|  | bbb3484ae9 | 
|  | @ -8,46 +8,70 @@ on: | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
| 
 | 
 | ||||||
| jobs: | jobs: | ||||||
| 
 |   # ------ sdist ------ | ||||||
|   mypy: |  | ||||||
|     name: 'MyPy' |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
| 
 |  | ||||||
|     steps: |  | ||||||
|       - name: Checkout |  | ||||||
|         uses: actions/checkout@v2 |  | ||||||
| 
 |  | ||||||
|       - name: Setup python |  | ||||||
|         uses: actions/setup-python@v2 |  | ||||||
|         with: |  | ||||||
|           python-version: '3.11' |  | ||||||
| 
 |  | ||||||
|       - name: Install dependencies |  | ||||||
|         run: pip install -U . --upgrade-strategy eager -r requirements-test.txt |  | ||||||
| 
 |  | ||||||
|       - name: Run MyPy check |  | ||||||
|         run: mypy tractor/ --ignore-missing-imports --show-traceback |  | ||||||
| 
 |  | ||||||
|   # test that we can generate a software distribution and install it |   # test that we can generate a software distribution and install it | ||||||
|   # thus avoid missing file issues after packaging. |   # thus avoid missing file issues after packaging. | ||||||
|  |   # | ||||||
|  |   # -[x] produce sdist with uv | ||||||
|  |   # ------ - ------ | ||||||
|   sdist-linux: |   sdist-linux: | ||||||
|     name: 'sdist' |     name: 'sdist' | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
|       - name: Checkout |       - name: Checkout | ||||||
|         uses: actions/checkout@v2 |         uses: actions/checkout@v4 | ||||||
| 
 | 
 | ||||||
|       - name: Setup python |       - name: Install latest uv | ||||||
|         uses: actions/setup-python@v2 |         uses: astral-sh/setup-uv@v6 | ||||||
|         with: |  | ||||||
|           python-version: '3.11' |  | ||||||
| 
 | 
 | ||||||
|       - name: Build sdist |       - name: Build sdist as tar.gz | ||||||
|         run: python setup.py sdist --formats=zip |         run: uv build --sdist --python=3.13 | ||||||
| 
 | 
 | ||||||
|       - name: Install sdist from .zips |       - name: Install sdist from .tar.gz | ||||||
|         run: python -m pip install dist/*.zip |         run: python -m pip install dist/*.tar.gz | ||||||
|  | 
 | ||||||
|  |   # ------ type-check ------ | ||||||
|  |   # mypy: | ||||||
|  |   #   name: 'MyPy' | ||||||
|  |   #   runs-on: ubuntu-latest | ||||||
|  | 
 | ||||||
|  |   #   steps: | ||||||
|  |   #     - name: Checkout | ||||||
|  |   #       uses: actions/checkout@v4 | ||||||
|  | 
 | ||||||
|  |   #     - name: Install latest uv | ||||||
|  |   #       uses: astral-sh/setup-uv@v6 | ||||||
|  | 
 | ||||||
|  |   #     # faster due to server caching? | ||||||
|  |   #     # https://docs.astral.sh/uv/guides/integration/github/#setting-up-python | ||||||
|  |   #     - name: "Set up Python" | ||||||
|  |   #       uses: actions/setup-python@v6 | ||||||
|  |   #       with: | ||||||
|  |   #         python-version-file: "pyproject.toml" | ||||||
|  | 
 | ||||||
|  |   #     # w uv | ||||||
|  |   #     # - name: Set up Python | ||||||
|  |   #     #   run: uv python install | ||||||
|  | 
 | ||||||
|  |   #     - name: Setup uv venv | ||||||
|  |   #       run: uv venv .venv --python=3.13 | ||||||
|  | 
 | ||||||
|  |   #     - name: Install | ||||||
|  |   #       run: uv sync --dev | ||||||
|  | 
 | ||||||
|  |   #     # TODO, ty cmd over repo | ||||||
|  |   #     # - name: type check with ty | ||||||
|  |   #     #   run: ty ./tractor/ | ||||||
|  | 
 | ||||||
|  |   #     # - uses: actions/cache@v3 | ||||||
|  |   #     #     name: Cache uv virtenv as default .venv | ||||||
|  |   #     #     with: | ||||||
|  |   #     #       path: ./.venv | ||||||
|  |   #     #       key: venv-${{ hashFiles('uv.lock') }} | ||||||
|  | 
 | ||||||
|  |   #     - name: Run MyPy check | ||||||
|  |   #       run: mypy tractor/ --ignore-missing-imports --show-traceback | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|   testing-linux: |   testing-linux: | ||||||
|  | @ -59,32 +83,45 @@ jobs: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         os: [ubuntu-latest] |         os: [ubuntu-latest] | ||||||
|         python: ['3.11'] |         python-version: ['3.13'] | ||||||
|         spawn_backend: [ |         spawn_backend: [ | ||||||
|           'trio', |           'trio', | ||||||
|           'mp_spawn', |           # 'mp_spawn', | ||||||
|           'mp_forkserver', |           # 'mp_forkserver', | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|     steps: |     steps: | ||||||
| 
 | 
 | ||||||
|       - name: Checkout |       - uses: actions/checkout@v4 | ||||||
|         uses: actions/checkout@v2 |  | ||||||
| 
 | 
 | ||||||
|       - name: Setup python |       - name: 'Install uv + py-${{ matrix.python-version }}' | ||||||
|         uses: actions/setup-python@v2 |         uses: astral-sh/setup-uv@v6 | ||||||
|         with: |         with: | ||||||
|           python-version: '${{ matrix.python }}' |           python-version: ${{ matrix.python-version }} | ||||||
| 
 | 
 | ||||||
|       - name: Install dependencies |       # GH way.. faster? | ||||||
|         run: pip install -U . -r requirements-test.txt -r requirements-docs.txt --upgrade-strategy eager |       # - name: setup-python@v6 | ||||||
|  |       #   uses: actions/setup-python@v6 | ||||||
|  |       #   with: | ||||||
|  |       #     python-version: '${{ matrix.python-version }}' | ||||||
| 
 | 
 | ||||||
|       - name: List dependencies |       # consider caching for speedups? | ||||||
|         run: pip list |       # https://docs.astral.sh/uv/guides/integration/github/#caching | ||||||
|  | 
 | ||||||
|  |       - name: Install the project w uv | ||||||
|  |         run: uv sync --all-extras --dev | ||||||
|  | 
 | ||||||
|  |       # - name: Install dependencies | ||||||
|  |       #   run: pip install -U . -r requirements-test.txt -r requirements-docs.txt --upgrade-strategy eager | ||||||
|  | 
 | ||||||
|  |       - name: List deps tree | ||||||
|  |         run: uv tree | ||||||
| 
 | 
 | ||||||
|       - name: Run tests |       - name: Run tests | ||||||
|         run: pytest tests/ --spawn-backend=${{ matrix.spawn_backend }} -rsx |         run: uv run pytest tests/ --spawn-backend=${{ matrix.spawn_backend }} -rsx | ||||||
| 
 | 
 | ||||||
|  |   # XXX legacy NOTE XXX | ||||||
|  |   # | ||||||
|   # We skip 3.10 on windows for now due to not having any collabs to |   # We skip 3.10 on windows for now due to not having any collabs to | ||||||
|   # debug the CI failures. Anyone wanting to hack and solve them is very |   # debug the CI failures. Anyone wanting to hack and solve them is very | ||||||
|   # welcome, but our primary user base is not using that OS. |   # welcome, but our primary user base is not using that OS. | ||||||
|  |  | ||||||
|  | @ -1,8 +1,5 @@ | ||||||
| |logo| ``tractor``: distributed structurred concurrency | |logo| ``tractor``: distributed structurred concurrency | ||||||
| 
 | 
 | ||||||
| |gh_actions| |  | ||||||
| |docs| |  | ||||||
| 
 |  | ||||||
| ``tractor`` is a `structured concurrency`_ (SC), multi-processing_ runtime built on trio_. | ``tractor`` is a `structured concurrency`_ (SC), multi-processing_ runtime built on trio_. | ||||||
| 
 | 
 | ||||||
| Fundamentally, ``tractor`` provides parallelism via | Fundamentally, ``tractor`` provides parallelism via | ||||||
|  | @ -66,6 +63,13 @@ Features | ||||||
|   - (WIP) a ``TaskMngr``: one-cancels-one style nursery supervisor. |   - (WIP) a ``TaskMngr``: one-cancels-one style nursery supervisor. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | Status of `main` / infra | ||||||
|  | ------------------------ | ||||||
|  | 
 | ||||||
|  | - |gh_actions| | ||||||
|  | - |docs| | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| Install | Install | ||||||
| ------- | ------- | ||||||
| ``tractor`` is still in a *alpha-near-beta-stage* for many | ``tractor`` is still in a *alpha-near-beta-stage* for many | ||||||
|  | @ -689,9 +693,11 @@ channel`_! | ||||||
| .. _msgspec: https://jcristharif.com/msgspec/ | .. _msgspec: https://jcristharif.com/msgspec/ | ||||||
| .. _guest: https://trio.readthedocs.io/en/stable/reference-lowlevel.html?highlight=guest%20mode#using-guest-mode-to-run-trio-on-top-of-other-event-loops | .. _guest: https://trio.readthedocs.io/en/stable/reference-lowlevel.html?highlight=guest%20mode#using-guest-mode-to-run-trio-on-top-of-other-event-loops | ||||||
| 
 | 
 | ||||||
| 
 | .. | ||||||
| .. |gh_actions| image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fgoodboy%2Ftractor%2Fbadge&style=popout-square |    NOTE, on generating badge links from the UI | ||||||
|     :target: https://actions-badge.atrox.dev/goodboy/tractor/goto |    https://docs.github.com/en/actions/how-tos/monitoring-and-troubleshooting-workflows/monitoring-workflows/adding-a-workflow-status-badge?ref=gitguardian-blog-automated-secrets-detection#using-the-ui | ||||||
|  | .. |gh_actions| image:: https://github.com/goodboy/tractor/actions/workflows/ci.yml/badge.svg?branch=main | ||||||
|  |     :target: https://github.com/goodboy/tractor/actions/workflows/ci.yml | ||||||
| 
 | 
 | ||||||
| .. |docs| image:: https://readthedocs.org/projects/tractor/badge/?version=latest | .. |docs| image:: https://readthedocs.org/projects/tractor/badge/?version=latest | ||||||
|     :target: https://tractor.readthedocs.io/en/latest/?badge=latest |     :target: https://tractor.readthedocs.io/en/latest/?badge=latest | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ from tractor import ( | ||||||
|     ContextCancelled, |     ContextCancelled, | ||||||
|     MsgStream, |     MsgStream, | ||||||
|     _testing, |     _testing, | ||||||
|  |     trionics, | ||||||
| ) | ) | ||||||
| import trio | import trio | ||||||
| import pytest | import pytest | ||||||
|  | @ -62,9 +63,8 @@ async def recv_and_spawn_net_killers( | ||||||
|     await ctx.started() |     await ctx.started() | ||||||
|     async with ( |     async with ( | ||||||
|         ctx.open_stream() as stream, |         ctx.open_stream() as stream, | ||||||
|         trio.open_nursery( |         trionics.collapse_eg(), | ||||||
|             strict_exception_groups=False, |         trio.open_nursery() as tn, | ||||||
|         ) as tn, |  | ||||||
|     ): |     ): | ||||||
|         async for i in stream: |         async for i in stream: | ||||||
|             print(f'child echoing {i}') |             print(f'child echoing {i}') | ||||||
|  |  | ||||||
|  | @ -29,7 +29,7 @@ async def bp_then_error( | ||||||
|     to_trio.send_nowait('start') |     to_trio.send_nowait('start') | ||||||
| 
 | 
 | ||||||
|     # NOTE: what happens here inside the hook needs some refinement.. |     # NOTE: what happens here inside the hook needs some refinement.. | ||||||
|     # => seems like it's still `._debug._set_trace()` but |     # => seems like it's still `.debug._set_trace()` but | ||||||
|     #    we set `Lock.local_task_in_debug = 'sync'`, we probably want |     #    we set `Lock.local_task_in_debug = 'sync'`, we probably want | ||||||
|     #    some further, at least, meta-data about the task/actor in debug |     #    some further, at least, meta-data about the task/actor in debug | ||||||
|     #    in terms of making it clear it's `asyncio` mucking about. |     #    in terms of making it clear it's `asyncio` mucking about. | ||||||
|  |  | ||||||
|  | @ -4,6 +4,11 @@ import sys | ||||||
| import trio | import trio | ||||||
| import tractor | import tractor | ||||||
| 
 | 
 | ||||||
|  | # ensure mod-path is correct! | ||||||
|  | from tractor.devx.debug import ( | ||||||
|  |     _sync_pause_from_builtin as _sync_pause_from_builtin, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| async def main() -> None: | async def main() -> None: | ||||||
| 
 | 
 | ||||||
|  | @ -13,19 +18,23 @@ async def main() -> None: | ||||||
| 
 | 
 | ||||||
|     async with tractor.open_nursery( |     async with tractor.open_nursery( | ||||||
|         debug_mode=True, |         debug_mode=True, | ||||||
|     ) as an: |         loglevel='devx', | ||||||
|         assert an |         maybe_enable_greenback=True, | ||||||
|  |         # ^XXX REQUIRED to enable `breakpoint()` support (from sync | ||||||
|  |         # fns) and thus required here to avoid an assertion err | ||||||
|  |         # on the next line | ||||||
|  |     ): | ||||||
|         assert ( |         assert ( | ||||||
|             (pybp_var := os.environ['PYTHONBREAKPOINT']) |             (pybp_var := os.environ['PYTHONBREAKPOINT']) | ||||||
|             == |             == | ||||||
|             'tractor.devx._debug._sync_pause_from_builtin' |             'tractor.devx.debug._sync_pause_from_builtin' | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         # TODO: an assert that verifies the hook has indeed been, hooked |         # TODO: an assert that verifies the hook has indeed been, hooked | ||||||
|         # XD |         # XD | ||||||
|         assert ( |         assert ( | ||||||
|             (pybp_hook := sys.breakpointhook) |             (pybp_hook := sys.breakpointhook) | ||||||
|             is not tractor.devx._debug._set_trace |             is not tractor.devx.debug._set_trace | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         print( |         print( | ||||||
|  |  | ||||||
|  | @ -24,10 +24,9 @@ async def spawn_until(depth=0): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def main(): | async def main(): | ||||||
|     """The main ``tractor`` routine. |     ''' | ||||||
| 
 |     The process tree should look as approximately as follows when the | ||||||
|     The process tree should look as approximately as follows when the debugger |     debugger first engages: | ||||||
|     first engages: |  | ||||||
| 
 | 
 | ||||||
|     python examples/debugging/multi_nested_subactors_bp_forever.py |     python examples/debugging/multi_nested_subactors_bp_forever.py | ||||||
|     ├─ python -m tractor._child --uid ('spawner1', '7eab8462 ...) |     ├─ python -m tractor._child --uid ('spawner1', '7eab8462 ...) | ||||||
|  | @ -37,10 +36,11 @@ async def main(): | ||||||
|     └─ python -m tractor._child --uid ('spawner0', '1d42012b ...) |     └─ python -m tractor._child --uid ('spawner0', '1d42012b ...) | ||||||
|        └─ python -m tractor._child --uid ('name_error', '6c2733b8 ...) |        └─ python -m tractor._child --uid ('name_error', '6c2733b8 ...) | ||||||
| 
 | 
 | ||||||
|     """ |     ''' | ||||||
|     async with tractor.open_nursery( |     async with tractor.open_nursery( | ||||||
|         debug_mode=True, |         debug_mode=True, | ||||||
|         loglevel='warning' |         loglevel='devx', | ||||||
|  |         enable_transports=['uds'], | ||||||
|     ) as n: |     ) as n: | ||||||
| 
 | 
 | ||||||
|         # spawn both actors |         # spawn both actors | ||||||
|  |  | ||||||
|  | @ -0,0 +1,35 @@ | ||||||
|  | import trio | ||||||
|  | import tractor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def main(): | ||||||
|  |     async with tractor.open_root_actor( | ||||||
|  |         debug_mode=True, | ||||||
|  |         loglevel='cancel', | ||||||
|  |     ) as _root: | ||||||
|  | 
 | ||||||
|  |         # manually trigger self-cancellation and wait | ||||||
|  |         # for it to fully trigger. | ||||||
|  |         _root.cancel_soon() | ||||||
|  |         await _root._cancel_complete.wait() | ||||||
|  |         print('root cancelled') | ||||||
|  | 
 | ||||||
|  |         # now ensure we can still use the REPL | ||||||
|  |         try: | ||||||
|  |             await tractor.pause() | ||||||
|  |         except trio.Cancelled as _taskc: | ||||||
|  |             assert (root_cs := _root._root_tn.cancel_scope).cancel_called | ||||||
|  |             # NOTE^^ above logic but inside `open_root_actor()` and | ||||||
|  |             # passed to the `shield=` expression is effectively what | ||||||
|  |             # we're testing here! | ||||||
|  |             await tractor.pause(shield=root_cs.cancel_called) | ||||||
|  | 
 | ||||||
|  |         # XXX, if shield logic *is wrong* inside `open_root_actor()`'s | ||||||
|  |         # crash-handler block this should never be interacted, | ||||||
|  |         # instead `trio.Cancelled` would be bubbled up: the original | ||||||
|  |         # BUG. | ||||||
|  |         assert 0 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     trio.run(main) | ||||||
|  | @ -37,6 +37,7 @@ async def main( | ||||||
|             enable_stack_on_sig=True, |             enable_stack_on_sig=True, | ||||||
|             # maybe_enable_greenback=False, |             # maybe_enable_greenback=False, | ||||||
|             loglevel='devx', |             loglevel='devx', | ||||||
|  |             enable_transports=['uds'], | ||||||
|         ) as an, |         ) as an, | ||||||
|     ): |     ): | ||||||
|         ptl: tractor.Portal  = await an.start_actor( |         ptl: tractor.Portal  = await an.start_actor( | ||||||
|  |  | ||||||
|  | @ -33,8 +33,11 @@ async def just_bp( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def main(): | async def main(): | ||||||
|  | 
 | ||||||
|     async with tractor.open_nursery( |     async with tractor.open_nursery( | ||||||
|         debug_mode=True, |         debug_mode=True, | ||||||
|  |         enable_transports=['uds'], | ||||||
|  |         loglevel='devx', | ||||||
|     ) as n: |     ) as n: | ||||||
|         p = await n.start_actor( |         p = await n.start_actor( | ||||||
|             'bp_boi', |             'bp_boi', | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ import tractor | ||||||
| 
 | 
 | ||||||
| # TODO: only import these when not running from test harness? | # TODO: only import these when not running from test harness? | ||||||
| # can we detect `pexpect` usage maybe? | # can we detect `pexpect` usage maybe? | ||||||
| # from tractor.devx._debug import ( | # from tractor.devx.debug import ( | ||||||
| #     get_lock, | #     get_lock, | ||||||
| #     get_debug_req, | #     get_debug_req, | ||||||
| # ) | # ) | ||||||
|  |  | ||||||
|  | @ -23,9 +23,8 @@ async def main(): | ||||||
|             modules=[__name__] |             modules=[__name__] | ||||||
|         ) as portal_map, |         ) as portal_map, | ||||||
| 
 | 
 | ||||||
|         trio.open_nursery( |         tractor.trionics.collapse_eg(), | ||||||
|             strict_exception_groups=False, |         trio.open_nursery() as tn, | ||||||
|         ) as tn, |  | ||||||
|     ): |     ): | ||||||
| 
 | 
 | ||||||
|         for (name, portal) in portal_map.items(): |         for (name, portal) in portal_map.items(): | ||||||
|  |  | ||||||
|  | @ -61,6 +61,9 @@ dev = [ | ||||||
|   # `tractor.devx` tooling |   # `tractor.devx` tooling | ||||||
|   "greenback>=1.2.1,<2", |   "greenback>=1.2.1,<2", | ||||||
|   "stackscope>=0.2.2,<0.3", |   "stackscope>=0.2.2,<0.3", | ||||||
|  |   # ^ requires this? | ||||||
|  |   "typing-extensions>=4.14.1", | ||||||
|  | 
 | ||||||
|   "pyperclip>=1.9.0", |   "pyperclip>=1.9.0", | ||||||
|   "prompt-toolkit>=3.0.50", |   "prompt-toolkit>=3.0.50", | ||||||
|   "xonsh>=0.19.2", |   "xonsh>=0.19.2", | ||||||
|  |  | ||||||
|  | @ -103,7 +103,7 @@ def sig_prog( | ||||||
| def daemon( | def daemon( | ||||||
|     debug_mode: bool, |     debug_mode: bool, | ||||||
|     loglevel: str, |     loglevel: str, | ||||||
|     testdir, |     testdir: pytest.Pytester, | ||||||
|     reg_addr: tuple[str, int], |     reg_addr: tuple[str, int], | ||||||
|     tpt_proto: str, |     tpt_proto: str, | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -2,9 +2,11 @@ | ||||||
| `tractor.devx.*` tooling sub-pkg test space. | `tractor.devx.*` tooling sub-pkg test space. | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
|  | from __future__ import annotations | ||||||
| import time | import time | ||||||
| from typing import ( | from typing import ( | ||||||
|     Callable, |     Callable, | ||||||
|  |     TYPE_CHECKING, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
|  | @ -16,7 +18,7 @@ from pexpect.spawnbase import SpawnBase | ||||||
| from tractor._testing import ( | from tractor._testing import ( | ||||||
|     mk_cmd, |     mk_cmd, | ||||||
| ) | ) | ||||||
| from tractor.devx._debug import ( | from tractor.devx.debug import ( | ||||||
|     _pause_msg as _pause_msg, |     _pause_msg as _pause_msg, | ||||||
|     _crash_msg as _crash_msg, |     _crash_msg as _crash_msg, | ||||||
|     _repl_fail_msg as _repl_fail_msg, |     _repl_fail_msg as _repl_fail_msg, | ||||||
|  | @ -26,14 +28,22 @@ from ..conftest import ( | ||||||
|     _ci_env, |     _ci_env, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from pexpect import pty_spawn | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # a fn that sub-instantiates a `pexpect.spawn()` | ||||||
|  | # and returns it. | ||||||
|  | type PexpectSpawner = Callable[[str], pty_spawn.spawn] | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture | @pytest.fixture | ||||||
| def spawn( | def spawn( | ||||||
|     start_method, |     start_method: str, | ||||||
|     testdir: pytest.Pytester, |     testdir: pytest.Pytester, | ||||||
|     reg_addr: tuple[str, int], |     reg_addr: tuple[str, int], | ||||||
| 
 | 
 | ||||||
| ) -> Callable[[str], None]: | ) -> PexpectSpawner: | ||||||
|     ''' |     ''' | ||||||
|     Use the `pexpect` module shipped via `testdir.spawn()` to |     Use the `pexpect` module shipped via `testdir.spawn()` to | ||||||
|     run an `./examples/..` script by name. |     run an `./examples/..` script by name. | ||||||
|  | @ -59,7 +69,7 @@ def spawn( | ||||||
|     def _spawn( |     def _spawn( | ||||||
|         cmd: str, |         cmd: str, | ||||||
|         **mkcmd_kwargs, |         **mkcmd_kwargs, | ||||||
|     ): |     ) -> pty_spawn.spawn: | ||||||
|         unset_colors() |         unset_colors() | ||||||
|         return testdir.spawn( |         return testdir.spawn( | ||||||
|             cmd=mk_cmd( |             cmd=mk_cmd( | ||||||
|  | @ -73,7 +83,7 @@ def spawn( | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     # such that test-dep can pass input script name. |     # such that test-dep can pass input script name. | ||||||
|     return _spawn |     return _spawn  # the `PexpectSpawner`, type alias. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture( | @pytest.fixture( | ||||||
|  | @ -111,7 +121,7 @@ def ctlc( | ||||||
|         # XXX: disable pygments highlighting for auto-tests |         # XXX: disable pygments highlighting for auto-tests | ||||||
|         # since some envs (like actions CI) will struggle |         # since some envs (like actions CI) will struggle | ||||||
|         # the the added color-char encoding.. |         # the the added color-char encoding.. | ||||||
|         from tractor.devx._debug import TractorConfig |         from tractor.devx.debug import TractorConfig | ||||||
|         TractorConfig.use_pygements = False |         TractorConfig.use_pygements = False | ||||||
| 
 | 
 | ||||||
|     yield use_ctlc |     yield use_ctlc | ||||||
|  |  | ||||||
|  | @ -1,19 +1,23 @@ | ||||||
| """ | """ | ||||||
| That "native" debug mode better work! | That "native" debug mode better work! | ||||||
| 
 | 
 | ||||||
| All these tests can be understood (somewhat) by running the equivalent | All these tests can be understood (somewhat) by running the | ||||||
| `examples/debugging/` scripts manually. | equivalent `examples/debugging/` scripts manually. | ||||||
| 
 | 
 | ||||||
| TODO: | TODO: | ||||||
|     - none of these tests have been run successfully on windows yet but |   - none of these tests have been run successfully on windows yet but | ||||||
|       there's been manual testing that verified it works. |     there's been manual testing that verified it works. | ||||||
|     - wonder if any of it'll work on OS X? |   - wonder if any of it'll work on OS X? | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
|  | from __future__ import annotations | ||||||
| from functools import partial | from functools import partial | ||||||
| import itertools | import itertools | ||||||
| import platform | import platform | ||||||
| import time | import time | ||||||
|  | from typing import ( | ||||||
|  |     TYPE_CHECKING, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
| from pexpect.exceptions import ( | from pexpect.exceptions import ( | ||||||
|  | @ -34,6 +38,9 @@ from .conftest import ( | ||||||
|     assert_before, |     assert_before, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from ..conftest import PexpectSpawner | ||||||
|  | 
 | ||||||
| # TODO: The next great debugger audit could be done by you! | # TODO: The next great debugger audit could be done by you! | ||||||
| # - recurrent entry to breakpoint() from single actor *after* and an | # - recurrent entry to breakpoint() from single actor *after* and an | ||||||
| #   error in another task? | #   error in another task? | ||||||
|  | @ -310,7 +317,6 @@ def test_subactor_breakpoint( | ||||||
| 
 | 
 | ||||||
|     assert in_prompt_msg( |     assert in_prompt_msg( | ||||||
|         child, [ |         child, [ | ||||||
|         'MessagingError:', |  | ||||||
|         'RemoteActorError:', |         'RemoteActorError:', | ||||||
|          "('breakpoint_forever'", |          "('breakpoint_forever'", | ||||||
|          'bdb.BdbQuit', |          'bdb.BdbQuit', | ||||||
|  | @ -528,7 +534,7 @@ def test_multi_daemon_subactors( | ||||||
|     # now the root actor won't clobber the bp_forever child |     # now the root actor won't clobber the bp_forever child | ||||||
|     # during it's first access to the debug lock, but will instead |     # during it's first access to the debug lock, but will instead | ||||||
|     # wait for the lock to release, by the edge triggered |     # wait for the lock to release, by the edge triggered | ||||||
|     # ``devx._debug.Lock.no_remote_has_tty`` event before sending cancel messages |     # ``devx.debug.Lock.no_remote_has_tty`` event before sending cancel messages | ||||||
|     # (via portals) to its underlings B) |     # (via portals) to its underlings B) | ||||||
| 
 | 
 | ||||||
|     # at some point here there should have been some warning msg from |     # at some point here there should have been some warning msg from | ||||||
|  | @ -919,6 +925,7 @@ def test_post_mortem_api( | ||||||
|             "<Task 'name_error'", |             "<Task 'name_error'", | ||||||
|             "NameError", |             "NameError", | ||||||
|             "('child'", |             "('child'", | ||||||
|  |             'getattr(doggypants)',  # exc-LoC | ||||||
|         ] |         ] | ||||||
|     ) |     ) | ||||||
|     if ctlc: |     if ctlc: | ||||||
|  | @ -935,8 +942,8 @@ def test_post_mortem_api( | ||||||
|             "<Task '__main__.main'", |             "<Task '__main__.main'", | ||||||
|             "('root'", |             "('root'", | ||||||
|             "NameError", |             "NameError", | ||||||
|             "tractor.post_mortem()", |  | ||||||
|             "src_uid=('child'", |             "src_uid=('child'", | ||||||
|  |             "tractor.post_mortem()",  # in `main()`-LoC | ||||||
|         ] |         ] | ||||||
|     ) |     ) | ||||||
|     if ctlc: |     if ctlc: | ||||||
|  | @ -954,6 +961,10 @@ def test_post_mortem_api( | ||||||
|             "('root'", |             "('root'", | ||||||
|             "NameError", |             "NameError", | ||||||
|             "src_uid=('child'", |             "src_uid=('child'", | ||||||
|  | 
 | ||||||
|  |             # raising line in `main()` but from crash-handling | ||||||
|  |             # in `tractor.open_nursery()`. | ||||||
|  |             'async with p.open_context(name_error) as (ctx, first):', | ||||||
|         ] |         ] | ||||||
|     ) |     ) | ||||||
|     if ctlc: |     if ctlc: | ||||||
|  | @ -1063,6 +1074,136 @@ def test_shield_pause( | ||||||
|     child.expect(EOF) |     child.expect(EOF) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     'quit_early', [False, True] | ||||||
|  | ) | ||||||
|  | def test_ctxep_pauses_n_maybe_ipc_breaks( | ||||||
|  |     spawn: PexpectSpawner, | ||||||
|  |     quit_early: bool, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Audit generator embedded `.pause()`es from within a `@context` | ||||||
|  |     endpoint with a chan close at the end, requiring that ctl-c is | ||||||
|  |     mashed and zombie reaper kills sub with no hangs. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     child = spawn('subactor_bp_in_ctx') | ||||||
|  |     child.expect(PROMPT) | ||||||
|  | 
 | ||||||
|  |     # 3 iters for the `gen()` pause-points | ||||||
|  |     for i in range(3): | ||||||
|  |         assert_before( | ||||||
|  |             child, | ||||||
|  |             [ | ||||||
|  |                 _pause_msg, | ||||||
|  |                 "('bp_boi'",  # actor name | ||||||
|  |                 "<Task 'just_bp'",  # task name | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |         if ( | ||||||
|  |             i == 1 | ||||||
|  |             and | ||||||
|  |             quit_early | ||||||
|  |         ): | ||||||
|  |             child.sendline('q') | ||||||
|  |             child.expect(PROMPT) | ||||||
|  |             assert_before( | ||||||
|  |                 child, | ||||||
|  |                 ["tractor._exceptions.RemoteActorError: remote task raised a 'BdbQuit'", | ||||||
|  |                  "bdb.BdbQuit", | ||||||
|  |                  "('bp_boi'", | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |             child.sendline('c') | ||||||
|  |             child.expect(EOF) | ||||||
|  |             assert_before( | ||||||
|  |                 child, | ||||||
|  |                 ["tractor._exceptions.RemoteActorError: remote task raised a 'BdbQuit'", | ||||||
|  |                  "bdb.BdbQuit", | ||||||
|  |                  "('bp_boi'", | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |             break  # end-of-test | ||||||
|  | 
 | ||||||
|  |         child.sendline('c') | ||||||
|  |         try: | ||||||
|  |             child.expect(PROMPT) | ||||||
|  |         except TIMEOUT: | ||||||
|  |             # no prompt since we hang due to IPC chan purposely | ||||||
|  |             # closed so verify we see error reporting as well as | ||||||
|  |             # a failed crash-REPL request msg and can CTL-c our way | ||||||
|  |             # out. | ||||||
|  |             assert_before( | ||||||
|  |                 child, | ||||||
|  |                 ['peer IPC channel closed abruptly?', | ||||||
|  |                  'another task closed this fd', | ||||||
|  |                  'Debug lock request was CANCELLED?', | ||||||
|  |                  "TransportClosed: 'MsgpackUDSStream' was already closed locally ?",] | ||||||
|  | 
 | ||||||
|  |                 # XXX races on whether these show/hit? | ||||||
|  |                  # 'Failed to REPl via `_pause()` You called `tractor.pause()` from an already cancelled scope!', | ||||||
|  |                  # 'AssertionError', | ||||||
|  |             ) | ||||||
|  |             # OSc(ancel) the hanging tree | ||||||
|  |             do_ctlc( | ||||||
|  |                 child=child, | ||||||
|  |                 expect_prompt=False, | ||||||
|  |             ) | ||||||
|  |             child.expect(EOF) | ||||||
|  |             assert_before( | ||||||
|  |                 child, | ||||||
|  |                 ['KeyboardInterrupt'], | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_crash_handling_within_cancelled_root_actor( | ||||||
|  |     spawn: PexpectSpawner, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Ensure that when only a root-actor is started via `open_root_actor()` | ||||||
|  |     we can crash-handle in debug-mode despite self-cancellation. | ||||||
|  | 
 | ||||||
|  |     More-or-less ensures we conditionally shield the pause in | ||||||
|  |     `._root.open_root_actor()`'s `await debug._maybe_enter_pm()` | ||||||
|  |     call. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     child = spawn('root_self_cancelled_w_error') | ||||||
|  |     child.expect(PROMPT) | ||||||
|  | 
 | ||||||
|  |     assert_before( | ||||||
|  |         child, | ||||||
|  |         [ | ||||||
|  |             "Actor.cancel_soon()` was called!", | ||||||
|  |             "root cancelled", | ||||||
|  |             _pause_msg, | ||||||
|  |             "('root'",  # actor name | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     child.sendline('c') | ||||||
|  |     child.expect(PROMPT) | ||||||
|  |     assert_before( | ||||||
|  |         child, | ||||||
|  |         [ | ||||||
|  |             _crash_msg, | ||||||
|  |             "('root'",  # actor name | ||||||
|  |             "AssertionError", | ||||||
|  |             "assert 0", | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     child.sendline('c') | ||||||
|  |     child.expect(EOF) | ||||||
|  |     assert_before( | ||||||
|  |         child, | ||||||
|  |         [ | ||||||
|  |             "AssertionError", | ||||||
|  |             "assert 0", | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| # TODO: better error for "non-ideal" usage from the root actor. | # TODO: better error for "non-ideal" usage from the root actor. | ||||||
| # -[ ] if called from an async scope emit a message that suggests | # -[ ] if called from an async scope emit a message that suggests | ||||||
| #    using `await tractor.pause()` instead since it's less overhead | #    using `await tractor.pause()` instead since it's less overhead | ||||||
|  |  | ||||||
|  | @ -13,9 +13,16 @@ TODO: | ||||||
|   when debugging a problem inside the stack vs. in their app. |   when debugging a problem inside the stack vs. in their app. | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | from contextlib import ( | ||||||
|  |     contextmanager as cm, | ||||||
|  | ) | ||||||
| import os | import os | ||||||
| import signal | import signal | ||||||
| import time | import time | ||||||
|  | from typing import ( | ||||||
|  |     TYPE_CHECKING, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| from .conftest import ( | from .conftest import ( | ||||||
|     expect, |     expect, | ||||||
|  | @ -24,14 +31,19 @@ from .conftest import ( | ||||||
|     PROMPT, |     PROMPT, | ||||||
|     _pause_msg, |     _pause_msg, | ||||||
| ) | ) | ||||||
|  | 
 | ||||||
|  | import pytest | ||||||
| from pexpect.exceptions import ( | from pexpect.exceptions import ( | ||||||
|     # TIMEOUT, |     # TIMEOUT, | ||||||
|     EOF, |     EOF, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from ..conftest import PexpectSpawner | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def test_shield_pause( | def test_shield_pause( | ||||||
|     spawn, |     spawn: PexpectSpawner, | ||||||
| ): | ): | ||||||
|     ''' |     ''' | ||||||
|     Verify the `tractor.pause()/.post_mortem()` API works inside an |     Verify the `tractor.pause()/.post_mortem()` API works inside an | ||||||
|  | @ -109,9 +121,11 @@ def test_shield_pause( | ||||||
|         child.pid, |         child.pid, | ||||||
|         signal.SIGINT, |         signal.SIGINT, | ||||||
|     ) |     ) | ||||||
|  |     from tractor._supervise import _shutdown_msg | ||||||
|     expect( |     expect( | ||||||
|         child, |         child, | ||||||
|         'Shutting down actor runtime', |         # 'Shutting down actor runtime', | ||||||
|  |         _shutdown_msg, | ||||||
|         timeout=6, |         timeout=6, | ||||||
|     ) |     ) | ||||||
|     assert_before( |     assert_before( | ||||||
|  | @ -126,7 +140,7 @@ def test_shield_pause( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_breakpoint_hook_restored( | def test_breakpoint_hook_restored( | ||||||
|     spawn, |     spawn: PexpectSpawner, | ||||||
| ): | ): | ||||||
|     ''' |     ''' | ||||||
|     Ensures our actor runtime sets a custom `breakpoint()` hook |     Ensures our actor runtime sets a custom `breakpoint()` hook | ||||||
|  | @ -140,16 +154,22 @@ def test_breakpoint_hook_restored( | ||||||
|     child = spawn('restore_builtin_breakpoint') |     child = spawn('restore_builtin_breakpoint') | ||||||
| 
 | 
 | ||||||
|     child.expect(PROMPT) |     child.expect(PROMPT) | ||||||
|     assert_before( |     try: | ||||||
|         child, |         assert_before( | ||||||
|         [ |             child, | ||||||
|             _pause_msg, |             [ | ||||||
|             "<Task '__main__.main'", |                 _pause_msg, | ||||||
|             "('root'", |                 "<Task '__main__.main'", | ||||||
|             "first bp, tractor hook set", |                 "('root'", | ||||||
|         ] |                 "first bp, tractor hook set", | ||||||
|     ) |             ] | ||||||
|     child.sendline('c') |         ) | ||||||
|  |     # XXX if the above raises `AssertionError`, without sending | ||||||
|  |     # the final 'continue' cmd to the REPL-active sub-process, | ||||||
|  |     # we'll hang waiting for that pexpect instance to terminate.. | ||||||
|  |     finally: | ||||||
|  |         child.sendline('c') | ||||||
|  | 
 | ||||||
|     child.expect(PROMPT) |     child.expect(PROMPT) | ||||||
|     assert_before( |     assert_before( | ||||||
|         child, |         child, | ||||||
|  | @ -170,3 +190,117 @@ def test_breakpoint_hook_restored( | ||||||
|     ) |     ) | ||||||
|     child.sendline('c') |     child.sendline('c') | ||||||
|     child.expect(EOF) |     child.expect(EOF) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _to_raise = Exception('Triggering a crash') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     'to_raise', | ||||||
|  |     [ | ||||||
|  |         None, | ||||||
|  |         _to_raise, | ||||||
|  |         RuntimeError('Never crash handle this!'), | ||||||
|  |     ], | ||||||
|  | ) | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     'raise_on_exit', | ||||||
|  |     [ | ||||||
|  |         True, | ||||||
|  |         [type(_to_raise)], | ||||||
|  |         False, | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  | def test_crash_handler_cms( | ||||||
|  |     debug_mode: bool, | ||||||
|  |     to_raise: Exception, | ||||||
|  |     raise_on_exit: bool|list[Exception], | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Verify the `.devx.open_crash_handler()` API(s) by also | ||||||
|  |     (conveniently enough) tesing its `repl_fixture: ContextManager` | ||||||
|  |     param support which for this suite allows use to avoid use of | ||||||
|  |     a `pexpect`-style-test since we use the fixture to avoid actually | ||||||
|  |     entering `PdbpREPL.iteract()` :smirk: | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     import tractor | ||||||
|  |     # import trio | ||||||
|  | 
 | ||||||
|  |     # state flags | ||||||
|  |     repl_acquired: bool = False | ||||||
|  |     repl_released: bool = False | ||||||
|  | 
 | ||||||
|  |     @cm | ||||||
|  |     def block_repl_ux( | ||||||
|  |         repl: tractor.devx.debug.PdbREPL, | ||||||
|  |         maybe_bxerr: ( | ||||||
|  |             tractor.devx._debug.BoxedMaybeException | ||||||
|  |             |None | ||||||
|  |         ) = None, | ||||||
|  |         enter_repl: bool = True, | ||||||
|  | 
 | ||||||
|  |     ) -> bool: | ||||||
|  |         ''' | ||||||
|  |         Set pre/post-REPL state vars and bypass actual conole | ||||||
|  |         interaction. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         nonlocal repl_acquired, repl_released | ||||||
|  | 
 | ||||||
|  |         # task: trio.Task = trio.lowlevel.current_task() | ||||||
|  |         # print(f'pre-REPL active_task={task.name}') | ||||||
|  | 
 | ||||||
|  |         print('pre-REPL') | ||||||
|  |         repl_acquired = True | ||||||
|  |         yield False  # never actually .interact() | ||||||
|  |         print('post-REPL') | ||||||
|  |         repl_released = True | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         # TODO, with runtime's `debug_mode` setting | ||||||
|  |         # -[ ] need to open runtime tho obvi.. | ||||||
|  |         # | ||||||
|  |         # with tractor.devx.maybe_open_crash_handler( | ||||||
|  |         #     pdb=True, | ||||||
|  | 
 | ||||||
|  |         with tractor.devx.open_crash_handler( | ||||||
|  |             raise_on_exit=raise_on_exit, | ||||||
|  |             repl_fixture=block_repl_ux | ||||||
|  |         ) as bxerr: | ||||||
|  |             if to_raise is not None: | ||||||
|  |                 raise to_raise | ||||||
|  | 
 | ||||||
|  |     except Exception as _exc: | ||||||
|  |         exc = _exc | ||||||
|  |         if ( | ||||||
|  |             raise_on_exit is True | ||||||
|  |             or | ||||||
|  |             type(to_raise) in raise_on_exit | ||||||
|  |         ): | ||||||
|  |             assert ( | ||||||
|  |                 exc | ||||||
|  |                 is | ||||||
|  |                 to_raise | ||||||
|  |                 is | ||||||
|  |                 bxerr.value | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         else: | ||||||
|  |             raise | ||||||
|  |     else: | ||||||
|  |         assert ( | ||||||
|  |             to_raise is None | ||||||
|  |             or | ||||||
|  |             not raise_on_exit | ||||||
|  |             or | ||||||
|  |             type(to_raise) not in raise_on_exit | ||||||
|  |         ) | ||||||
|  |         assert bxerr.value is to_raise | ||||||
|  | 
 | ||||||
|  |     assert bxerr.raise_on_exit == raise_on_exit | ||||||
|  | 
 | ||||||
|  |     if to_raise is not None: | ||||||
|  |         assert repl_acquired | ||||||
|  |         assert repl_released | ||||||
|  |  | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | ''' | ||||||
|  | `tractor.ipc` subsystem(s)/unit testing suites. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | @ -0,0 +1,114 @@ | ||||||
|  | ''' | ||||||
|  | Unit-ish tests for specific IPC transport protocol backends. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | from pathlib import Path | ||||||
|  | 
 | ||||||
|  | import pytest | ||||||
|  | import trio | ||||||
|  | import tractor | ||||||
|  | from tractor import ( | ||||||
|  |     Actor, | ||||||
|  |     _state, | ||||||
|  |     _addr, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.fixture | ||||||
|  | def bindspace_dir_str() -> str: | ||||||
|  | 
 | ||||||
|  |     rt_dir: Path = tractor._state.get_rt_dir() | ||||||
|  |     bs_dir: Path = rt_dir / 'doggy' | ||||||
|  |     bs_dir_str: str = str(bs_dir) | ||||||
|  |     assert not bs_dir.is_dir() | ||||||
|  | 
 | ||||||
|  |     yield bs_dir_str | ||||||
|  | 
 | ||||||
|  |     # delete it on suite teardown. | ||||||
|  |     # ?TODO? should we support this internally | ||||||
|  |     # or is leaking it ok? | ||||||
|  |     if bs_dir.is_dir(): | ||||||
|  |         bs_dir.rmdir() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_uds_bindspace_created_implicitly( | ||||||
|  |     debug_mode: bool, | ||||||
|  |     bindspace_dir_str: str, | ||||||
|  | ): | ||||||
|  |     registry_addr: tuple = ( | ||||||
|  |         f'{bindspace_dir_str}', | ||||||
|  |         'registry@doggy.sock', | ||||||
|  |     ) | ||||||
|  |     bs_dir_str: str = registry_addr[0] | ||||||
|  | 
 | ||||||
|  |     # XXX, ensure bindspace-dir DNE beforehand! | ||||||
|  |     assert not Path(bs_dir_str).is_dir() | ||||||
|  | 
 | ||||||
|  |     async def main(): | ||||||
|  |         async with tractor.open_nursery( | ||||||
|  |             enable_transports=['uds'], | ||||||
|  |             registry_addrs=[registry_addr], | ||||||
|  |             debug_mode=debug_mode, | ||||||
|  |         ) as _an: | ||||||
|  | 
 | ||||||
|  |             # XXX MUST be created implicitly by | ||||||
|  |             # `.ipc._uds.start_listener()`! | ||||||
|  |             assert Path(bs_dir_str).is_dir() | ||||||
|  | 
 | ||||||
|  |             root: Actor = tractor.current_actor() | ||||||
|  |             assert root.is_registrar | ||||||
|  | 
 | ||||||
|  |             assert registry_addr in root.reg_addrs | ||||||
|  |             assert ( | ||||||
|  |                 registry_addr | ||||||
|  |                 in | ||||||
|  |                 _state._runtime_vars['_registry_addrs'] | ||||||
|  |             ) | ||||||
|  |             assert ( | ||||||
|  |                 _addr.wrap_address(registry_addr) | ||||||
|  |                 in | ||||||
|  |                 root.registry_addrs | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     trio.run(main) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_uds_double_listen_raises_connerr( | ||||||
|  |     debug_mode: bool, | ||||||
|  |     bindspace_dir_str: str, | ||||||
|  | ): | ||||||
|  |     registry_addr: tuple = ( | ||||||
|  |         f'{bindspace_dir_str}', | ||||||
|  |         'registry@doggy.sock', | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     async def main(): | ||||||
|  |         async with tractor.open_nursery( | ||||||
|  |             enable_transports=['uds'], | ||||||
|  |             registry_addrs=[registry_addr], | ||||||
|  |             debug_mode=debug_mode, | ||||||
|  |         ) as _an: | ||||||
|  | 
 | ||||||
|  |             # runtime up | ||||||
|  |             root: Actor = tractor.current_actor() | ||||||
|  | 
 | ||||||
|  |             from tractor.ipc._uds import ( | ||||||
|  |                 start_listener, | ||||||
|  |                 UDSAddress, | ||||||
|  |             ) | ||||||
|  |             ya_bound_addr: UDSAddress = root.registry_addrs[0] | ||||||
|  |             try: | ||||||
|  |                 await start_listener( | ||||||
|  |                     addr=ya_bound_addr, | ||||||
|  |                 ) | ||||||
|  |             except ConnectionError as connerr: | ||||||
|  |                 assert type(src_exc := connerr.__context__) is OSError | ||||||
|  |                 assert 'Address already in use' in src_exc.args | ||||||
|  |                 # complete, exit test. | ||||||
|  | 
 | ||||||
|  |             else: | ||||||
|  |                 pytest.fail('It dint raise a connerr !?') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     trio.run(main) | ||||||
|  | @ -0,0 +1,95 @@ | ||||||
|  | ''' | ||||||
|  | Verify the `enable_transports` param drives various | ||||||
|  | per-root/sub-actor IPC endpoint/server settings. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | 
 | ||||||
|  | import pytest | ||||||
|  | import trio | ||||||
|  | import tractor | ||||||
|  | from tractor import ( | ||||||
|  |     Actor, | ||||||
|  |     Portal, | ||||||
|  |     ipc, | ||||||
|  |     msg, | ||||||
|  |     _state, | ||||||
|  |     _addr, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | @tractor.context | ||||||
|  | async def chk_tpts( | ||||||
|  |     ctx: tractor.Context, | ||||||
|  |     tpt_proto_key: str, | ||||||
|  | ): | ||||||
|  |     rtvars = _state._runtime_vars | ||||||
|  |     assert ( | ||||||
|  |         tpt_proto_key | ||||||
|  |         in | ||||||
|  |         rtvars['_enable_tpts'] | ||||||
|  |     ) | ||||||
|  |     actor: Actor = tractor.current_actor() | ||||||
|  |     spec: msg.types.SpawnSpec = actor._spawn_spec | ||||||
|  |     assert spec._runtime_vars == rtvars | ||||||
|  | 
 | ||||||
|  |     # ensure individual IPC ep-addr types | ||||||
|  |     serv: ipc._server.Server = actor.ipc_server | ||||||
|  |     addr: ipc._types.Address | ||||||
|  |     for addr in serv.addrs: | ||||||
|  |         assert addr.proto_key == tpt_proto_key | ||||||
|  | 
 | ||||||
|  |     # Actor delegate-props enforcement | ||||||
|  |     assert ( | ||||||
|  |         actor.accept_addrs | ||||||
|  |         == | ||||||
|  |         serv.accept_addrs | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     await ctx.started(serv.accept_addrs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO, parametrize over mis-matched-proto-typed `registry_addrs` | ||||||
|  | # since i seems to work in `piker` but not exactly sure if both tcp | ||||||
|  | # & uds are being deployed then? | ||||||
|  | # | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     'tpt_proto_key', | ||||||
|  |     ['tcp', 'uds'], | ||||||
|  |     ids=lambda item: f'ipc_tpt={item!r}' | ||||||
|  | ) | ||||||
|  | def test_root_passes_tpt_to_sub( | ||||||
|  |     tpt_proto_key: str, | ||||||
|  |     reg_addr: tuple, | ||||||
|  |     debug_mode: bool, | ||||||
|  | ): | ||||||
|  |     async def main(): | ||||||
|  |         async with tractor.open_nursery( | ||||||
|  |             enable_transports=[tpt_proto_key], | ||||||
|  |             registry_addrs=[reg_addr], | ||||||
|  |             debug_mode=debug_mode, | ||||||
|  |         ) as an: | ||||||
|  | 
 | ||||||
|  |             assert ( | ||||||
|  |                 tpt_proto_key | ||||||
|  |                 in | ||||||
|  |                 _state._runtime_vars['_enable_tpts'] | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             ptl: Portal = await an.start_actor( | ||||||
|  |                 name='sub', | ||||||
|  |                 enable_modules=[__name__], | ||||||
|  |             ) | ||||||
|  |             async with ptl.open_context( | ||||||
|  |                 chk_tpts, | ||||||
|  |                 tpt_proto_key=tpt_proto_key, | ||||||
|  |             ) as (ctx, accept_addrs): | ||||||
|  | 
 | ||||||
|  |                 uw_addr: tuple | ||||||
|  |                 for uw_addr in accept_addrs: | ||||||
|  |                     addr = _addr.wrap_address(uw_addr) | ||||||
|  |                     assert addr.is_valid | ||||||
|  | 
 | ||||||
|  |             # shudown sub-actor(s) | ||||||
|  |             await an.cancel() | ||||||
|  | 
 | ||||||
|  |     trio.run(main) | ||||||
|  | @ -0,0 +1,72 @@ | ||||||
|  | ''' | ||||||
|  | High-level `.ipc._server` unit tests. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | 
 | ||||||
|  | import pytest | ||||||
|  | import trio | ||||||
|  | from tractor import ( | ||||||
|  |     devx, | ||||||
|  |     ipc, | ||||||
|  |     log, | ||||||
|  | ) | ||||||
|  | from tractor._testing.addr import ( | ||||||
|  |     get_rando_addr, | ||||||
|  | ) | ||||||
|  | # TODO, use/check-roundtripping with some of these wrapper types? | ||||||
|  | # | ||||||
|  | # from .._addr import Address | ||||||
|  | # from ._chan import Channel | ||||||
|  | # from ._transport import MsgTransport | ||||||
|  | # from ._uds import UDSAddress | ||||||
|  | # from ._tcp import TCPAddress | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     '_tpt_proto', | ||||||
|  |     ['uds', 'tcp'] | ||||||
|  | ) | ||||||
|  | def test_basic_ipc_server( | ||||||
|  |     _tpt_proto: str, | ||||||
|  |     debug_mode: bool, | ||||||
|  |     loglevel: str, | ||||||
|  | ): | ||||||
|  | 
 | ||||||
|  |     # so we see the socket-listener reporting on console | ||||||
|  |     log.get_console_log("INFO") | ||||||
|  | 
 | ||||||
|  |     rando_addr: tuple = get_rando_addr( | ||||||
|  |         tpt_proto=_tpt_proto, | ||||||
|  |     ) | ||||||
|  |     async def main(): | ||||||
|  |         async with ipc._server.open_ipc_server() as server: | ||||||
|  | 
 | ||||||
|  |             assert ( | ||||||
|  |                 server._parent_tn | ||||||
|  |                 and | ||||||
|  |                 server._parent_tn is server._stream_handler_tn | ||||||
|  |             ) | ||||||
|  |             assert server._no_more_peers.is_set() | ||||||
|  | 
 | ||||||
|  |             eps: list[ipc._server.Endpoint] = await server.listen_on( | ||||||
|  |                 accept_addrs=[rando_addr], | ||||||
|  |                 stream_handler_nursery=None, | ||||||
|  |             ) | ||||||
|  |             assert ( | ||||||
|  |                 len(eps) == 1 | ||||||
|  |                 and | ||||||
|  |                 (ep := eps[0])._listener | ||||||
|  |                 and | ||||||
|  |                 not ep.peer_tpts | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             server._parent_tn.cancel_scope.cancel() | ||||||
|  | 
 | ||||||
|  |         # !TODO! actually make a bg-task connection from a client | ||||||
|  |         # using `ipc._chan._connect_chan()` | ||||||
|  | 
 | ||||||
|  |     with devx.maybe_open_crash_handler( | ||||||
|  |         pdb=debug_mode, | ||||||
|  |     ): | ||||||
|  |         trio.run(main) | ||||||
|  | @ -313,9 +313,8 @@ async def inf_streamer( | ||||||
|         # `trio.EndOfChannel` doesn't propagate directly to the above |         # `trio.EndOfChannel` doesn't propagate directly to the above | ||||||
|         # .open_stream() parent, resulting in it also raising instead |         # .open_stream() parent, resulting in it also raising instead | ||||||
|         # of gracefully absorbing as normal.. so how to handle? |         # of gracefully absorbing as normal.. so how to handle? | ||||||
|         trio.open_nursery( |         tractor.trionics.collapse_eg(), | ||||||
|             strict_exception_groups=False, |         trio.open_nursery() as tn, | ||||||
|         ) as tn, |  | ||||||
|     ): |     ): | ||||||
|         async def close_stream_on_sentinel(): |         async def close_stream_on_sentinel(): | ||||||
|             async for msg in stream: |             async for msg in stream: | ||||||
|  |  | ||||||
|  | @ -236,7 +236,10 @@ async def stream_forever(): | ||||||
| async def test_cancel_infinite_streamer(start_method): | async def test_cancel_infinite_streamer(start_method): | ||||||
| 
 | 
 | ||||||
|     # stream for at most 1 seconds |     # stream for at most 1 seconds | ||||||
|     with trio.move_on_after(1) as cancel_scope: |     with ( | ||||||
|  |         trio.fail_after(4), | ||||||
|  |         trio.move_on_after(1) as cancel_scope | ||||||
|  |     ): | ||||||
|         async with tractor.open_nursery() as n: |         async with tractor.open_nursery() as n: | ||||||
|             portal = await n.start_actor( |             portal = await n.start_actor( | ||||||
|                 'donny', |                 'donny', | ||||||
|  | @ -284,20 +287,32 @@ async def test_cancel_infinite_streamer(start_method): | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
| @tractor_test | @tractor_test | ||||||
| async def test_some_cancels_all(num_actors_and_errs, start_method, loglevel): | async def test_some_cancels_all( | ||||||
|     """Verify a subset of failed subactors causes all others in |     num_actors_and_errs: tuple, | ||||||
|  |     start_method: str, | ||||||
|  |     loglevel: str, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Verify a subset of failed subactors causes all others in | ||||||
|     the nursery to be cancelled just like the strategy in trio. |     the nursery to be cancelled just like the strategy in trio. | ||||||
| 
 | 
 | ||||||
|     This is the first and only supervisory strategy at the moment. |     This is the first and only supervisory strategy at the moment. | ||||||
|     """ | 
 | ||||||
|     num_actors, first_err, err_type, ria_func, da_func = num_actors_and_errs |     ''' | ||||||
|  |     ( | ||||||
|  |         num_actors, | ||||||
|  |         first_err, | ||||||
|  |         err_type, | ||||||
|  |         ria_func, | ||||||
|  |         da_func, | ||||||
|  |     ) = num_actors_and_errs | ||||||
|     try: |     try: | ||||||
|         async with tractor.open_nursery() as n: |         async with tractor.open_nursery() as an: | ||||||
| 
 | 
 | ||||||
|             # spawn the same number of deamon actors which should be cancelled |             # spawn the same number of deamon actors which should be cancelled | ||||||
|             dactor_portals = [] |             dactor_portals = [] | ||||||
|             for i in range(num_actors): |             for i in range(num_actors): | ||||||
|                 dactor_portals.append(await n.start_actor( |                 dactor_portals.append(await an.start_actor( | ||||||
|                     f'deamon_{i}', |                     f'deamon_{i}', | ||||||
|                     enable_modules=[__name__], |                     enable_modules=[__name__], | ||||||
|                 )) |                 )) | ||||||
|  | @ -307,7 +322,7 @@ async def test_some_cancels_all(num_actors_and_errs, start_method, loglevel): | ||||||
|             for i in range(num_actors): |             for i in range(num_actors): | ||||||
|                 # start actor(s) that will fail immediately |                 # start actor(s) that will fail immediately | ||||||
|                 riactor_portals.append( |                 riactor_portals.append( | ||||||
|                     await n.run_in_actor( |                     await an.run_in_actor( | ||||||
|                         func, |                         func, | ||||||
|                         name=f'actor_{i}', |                         name=f'actor_{i}', | ||||||
|                         **kwargs |                         **kwargs | ||||||
|  | @ -337,7 +352,8 @@ async def test_some_cancels_all(num_actors_and_errs, start_method, loglevel): | ||||||
| 
 | 
 | ||||||
|         # should error here with a ``RemoteActorError`` or ``MultiError`` |         # should error here with a ``RemoteActorError`` or ``MultiError`` | ||||||
| 
 | 
 | ||||||
|     except first_err as err: |     except first_err as _err: | ||||||
|  |         err = _err | ||||||
|         if isinstance(err, BaseExceptionGroup): |         if isinstance(err, BaseExceptionGroup): | ||||||
|             assert len(err.exceptions) == num_actors |             assert len(err.exceptions) == num_actors | ||||||
|             for exc in err.exceptions: |             for exc in err.exceptions: | ||||||
|  | @ -348,8 +364,8 @@ async def test_some_cancels_all(num_actors_and_errs, start_method, loglevel): | ||||||
|         elif isinstance(err, tractor.RemoteActorError): |         elif isinstance(err, tractor.RemoteActorError): | ||||||
|             assert err.boxed_type == err_type |             assert err.boxed_type == err_type | ||||||
| 
 | 
 | ||||||
|         assert n.cancelled is True |         assert an.cancelled is True | ||||||
|         assert not n._children |         assert not an._children | ||||||
|     else: |     else: | ||||||
|         pytest.fail("Should have gotten a remote assertion error?") |         pytest.fail("Should have gotten a remote assertion error?") | ||||||
| 
 | 
 | ||||||
|  | @ -519,10 +535,15 @@ def test_cancel_via_SIGINT_other_task( | ||||||
|     async def main(): |     async def main(): | ||||||
|         # should never timeout since SIGINT should cancel the current program |         # should never timeout since SIGINT should cancel the current program | ||||||
|         with trio.fail_after(timeout): |         with trio.fail_after(timeout): | ||||||
|             async with trio.open_nursery( |             async with ( | ||||||
|                 strict_exception_groups=False, | 
 | ||||||
|             ) as n: |                 # XXX ?TODO? why no work!? | ||||||
|                 await n.start(spawn_and_sleep_forever) |                 # tractor.trionics.collapse_eg(), | ||||||
|  |                 trio.open_nursery( | ||||||
|  |                     strict_exception_groups=False, | ||||||
|  |                 ) as tn, | ||||||
|  |             ): | ||||||
|  |                 await tn.start(spawn_and_sleep_forever) | ||||||
|                 if 'mp' in spawn_backend: |                 if 'mp' in spawn_backend: | ||||||
|                     time.sleep(0.1) |                     time.sleep(0.1) | ||||||
|                 os.kill(pid, signal.SIGINT) |                 os.kill(pid, signal.SIGINT) | ||||||
|  | @ -533,38 +554,123 @@ def test_cancel_via_SIGINT_other_task( | ||||||
| 
 | 
 | ||||||
| async def spin_for(period=3): | async def spin_for(period=3): | ||||||
|     "Sync sleep." |     "Sync sleep." | ||||||
|  |     print(f'sync sleeping in sub-sub for {period}\n') | ||||||
|     time.sleep(period) |     time.sleep(period) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def spawn(): | async def spawn_sub_with_sync_blocking_task(): | ||||||
|     async with tractor.open_nursery() as tn: |     async with tractor.open_nursery() as an: | ||||||
|         await tn.run_in_actor( |         print('starting sync blocking subactor..\n') | ||||||
|  |         await an.run_in_actor( | ||||||
|             spin_for, |             spin_for, | ||||||
|             name='sleeper', |             name='sleeper', | ||||||
|         ) |         ) | ||||||
|  |         print('exiting first subactor layer..\n') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     'man_cancel_outer', | ||||||
|  |     [ | ||||||
|  |         False,  # passes if delay != 2 | ||||||
|  | 
 | ||||||
|  |         # always causes an unexpected eg-w-embedded-assert-err? | ||||||
|  |         pytest.param(True, | ||||||
|  |              marks=pytest.mark.xfail( | ||||||
|  |                  reason=( | ||||||
|  |                     'always causes an unexpected eg-w-embedded-assert-err?' | ||||||
|  |                 ) | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ], | ||||||
|  | ) | ||||||
| @no_windows | @no_windows | ||||||
| def test_cancel_while_childs_child_in_sync_sleep( | def test_cancel_while_childs_child_in_sync_sleep( | ||||||
|     loglevel, |     loglevel: str, | ||||||
|     start_method, |     start_method: str, | ||||||
|     spawn_backend, |     spawn_backend: str, | ||||||
|  |     debug_mode: bool, | ||||||
|  |     reg_addr: tuple, | ||||||
|  |     man_cancel_outer: bool, | ||||||
| ): | ): | ||||||
|     """Verify that a child cancelled while executing sync code is torn |     ''' | ||||||
|  |     Verify that a child cancelled while executing sync code is torn | ||||||
|     down even when that cancellation is triggered by the parent |     down even when that cancellation is triggered by the parent | ||||||
|     2 nurseries "up". |     2 nurseries "up". | ||||||
|     """ | 
 | ||||||
|  |     Though the grandchild should stay blocking its actor runtime, its | ||||||
|  |     parent should issue a "zombie reaper" to hard kill it after | ||||||
|  |     sufficient timeout. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|     if start_method == 'forkserver': |     if start_method == 'forkserver': | ||||||
|         pytest.skip("Forksever sux hard at resuming from sync sleep...") |         pytest.skip("Forksever sux hard at resuming from sync sleep...") | ||||||
| 
 | 
 | ||||||
|     async def main(): |     async def main(): | ||||||
|         with trio.fail_after(2): |         # | ||||||
|             async with tractor.open_nursery() as tn: |         # XXX BIG TODO NOTE XXX | ||||||
|                 await tn.run_in_actor( |         # | ||||||
|                     spawn, |         # it seems there's a strange race that can happen | ||||||
|                     name='spawn', |         # where where the fail-after will trigger outer scope | ||||||
|  |         # .cancel() which then causes the inner scope to raise, | ||||||
|  |         # | ||||||
|  |         # BaseExceptionGroup('Exceptions from Trio nursery', [ | ||||||
|  |         #   BaseExceptionGroup('Exceptions from Trio nursery', | ||||||
|  |         #   [ | ||||||
|  |         #       Cancelled(), | ||||||
|  |         #       Cancelled(), | ||||||
|  |         #   ] | ||||||
|  |         #   ), | ||||||
|  |         #   AssertionError('assert 0') | ||||||
|  |         # ]) | ||||||
|  |         # | ||||||
|  |         # WHY THIS DOESN'T MAKE SENSE: | ||||||
|  |         # --------------------------- | ||||||
|  |         # - it should raise too-slow-error when too slow.. | ||||||
|  |         #  * verified that using simple-cs and manually cancelling | ||||||
|  |         #    you get same outcome -> indicates that the fail-after | ||||||
|  |         #    can have its TooSlowError overriden! | ||||||
|  |         #  |_ to check this it's easy, simplly decrease the timeout | ||||||
|  |         #     as per the var below. | ||||||
|  |         # | ||||||
|  |         # - when using the manual simple-cs the outcome is different | ||||||
|  |         #   DESPITE the `assert 0` which means regardless of the | ||||||
|  |         #   inner scope effectively failing in the same way, the | ||||||
|  |         #   bubbling up **is NOT the same**. | ||||||
|  |         # | ||||||
|  |         # delays trigger diff outcomes.. | ||||||
|  |         # --------------------------- | ||||||
|  |         # as seen by uncommenting various lines below there is from | ||||||
|  |         # my POV an unexpected outcome due to the delay=2 case. | ||||||
|  |         # | ||||||
|  |         # delay = 1  # no AssertionError in eg, TooSlowError raised. | ||||||
|  |         # delay = 2  # is AssertionError in eg AND no TooSlowError !? | ||||||
|  |         delay = 4  # is AssertionError in eg AND no _cs cancellation. | ||||||
|  | 
 | ||||||
|  |         with trio.fail_after(delay) as _cs: | ||||||
|  |         # with trio.CancelScope() as cs: | ||||||
|  |         # ^XXX^ can be used instead to see same outcome. | ||||||
|  | 
 | ||||||
|  |             async with ( | ||||||
|  |                 # tractor.trionics.collapse_eg(),  # doesn't help | ||||||
|  |                 tractor.open_nursery( | ||||||
|  |                     hide_tb=False, | ||||||
|  |                     debug_mode=debug_mode, | ||||||
|  |                     registry_addrs=[reg_addr], | ||||||
|  |                 ) as an, | ||||||
|  |             ): | ||||||
|  |                 await an.run_in_actor( | ||||||
|  |                     spawn_sub_with_sync_blocking_task, | ||||||
|  |                     name='sync_blocking_sub', | ||||||
|                 ) |                 ) | ||||||
|                 await trio.sleep(1) |                 await trio.sleep(1) | ||||||
|  | 
 | ||||||
|  |                 if man_cancel_outer: | ||||||
|  |                     print('Cancelling manually in root') | ||||||
|  |                     _cs.cancel() | ||||||
|  | 
 | ||||||
|  |                 # trigger exc-srced taskc down | ||||||
|  |                 # the actor tree. | ||||||
|  |                 print('RAISING IN ROOT') | ||||||
|                 assert 0 |                 assert 0 | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(AssertionError): |     with pytest.raises(AssertionError): | ||||||
|  |  | ||||||
|  | @ -117,9 +117,10 @@ async def open_actor_local_nursery( | ||||||
|     ctx: tractor.Context, |     ctx: tractor.Context, | ||||||
| ): | ): | ||||||
|     global _nursery |     global _nursery | ||||||
|     async with trio.open_nursery( |     async with ( | ||||||
|         strict_exception_groups=False, |         tractor.trionics.collapse_eg(), | ||||||
|     ) as tn: |         trio.open_nursery() as tn | ||||||
|  |     ): | ||||||
|         _nursery = tn |         _nursery = tn | ||||||
|         await ctx.started() |         await ctx.started() | ||||||
|         await trio.sleep(10) |         await trio.sleep(10) | ||||||
|  |  | ||||||
|  | @ -13,26 +13,24 @@ MESSAGE = 'tractoring at full speed' | ||||||
| def test_empty_mngrs_input_raises() -> None: | def test_empty_mngrs_input_raises() -> None: | ||||||
| 
 | 
 | ||||||
|     async def main(): |     async def main(): | ||||||
|         with trio.fail_after(1): |         with trio.fail_after(3): | ||||||
|             async with ( |             async with ( | ||||||
|                 open_actor_cluster( |                 open_actor_cluster( | ||||||
|                     modules=[__name__], |                     modules=[__name__], | ||||||
| 
 | 
 | ||||||
|                     # NOTE: ensure we can passthrough runtime opts |                     # NOTE: ensure we can passthrough runtime opts | ||||||
|                     loglevel='info', |                     loglevel='cancel', | ||||||
|                     # debug_mode=True, |                     debug_mode=False, | ||||||
| 
 | 
 | ||||||
|                 ) as portals, |                 ) as portals, | ||||||
| 
 | 
 | ||||||
|                 gather_contexts( |                 gather_contexts(mngrs=()), | ||||||
|                     # NOTE: it's the use of inline-generator syntax |  | ||||||
|                     # here that causes the empty input. |  | ||||||
|                     mngrs=( |  | ||||||
|                         p.open_context(worker) for p in portals.values() |  | ||||||
|                     ), |  | ||||||
|                 ), |  | ||||||
|             ): |             ): | ||||||
|                 assert 0 |                 # should fail before this? | ||||||
|  |                 assert portals | ||||||
|  | 
 | ||||||
|  |                 # test should fail if we mk it here! | ||||||
|  |                 assert 0, 'Should have raised val-err !?' | ||||||
| 
 | 
 | ||||||
|     with pytest.raises(ValueError): |     with pytest.raises(ValueError): | ||||||
|         trio.run(main) |         trio.run(main) | ||||||
|  |  | ||||||
|  | @ -252,7 +252,7 @@ def test_simple_context( | ||||||
|             pass |             pass | ||||||
|         except BaseExceptionGroup as beg: |         except BaseExceptionGroup as beg: | ||||||
|             # XXX: on windows it seems we may have to expect the group error |             # XXX: on windows it seems we may have to expect the group error | ||||||
|             from tractor._exceptions import is_multi_cancelled |             from tractor.trionics import is_multi_cancelled | ||||||
|             assert is_multi_cancelled(beg) |             assert is_multi_cancelled(beg) | ||||||
|     else: |     else: | ||||||
|         trio.run(main) |         trio.run(main) | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ import psutil | ||||||
| import pytest | import pytest | ||||||
| import subprocess | import subprocess | ||||||
| import tractor | import tractor | ||||||
|  | from tractor.trionics import collapse_eg | ||||||
| from tractor._testing import tractor_test | from tractor._testing import tractor_test | ||||||
| import trio | import trio | ||||||
| 
 | 
 | ||||||
|  | @ -193,10 +194,10 @@ async def spawn_and_check_registry( | ||||||
| 
 | 
 | ||||||
|             try: |             try: | ||||||
|                 async with tractor.open_nursery() as an: |                 async with tractor.open_nursery() as an: | ||||||
|                     async with trio.open_nursery( |                     async with ( | ||||||
|                         strict_exception_groups=False, |                         collapse_eg(), | ||||||
|                     ) as trion: |                         trio.open_nursery() as trion, | ||||||
| 
 |                     ): | ||||||
|                         portals = {} |                         portals = {} | ||||||
|                         for i in range(3): |                         for i in range(3): | ||||||
|                             name = f'a{i}' |                             name = f'a{i}' | ||||||
|  | @ -338,11 +339,12 @@ async def close_chans_before_nursery( | ||||||
|                         async with portal2.open_stream_from( |                         async with portal2.open_stream_from( | ||||||
|                             stream_forever |                             stream_forever | ||||||
|                         ) as agen2: |                         ) as agen2: | ||||||
|                             async with trio.open_nursery( |                             async with ( | ||||||
|                                 strict_exception_groups=False, |                                 collapse_eg(), | ||||||
|                             ) as n: |                                 trio.open_nursery() as tn, | ||||||
|                                 n.start_soon(streamer, agen1) |                             ): | ||||||
|                                 n.start_soon(cancel, use_signal, .5) |                                 tn.start_soon(streamer, agen1) | ||||||
|  |                                 tn.start_soon(cancel, use_signal, .5) | ||||||
|                                 try: |                                 try: | ||||||
|                                     await streamer(agen2) |                                     await streamer(agen2) | ||||||
|                                 finally: |                                 finally: | ||||||
|  |  | ||||||
|  | @ -234,10 +234,8 @@ async def trio_ctx( | ||||||
|     with trio.fail_after(1 + delay): |     with trio.fail_after(1 + delay): | ||||||
|         try: |         try: | ||||||
|             async with ( |             async with ( | ||||||
|                 trio.open_nursery( |                 tractor.trionics.collapse_eg(), | ||||||
|                     # TODO, for new `trio` / py3.13 |                 trio.open_nursery() as tn, | ||||||
|                     # strict_exception_groups=False, |  | ||||||
|                 ) as tn, |  | ||||||
|                 tractor.to_asyncio.open_channel_from( |                 tractor.to_asyncio.open_channel_from( | ||||||
|                     sleep_and_err, |                     sleep_and_err, | ||||||
|                 ) as (first, chan), |                 ) as (first, chan), | ||||||
|  | @ -573,14 +571,16 @@ def test_basic_interloop_channel_stream( | ||||||
|     fan_out: bool, |     fan_out: bool, | ||||||
| ): | ): | ||||||
|     async def main(): |     async def main(): | ||||||
|         async with tractor.open_nursery() as an: |         # TODO, figure out min timeout here! | ||||||
|             portal = await an.run_in_actor( |         with trio.fail_after(6): | ||||||
|                 stream_from_aio, |             async with tractor.open_nursery() as an: | ||||||
|                 infect_asyncio=True, |                 portal = await an.run_in_actor( | ||||||
|                 fan_out=fan_out, |                     stream_from_aio, | ||||||
|             ) |                     infect_asyncio=True, | ||||||
|             # should raise RAE diectly |                     fan_out=fan_out, | ||||||
|             await portal.result() |                 ) | ||||||
|  |                 # should raise RAE diectly | ||||||
|  |                 await portal.result() | ||||||
| 
 | 
 | ||||||
|     trio.run(main) |     trio.run(main) | ||||||
| 
 | 
 | ||||||
|  | @ -889,7 +889,7 @@ async def manage_file( | ||||||
| 
 | 
 | ||||||
|         # NOTE: turns out you don't even need to sched an aio task |         # NOTE: turns out you don't even need to sched an aio task | ||||||
|         # since the original issue, even though seemingly was due to |         # since the original issue, even though seemingly was due to | ||||||
|         # the guest-run being abandoned + a `._debug.pause()` inside |         # the guest-run being abandoned + a `.debug.pause()` inside | ||||||
|         # `._runtime._async_main()` (which was originally trying to |         # `._runtime._async_main()` (which was originally trying to | ||||||
|         # debug the `.lifetime_stack` not closing), IS NOT actually |         # debug the `.lifetime_stack` not closing), IS NOT actually | ||||||
|         # the core issue? |         # the core issue? | ||||||
|  | @ -1088,6 +1088,108 @@ def test_sigint_closes_lifetime_stack( | ||||||
|     trio.run(main) |     trio.run(main) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | # ?TODO asyncio.Task fn-deco? | ||||||
|  | # -[ ] do sig checkingat import time like @context? | ||||||
|  | # -[ ] maybe name it @aio_task ?? | ||||||
|  | # -[ ] chan: to_asyncio.InterloopChannel ?? | ||||||
|  | async def raise_before_started( | ||||||
|  |     # from_trio: asyncio.Queue, | ||||||
|  |     # to_trio: trio.abc.SendChannel, | ||||||
|  |     chan: to_asyncio.LinkedTaskChannel, | ||||||
|  | 
 | ||||||
|  | ) -> None: | ||||||
|  |     ''' | ||||||
|  |     `asyncio.Task` entry point which RTEs before calling | ||||||
|  |     `to_trio.send_nowait()`. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     await asyncio.sleep(0.2) | ||||||
|  |     raise RuntimeError('Some shite went wrong before `.send_nowait()`!!') | ||||||
|  | 
 | ||||||
|  |     # to_trio.send_nowait('Uhh we shouldve RTE-d ^^ ??') | ||||||
|  |     chan.started_nowait('Uhh we shouldve RTE-d ^^ ??') | ||||||
|  |     await asyncio.sleep(float('inf')) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @tractor.context | ||||||
|  | async def caching_ep( | ||||||
|  |     ctx: tractor.Context, | ||||||
|  | ): | ||||||
|  | 
 | ||||||
|  |     log = tractor.log.get_logger('caching_ep') | ||||||
|  |     log.info('syncing via `ctx.started()`') | ||||||
|  |     await ctx.started() | ||||||
|  | 
 | ||||||
|  |     # XXX, allocate the `open_channel_from()` inside | ||||||
|  |     # a `.trionics.maybe_open_context()`. | ||||||
|  |     chan: to_asyncio.LinkedTaskChannel | ||||||
|  |     async with ( | ||||||
|  |         tractor.trionics.maybe_open_context( | ||||||
|  |             acm_func=tractor.to_asyncio.open_channel_from, | ||||||
|  |             kwargs={ | ||||||
|  |                 'target': raise_before_started, | ||||||
|  |                 # ^XXX, kwarg to `open_channel_from()` | ||||||
|  |             }, | ||||||
|  | 
 | ||||||
|  |             # lock around current actor task access | ||||||
|  |             key=tractor.current_actor().uid, | ||||||
|  | 
 | ||||||
|  |         ) as (cache_hit, (clients, chan)), | ||||||
|  |     ): | ||||||
|  |         if cache_hit: | ||||||
|  |             log.error( | ||||||
|  |                 'Re-using cached `.open_from_channel()` call!\n' | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         else: | ||||||
|  |             log.info( | ||||||
|  |                 'Allocating SHOULD-FAIL `.open_from_channel()`\n' | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         await trio.sleep_forever() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_aio_side_raises_before_started( | ||||||
|  |     reg_addr: tuple[str, int], | ||||||
|  |     debug_mode: bool, | ||||||
|  |     loglevel: str, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Simulates connection-err from `piker.brokers.ib.api`.. | ||||||
|  | 
 | ||||||
|  |     Ensure any error raised by child-`asyncio.Task` BEFORE | ||||||
|  |     `chan.started()` | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     # delay = 999 if debug_mode else 1 | ||||||
|  |     async def main(): | ||||||
|  |         with trio.fail_after(3): | ||||||
|  |             an: tractor.ActorNursery | ||||||
|  |             async with tractor.open_nursery( | ||||||
|  |                 debug_mode=debug_mode, | ||||||
|  |                 loglevel=loglevel, | ||||||
|  |             ) as an: | ||||||
|  |                 p: tractor.Portal = await an.start_actor( | ||||||
|  |                     'lchan_cacher_that_raises_fast', | ||||||
|  |                     enable_modules=[__name__], | ||||||
|  |                     infect_asyncio=True, | ||||||
|  |                 ) | ||||||
|  |                 async with p.open_context( | ||||||
|  |                     caching_ep, | ||||||
|  |                 ) as (ctx, first): | ||||||
|  |                     assert not first | ||||||
|  | 
 | ||||||
|  |     with pytest.raises( | ||||||
|  |         expected_exception=(RemoteActorError), | ||||||
|  |     ) as excinfo: | ||||||
|  |         trio.run(main) | ||||||
|  | 
 | ||||||
|  |     # ensure `asyncio.Task` exception is bubbled | ||||||
|  |     # allll the way erp!! | ||||||
|  |     rae = excinfo.value | ||||||
|  |     assert rae.boxed_type is RuntimeError | ||||||
|  | 
 | ||||||
| # TODO: debug_mode tests once we get support for `asyncio`! | # TODO: debug_mode tests once we get support for `asyncio`! | ||||||
| # | # | ||||||
| # -[ ] need tests to wrap both scripts: | # -[ ] need tests to wrap both scripts: | ||||||
|  | @ -1101,7 +1203,7 @@ def test_sigint_closes_lifetime_stack( | ||||||
| #    => completed using `.bestow_portal(task)` inside | #    => completed using `.bestow_portal(task)` inside | ||||||
| #     `.to_asyncio._run_asyncio_task()` right? | #     `.to_asyncio._run_asyncio_task()` right? | ||||||
| #   -[ ] translation func to get from `asyncio` task calling to  | #   -[ ] translation func to get from `asyncio` task calling to  | ||||||
| #     `._debug.wait_for_parent_stdin_hijack()` which does root | #     `.debug.wait_for_parent_stdin_hijack()` which does root | ||||||
| #     call to do TTY locking. | #     call to do TTY locking. | ||||||
| # | # | ||||||
| def test_sync_breakpoint(): | def test_sync_breakpoint(): | ||||||
|  |  | ||||||
|  | @ -410,7 +410,6 @@ def test_peer_canceller( | ||||||
|     ''' |     ''' | ||||||
|     async def main(): |     async def main(): | ||||||
|         async with tractor.open_nursery( |         async with tractor.open_nursery( | ||||||
|             # NOTE: to halt the peer tasks on ctxc, uncomment this. |  | ||||||
|             debug_mode=debug_mode, |             debug_mode=debug_mode, | ||||||
|         ) as an: |         ) as an: | ||||||
|             canceller: Portal = await an.start_actor( |             canceller: Portal = await an.start_actor( | ||||||
|  |  | ||||||
|  | @ -235,10 +235,16 @@ async def cancel_after(wait, reg_addr): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @pytest.fixture(scope='module') | @pytest.fixture(scope='module') | ||||||
| def time_quad_ex(reg_addr, ci_env, spawn_backend): | def time_quad_ex( | ||||||
|  |     reg_addr: tuple, | ||||||
|  |     ci_env: bool, | ||||||
|  |     spawn_backend: str, | ||||||
|  | ): | ||||||
|     if spawn_backend == 'mp': |     if spawn_backend == 'mp': | ||||||
|         """no idea but the  mp *nix runs are flaking out here often... |         ''' | ||||||
|         """ |         no idea but the  mp *nix runs are flaking out here often... | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|         pytest.skip("Test is too flaky on mp in CI") |         pytest.skip("Test is too flaky on mp in CI") | ||||||
| 
 | 
 | ||||||
|     timeout = 7 if platform.system() in ('Windows', 'Darwin') else 4 |     timeout = 7 if platform.system() in ('Windows', 'Darwin') else 4 | ||||||
|  | @ -249,12 +255,24 @@ def time_quad_ex(reg_addr, ci_env, spawn_backend): | ||||||
|     return results, diff |     return results, diff | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_a_quadruple_example(time_quad_ex, ci_env, spawn_backend): | def test_a_quadruple_example( | ||||||
|     """This also serves as a kind of "we'd like to be this fast test".""" |     time_quad_ex: tuple, | ||||||
|  |     ci_env: bool, | ||||||
|  |     spawn_backend: str, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     This also serves as a kind of "we'd like to be this fast test". | ||||||
| 
 | 
 | ||||||
|  |     ''' | ||||||
|     results, diff = time_quad_ex |     results, diff = time_quad_ex | ||||||
|     assert results |     assert results | ||||||
|     this_fast = 6 if platform.system() in ('Windows', 'Darwin') else 3 |     this_fast = ( | ||||||
|  |         6 if platform.system() in ( | ||||||
|  |             'Windows', | ||||||
|  |             'Darwin', | ||||||
|  |         ) | ||||||
|  |         else 3 | ||||||
|  |     ) | ||||||
|     assert diff < this_fast |     assert diff < this_fast | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,237 @@ | ||||||
|  | ''' | ||||||
|  | Special case testing for issues not (dis)covered in the primary | ||||||
|  | `Context` related functional/scenario suites. | ||||||
|  | 
 | ||||||
|  | **NOTE: this mod is a WIP** space for handling | ||||||
|  | odd/rare/undiscovered/not-yet-revealed faults which either | ||||||
|  | loudly (ideal case) breakl our supervision protocol | ||||||
|  | or (worst case) result in distributed sys hangs. | ||||||
|  | 
 | ||||||
|  | Suites here further try to clarify (if [partially] ill-defined) and | ||||||
|  | verify our edge case semantics for inter-actor-relayed-exceptions | ||||||
|  | including, | ||||||
|  | 
 | ||||||
|  | - lowlevel: what remote obj-data is interchanged for IPC and what is | ||||||
|  |   native-obj form is expected from unpacking in the the new | ||||||
|  |   mem-domain. | ||||||
|  | 
 | ||||||
|  | - which kinds of `RemoteActorError` (and its derivs) are expected by which | ||||||
|  |   (types of) peers (parent, child, sibling, etc) with what | ||||||
|  |   particular meta-data set such as, | ||||||
|  | 
 | ||||||
|  |   - `.src_uid`: the original (maybe) peer who raised. | ||||||
|  |   - `.relay_uid`: the next-hop-peer who sent it. | ||||||
|  |   - `.relay_path`: the sequence of peer actor hops. | ||||||
|  |   - `.is_inception`: a predicate that denotes multi-hop remote errors. | ||||||
|  | 
 | ||||||
|  | - when should `ExceptionGroup`s be relayed from a particular | ||||||
|  |   remote endpoint, they should never be caused by implicit `._rpc` | ||||||
|  |   nursery machinery! | ||||||
|  | 
 | ||||||
|  | - various special `trio` edge cases around its cancellation semantics | ||||||
|  |   and how we (currently) leverage `trio.Cancelled` as a signal for | ||||||
|  |   whether a `Context` task should raise `ContextCancelled` (ctx). | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | import pytest | ||||||
|  | import trio | ||||||
|  | import tractor | ||||||
|  | from tractor import (  # typing | ||||||
|  |     ActorNursery, | ||||||
|  |     Portal, | ||||||
|  |     Context, | ||||||
|  |     ContextCancelled, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @tractor.context | ||||||
|  | async def sleep_n_chkpt_in_finally( | ||||||
|  |     ctx: Context, | ||||||
|  |     sleep_n_raise: bool, | ||||||
|  | 
 | ||||||
|  |     chld_raise_delay: float, | ||||||
|  |     chld_finally_delay: float, | ||||||
|  | 
 | ||||||
|  |     rent_cancels: bool, | ||||||
|  |     rent_ctxc_delay: float, | ||||||
|  | 
 | ||||||
|  |     expect_exc: str|None = None, | ||||||
|  | 
 | ||||||
|  | ) -> None: | ||||||
|  |     ''' | ||||||
|  |     Sync, open a tn, then wait for cancel, run a chkpt inside | ||||||
|  |     the user's `finally:` teardown. | ||||||
|  | 
 | ||||||
|  |     This covers a footgun case that `trio` core doesn't seem to care about | ||||||
|  |     wherein an exc can be masked by a `trio.Cancelled` raised inside a tn emedded | ||||||
|  |     `finally:`. | ||||||
|  | 
 | ||||||
|  |     Also see `test_trioisms::test_acm_embedded_nursery_propagates_enter_err` | ||||||
|  |     for the down and gritty details. | ||||||
|  | 
 | ||||||
|  |     Since a `@context` endpoint fn can also contain code like this, | ||||||
|  |     **and** bc we currently have no easy way other then | ||||||
|  |     `trio.Cancelled` to signal cancellation on each side of an IPC `Context`, | ||||||
|  |     the footgun issue can compound itself as demonstrated in this suite.. | ||||||
|  | 
 | ||||||
|  |     Here are some edge cases codified with our WIP "sclang" syntax | ||||||
|  |     (note the parent(rent)/child(chld) naming here is just | ||||||
|  |     pragmatism, generally these most of these cases can occurr | ||||||
|  |     regardless of the distributed-task's supervision hiearchy), | ||||||
|  | 
 | ||||||
|  |     - rent c)=> chld.raises-then-taskc-in-finally | ||||||
|  |      |_ chld's body raises an `exc: BaseException`. | ||||||
|  |       _ in its `finally:` block it runs a chkpoint | ||||||
|  |         which raises a taskc (`trio.Cancelled`) which | ||||||
|  |         masks `exc` instead raising taskc up to the first tn. | ||||||
|  |       _ the embedded/chld tn captures the masking taskc and then | ||||||
|  |         raises it up to the ._rpc-ep-tn instead of `exc`. | ||||||
|  |       _ the rent thinks the child ctxc-ed instead of errored.. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     await ctx.started() | ||||||
|  | 
 | ||||||
|  |     if expect_exc: | ||||||
|  |         expect_exc: BaseException = tractor._exceptions.get_err_type( | ||||||
|  |             type_name=expect_exc, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     berr: BaseException|None = None | ||||||
|  |     try: | ||||||
|  |         if not sleep_n_raise: | ||||||
|  |             await trio.sleep_forever() | ||||||
|  |         elif sleep_n_raise: | ||||||
|  | 
 | ||||||
|  |             # XXX this sleep is less then the sleep the parent | ||||||
|  |             # does before calling `ctx.cancel()` | ||||||
|  |             await trio.sleep(chld_raise_delay) | ||||||
|  | 
 | ||||||
|  |             # XXX this will be masked by a taskc raised in | ||||||
|  |             # the `finally:` if this fn doesn't terminate | ||||||
|  |             # before any ctxc-req arrives AND a checkpoint is hit | ||||||
|  |             # in that `finally:`. | ||||||
|  |             raise RuntimeError('my app krurshed..') | ||||||
|  | 
 | ||||||
|  |     except BaseException as _berr: | ||||||
|  |         berr = _berr | ||||||
|  | 
 | ||||||
|  |         # TODO: it'd sure be nice to be able to inject our own | ||||||
|  |         # `ContextCancelled` here instead of of `trio.Cancelled` | ||||||
|  |         # so that our runtime can expect it and this "user code" | ||||||
|  |         # would be able to tell the diff between a generic trio | ||||||
|  |         # cancel and a tractor runtime-IPC cancel. | ||||||
|  |         if expect_exc: | ||||||
|  |             if not isinstance( | ||||||
|  |                 berr, | ||||||
|  |                 expect_exc, | ||||||
|  |             ): | ||||||
|  |                 raise ValueError( | ||||||
|  |                     f'Unexpected exc type ??\n' | ||||||
|  |                     f'{berr!r}\n' | ||||||
|  |                     f'\n' | ||||||
|  |                     f'Expected a {expect_exc!r}\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |         raise berr | ||||||
|  | 
 | ||||||
|  |     # simulate what user code might try even though | ||||||
|  |     # it's a known boo-boo.. | ||||||
|  |     finally: | ||||||
|  |         # maybe wait for rent ctxc to arrive | ||||||
|  |         with trio.CancelScope(shield=True): | ||||||
|  |             await trio.sleep(chld_finally_delay) | ||||||
|  | 
 | ||||||
|  |         # !!XXX this will raise `trio.Cancelled` which | ||||||
|  |         # will mask the RTE from above!!! | ||||||
|  |         # | ||||||
|  |         # YES, it's the same case as our extant | ||||||
|  |         # `test_trioisms::test_acm_embedded_nursery_propagates_enter_err` | ||||||
|  |         try: | ||||||
|  |             await trio.lowlevel.checkpoint() | ||||||
|  |         except trio.Cancelled as taskc: | ||||||
|  |             if (scope_err := taskc.__context__): | ||||||
|  |                 print( | ||||||
|  |                     f'XXX MASKED REMOTE ERROR XXX\n' | ||||||
|  |                     f'ENDPOINT exception -> {scope_err!r}\n' | ||||||
|  |                     f'will be masked by -> {taskc!r}\n' | ||||||
|  |                 ) | ||||||
|  |                 # await tractor.pause(shield=True) | ||||||
|  | 
 | ||||||
|  |             raise taskc | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @pytest.mark.parametrize( | ||||||
|  |     'chld_callspec', | ||||||
|  |     [ | ||||||
|  |         dict( | ||||||
|  |             sleep_n_raise=None, | ||||||
|  |             chld_raise_delay=0.1, | ||||||
|  |             chld_finally_delay=0.1, | ||||||
|  |             expect_exc='Cancelled', | ||||||
|  |             rent_cancels=True, | ||||||
|  |             rent_ctxc_delay=0.1, | ||||||
|  |         ), | ||||||
|  |         dict( | ||||||
|  |             sleep_n_raise='RuntimeError', | ||||||
|  |             chld_raise_delay=0.1, | ||||||
|  |             chld_finally_delay=1, | ||||||
|  |             expect_exc='RuntimeError', | ||||||
|  |             rent_cancels=False, | ||||||
|  |             rent_ctxc_delay=0.1, | ||||||
|  |         ), | ||||||
|  |     ], | ||||||
|  |     ids=lambda item: f'chld_callspec={item!r}' | ||||||
|  | ) | ||||||
|  | def test_unmasked_remote_exc( | ||||||
|  |     debug_mode: bool, | ||||||
|  |     chld_callspec: dict, | ||||||
|  |     tpt_proto: str, | ||||||
|  | ): | ||||||
|  |     expect_exc_str: str|None = chld_callspec['sleep_n_raise'] | ||||||
|  |     rent_ctxc_delay: float|None = chld_callspec['rent_ctxc_delay'] | ||||||
|  |     async def main(): | ||||||
|  |         an: ActorNursery | ||||||
|  |         async with tractor.open_nursery( | ||||||
|  |             debug_mode=debug_mode, | ||||||
|  |             enable_transports=[tpt_proto], | ||||||
|  |         ) as an: | ||||||
|  |             ptl: Portal = await an.start_actor( | ||||||
|  |                 'cancellee', | ||||||
|  |                 enable_modules=[__name__], | ||||||
|  |             ) | ||||||
|  |             ctx: Context | ||||||
|  |             async with ( | ||||||
|  |                 ptl.open_context( | ||||||
|  |                     sleep_n_chkpt_in_finally, | ||||||
|  |                     **chld_callspec, | ||||||
|  |                 ) as (ctx, sent), | ||||||
|  |             ): | ||||||
|  |                 assert not sent | ||||||
|  |                 await trio.sleep(rent_ctxc_delay) | ||||||
|  |                 await ctx.cancel() | ||||||
|  | 
 | ||||||
|  |                 # recv error or result from chld | ||||||
|  |                 ctxc: ContextCancelled = await ctx.wait_for_result() | ||||||
|  |                 assert ( | ||||||
|  |                     ctxc is ctx.outcome | ||||||
|  |                     and | ||||||
|  |                     isinstance(ctxc, ContextCancelled) | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             # always graceful terminate the sub in non-error cases | ||||||
|  |             await an.cancel() | ||||||
|  | 
 | ||||||
|  |     if expect_exc_str: | ||||||
|  |         expect_exc: BaseException = tractor._exceptions.get_err_type( | ||||||
|  |             type_name=expect_exc_str, | ||||||
|  |         ) | ||||||
|  |         with pytest.raises( | ||||||
|  |             expected_exception=tractor.RemoteActorError, | ||||||
|  |         ) as excinfo: | ||||||
|  |             trio.run(main) | ||||||
|  | 
 | ||||||
|  |         rae = excinfo.value | ||||||
|  |         assert expect_exc == rae.boxed_type | ||||||
|  | 
 | ||||||
|  |     else: | ||||||
|  |         trio.run(main) | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| ''' | ''' | ||||||
| Async context manager cache api testing: ``trionics.maybe_open_context():`` | Suites for our `.trionics.maybe_open_context()` multi-task | ||||||
|  | shared-cached `@acm` API. | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| from contextlib import asynccontextmanager as acm | from contextlib import asynccontextmanager as acm | ||||||
|  | @ -9,6 +10,15 @@ from typing import Awaitable | ||||||
| import pytest | import pytest | ||||||
| import trio | import trio | ||||||
| import tractor | import tractor | ||||||
|  | from tractor.trionics import ( | ||||||
|  |     maybe_open_context, | ||||||
|  | ) | ||||||
|  | from tractor.log import ( | ||||||
|  |     get_console_log, | ||||||
|  |     get_logger, | ||||||
|  | ) | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| _resource: int = 0 | _resource: int = 0 | ||||||
|  | @ -52,7 +62,7 @@ def test_resource_only_entered_once(key_on): | ||||||
|                 # different task names per task will be used |                 # different task names per task will be used | ||||||
|                 kwargs = {'task_name': name} |                 kwargs = {'task_name': name} | ||||||
| 
 | 
 | ||||||
|             async with tractor.trionics.maybe_open_context( |             async with maybe_open_context( | ||||||
|                 maybe_increment_counter, |                 maybe_increment_counter, | ||||||
|                 kwargs=kwargs, |                 kwargs=kwargs, | ||||||
|                 key=key, |                 key=key, | ||||||
|  | @ -72,11 +82,13 @@ def test_resource_only_entered_once(key_on): | ||||||
|         with trio.move_on_after(0.5): |         with trio.move_on_after(0.5): | ||||||
|             async with ( |             async with ( | ||||||
|                 tractor.open_root_actor(), |                 tractor.open_root_actor(), | ||||||
|                 trio.open_nursery() as n, |                 trio.open_nursery() as tn, | ||||||
|             ): |             ): | ||||||
| 
 |  | ||||||
|                 for i in range(10): |                 for i in range(10): | ||||||
|                     n.start_soon(enter_cached_mngr, f'task_{i}') |                     tn.start_soon( | ||||||
|  |                         enter_cached_mngr, | ||||||
|  |                         f'task_{i}', | ||||||
|  |                     ) | ||||||
|                     await trio.sleep(0.001) |                     await trio.sleep(0.001) | ||||||
| 
 | 
 | ||||||
|     trio.run(main) |     trio.run(main) | ||||||
|  | @ -98,23 +110,34 @@ async def streamer( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @acm | @acm | ||||||
| async def open_stream() -> Awaitable[tractor.MsgStream]: | async def open_stream() -> Awaitable[ | ||||||
| 
 |     tuple[ | ||||||
|  |         tractor.ActorNursery, | ||||||
|  |         tractor.MsgStream, | ||||||
|  |     ] | ||||||
|  | ]: | ||||||
|     try: |     try: | ||||||
|         async with tractor.open_nursery() as an: |         async with tractor.open_nursery() as an: | ||||||
|             portal = await an.start_actor( |             portal = await an.start_actor( | ||||||
|                 'streamer', |                 'streamer', | ||||||
|                 enable_modules=[__name__], |                 enable_modules=[__name__], | ||||||
|             ) |             ) | ||||||
|             async with ( |             try: | ||||||
|                 portal.open_context(streamer) as (ctx, first), |                 async with ( | ||||||
|                 ctx.open_stream() as stream, |                     portal.open_context(streamer) as (ctx, first), | ||||||
|             ): |                     ctx.open_stream() as stream, | ||||||
|                 yield stream |                 ): | ||||||
|  |                     print('Entered open_stream() caller') | ||||||
|  |                     yield an, stream | ||||||
|  |                     print('Exited open_stream() caller') | ||||||
| 
 | 
 | ||||||
|             print('Cancelling streamer') |             finally: | ||||||
|             await portal.cancel_actor() |                 print( | ||||||
|             print('Cancelled streamer') |                     'Cancelling streamer with,\n' | ||||||
|  |                     '=> `Portal.cancel_actor()`' | ||||||
|  |                 ) | ||||||
|  |                 await portal.cancel_actor() | ||||||
|  |                 print('Cancelled streamer') | ||||||
| 
 | 
 | ||||||
|     except Exception as err: |     except Exception as err: | ||||||
|         print( |         print( | ||||||
|  | @ -127,11 +150,15 @@ async def open_stream() -> Awaitable[tractor.MsgStream]: | ||||||
| 
 | 
 | ||||||
| @acm | @acm | ||||||
| async def maybe_open_stream(taskname: str): | async def maybe_open_stream(taskname: str): | ||||||
|     async with tractor.trionics.maybe_open_context( |     async with maybe_open_context( | ||||||
|         # NOTE: all secondary tasks should cache hit on the same key |         # NOTE: all secondary tasks should cache hit on the same key | ||||||
|         acm_func=open_stream, |         acm_func=open_stream, | ||||||
|     ) as (cache_hit, stream): |     ) as ( | ||||||
| 
 |         cache_hit, | ||||||
|  |         (an, stream) | ||||||
|  |     ): | ||||||
|  |         # when the actor + portal + ctx + stream has already been | ||||||
|  |         # allocated we want to just bcast to this task. | ||||||
|         if cache_hit: |         if cache_hit: | ||||||
|             print(f'{taskname} loaded from cache') |             print(f'{taskname} loaded from cache') | ||||||
| 
 | 
 | ||||||
|  | @ -139,10 +166,43 @@ async def maybe_open_stream(taskname: str): | ||||||
|             # if this feed is already allocated by the first |             # if this feed is already allocated by the first | ||||||
|             # task that entereed |             # task that entereed | ||||||
|             async with stream.subscribe() as bstream: |             async with stream.subscribe() as bstream: | ||||||
|                 yield bstream |                 yield an, bstream | ||||||
|  |                 print( | ||||||
|  |                     f'cached task exited\n' | ||||||
|  |                     f')>\n' | ||||||
|  |                     f' |_{taskname}\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             # we should always unreg the "cloned" bcrc for this | ||||||
|  |             # consumer-task | ||||||
|  |             assert id(bstream) not in bstream._state.subs | ||||||
|  | 
 | ||||||
|         else: |         else: | ||||||
|             # yield the actual stream |             # yield the actual stream | ||||||
|             yield stream |             try: | ||||||
|  |                 yield an, stream | ||||||
|  |             finally: | ||||||
|  |                 print( | ||||||
|  |                     f'NON-cached task exited\n' | ||||||
|  |                     f')>\n' | ||||||
|  |                     f' |_{taskname}\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |         first_bstream = stream._broadcaster | ||||||
|  |         bcrx_state = first_bstream._state | ||||||
|  |         subs: dict[int, int] = bcrx_state.subs | ||||||
|  |         if len(subs) == 1: | ||||||
|  |             assert id(first_bstream) in subs | ||||||
|  |             # ^^TODO! the bcrx should always de-allocate all subs, | ||||||
|  |             # including the implicit first one allocated on entry | ||||||
|  |             # by the first subscribing peer task, no? | ||||||
|  |             # | ||||||
|  |             # -[ ] adjust `MsgStream.subscribe()` to do this mgmt! | ||||||
|  |             #  |_ allows reverting `MsgStream.receive()` to the | ||||||
|  |             #    non-bcaster method. | ||||||
|  |             #  |_ we can decide whether to reset `._broadcaster`? | ||||||
|  |             # | ||||||
|  |             # await tractor.pause(shield=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def test_open_local_sub_to_stream( | def test_open_local_sub_to_stream( | ||||||
|  | @ -159,16 +219,24 @@ def test_open_local_sub_to_stream( | ||||||
| 
 | 
 | ||||||
|     if debug_mode: |     if debug_mode: | ||||||
|         timeout = 999 |         timeout = 999 | ||||||
|  |         print(f'IN debug_mode, setting large timeout={timeout!r}..') | ||||||
| 
 | 
 | ||||||
|     async def main(): |     async def main(): | ||||||
| 
 | 
 | ||||||
|         full = list(range(1000)) |         full = list(range(1000)) | ||||||
|  |         an: tractor.ActorNursery|None = None | ||||||
|  |         num_tasks: int = 10 | ||||||
| 
 | 
 | ||||||
|         async def get_sub_and_pull(taskname: str): |         async def get_sub_and_pull(taskname: str): | ||||||
| 
 | 
 | ||||||
|  |             nonlocal an | ||||||
|  | 
 | ||||||
|             stream: tractor.MsgStream |             stream: tractor.MsgStream | ||||||
|             async with ( |             async with ( | ||||||
|                 maybe_open_stream(taskname) as stream, |                 maybe_open_stream(taskname) as ( | ||||||
|  |                     an, | ||||||
|  |                     stream, | ||||||
|  |                 ), | ||||||
|             ): |             ): | ||||||
|                 if '0' in taskname: |                 if '0' in taskname: | ||||||
|                     assert isinstance(stream, tractor.MsgStream) |                     assert isinstance(stream, tractor.MsgStream) | ||||||
|  | @ -180,34 +248,159 @@ def test_open_local_sub_to_stream( | ||||||
| 
 | 
 | ||||||
|                 first = await stream.receive() |                 first = await stream.receive() | ||||||
|                 print(f'{taskname} started with value {first}') |                 print(f'{taskname} started with value {first}') | ||||||
|                 seq = [] |                 seq: list[int] = [] | ||||||
|                 async for msg in stream: |                 async for msg in stream: | ||||||
|                     seq.append(msg) |                     seq.append(msg) | ||||||
| 
 | 
 | ||||||
|                 assert set(seq).issubset(set(full)) |                 assert set(seq).issubset(set(full)) | ||||||
|  | 
 | ||||||
|  |             # end of @acm block | ||||||
|             print(f'{taskname} finished') |             print(f'{taskname} finished') | ||||||
| 
 | 
 | ||||||
|  |         root: tractor.Actor | ||||||
|         with trio.fail_after(timeout) as cs: |         with trio.fail_after(timeout) as cs: | ||||||
|             # TODO: turns out this isn't multi-task entrant XD |             # TODO: turns out this isn't multi-task entrant XD | ||||||
|             # We probably need an indepotent entry semantic? |             # We probably need an indepotent entry semantic? | ||||||
|             async with tractor.open_root_actor( |             async with tractor.open_root_actor( | ||||||
|                 debug_mode=debug_mode, |                 debug_mode=debug_mode, | ||||||
|             ): |                 # maybe_enable_greenback=True, | ||||||
|  |                 # | ||||||
|  |                 # ^TODO? doesn't seem to mk breakpoint() usage work | ||||||
|  |                 # bc each bg task needs to open a portal?? | ||||||
|  |                 # - [ ] we should consider making this part of | ||||||
|  |                 #      our taskman defaults? | ||||||
|  |                 #   |_see https://github.com/goodboy/tractor/pull/363 | ||||||
|  |                 # | ||||||
|  |             ) as root: | ||||||
|  |                 assert root.is_registrar | ||||||
|  | 
 | ||||||
|                 async with ( |                 async with ( | ||||||
|                     trio.open_nursery() as tn, |                     trio.open_nursery() as tn, | ||||||
|                 ): |                 ): | ||||||
|                     for i in range(10): |                     for i in range(num_tasks): | ||||||
|                         tn.start_soon( |                         tn.start_soon( | ||||||
|                             get_sub_and_pull, |                             get_sub_and_pull, | ||||||
|                             f'task_{i}', |                             f'task_{i}', | ||||||
|                         ) |                         ) | ||||||
|                         await trio.sleep(0.001) |                         await trio.sleep(0.001) | ||||||
| 
 | 
 | ||||||
|                 print('all consumer tasks finished') |                 print('all consumer tasks finished!') | ||||||
|  | 
 | ||||||
|  |                 # ?XXX, ensure actor-nursery is shutdown or we might | ||||||
|  |                 # hang here due to a minor task deadlock/race-condition? | ||||||
|  |                 # | ||||||
|  |                 # - seems that all we need is a checkpoint to ensure | ||||||
|  |                 #   the last suspended task, which is inside | ||||||
|  |                 #   `.maybe_open_context()`, can do the | ||||||
|  |                 #   `Portal.cancel_actor()` call? | ||||||
|  |                 # | ||||||
|  |                 # - if that bg task isn't resumed, then this blocks | ||||||
|  |                 #   timeout might hit before that? | ||||||
|  |                 # | ||||||
|  |                 if root.ipc_server.has_peers(): | ||||||
|  |                     await trio.lowlevel.checkpoint() | ||||||
|  | 
 | ||||||
|  |                     # alt approach, cancel the entire `an` | ||||||
|  |                     # await tractor.pause() | ||||||
|  |                     # await an.cancel() | ||||||
|  | 
 | ||||||
|  |             # end of runtime scope | ||||||
|  |             print('root actor terminated.') | ||||||
| 
 | 
 | ||||||
|         if cs.cancelled_caught: |         if cs.cancelled_caught: | ||||||
|             pytest.fail( |             pytest.fail( | ||||||
|                 'Should NOT time out in `open_root_actor()` ?' |                 'Should NOT time out in `open_root_actor()` ?' | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  |         print('exiting main.') | ||||||
|  | 
 | ||||||
|  |     trio.run(main) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @acm | ||||||
|  | async def cancel_outer_cs( | ||||||
|  |     cs: trio.CancelScope|None = None, | ||||||
|  |     delay: float = 0, | ||||||
|  | ): | ||||||
|  |     # on first task delay this enough to block | ||||||
|  |     # the 2nd task but then cancel it mid sleep | ||||||
|  |     # so that the tn.start() inside the key-err handler block | ||||||
|  |     # is cancelled and would previously corrupt the | ||||||
|  |     # mutext state. | ||||||
|  |     log.info(f'task entering sleep({delay})') | ||||||
|  |     await trio.sleep(delay) | ||||||
|  |     if cs: | ||||||
|  |         log.info('task calling cs.cancel()') | ||||||
|  |         cs.cancel() | ||||||
|  |     trio.lowlevel.checkpoint() | ||||||
|  |     yield | ||||||
|  |     await trio.sleep_forever() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_lock_not_corrupted_on_fast_cancel( | ||||||
|  |     debug_mode: bool, | ||||||
|  |     loglevel: str, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Verify that if the caching-task (the first to enter | ||||||
|  |     `maybe_open_context()`) is cancelled mid-cache-miss, the embedded | ||||||
|  |     mutex can never be left in a corrupted state. | ||||||
|  | 
 | ||||||
|  |     That is, the lock is always eventually released ensuring a peer | ||||||
|  |     (cache-hitting) task will never, | ||||||
|  | 
 | ||||||
|  |     - be left to inf-block/hang on the `lock.acquire()`. | ||||||
|  |     - try to release the lock when still owned by the caching-task | ||||||
|  |       due to it having erronously exited without calling | ||||||
|  |       `lock.release()`. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     delay: float = 1. | ||||||
|  | 
 | ||||||
|  |     async def use_moc( | ||||||
|  |         cs: trio.CancelScope|None, | ||||||
|  |         delay: float, | ||||||
|  |     ): | ||||||
|  |         log.info('task entering moc') | ||||||
|  |         async with maybe_open_context( | ||||||
|  |             cancel_outer_cs, | ||||||
|  |             kwargs={ | ||||||
|  |                 'cs': cs, | ||||||
|  |                 'delay': delay, | ||||||
|  |             }, | ||||||
|  |         ) as (cache_hit, _null): | ||||||
|  |             if cache_hit: | ||||||
|  |                 log.info('2nd task entered') | ||||||
|  |             else: | ||||||
|  |                 log.info('1st task entered') | ||||||
|  | 
 | ||||||
|  |             await trio.sleep_forever() | ||||||
|  | 
 | ||||||
|  |     async def main(): | ||||||
|  |         with trio.fail_after(delay + 2): | ||||||
|  |             async with ( | ||||||
|  |                 tractor.open_root_actor( | ||||||
|  |                     debug_mode=debug_mode, | ||||||
|  |                     loglevel=loglevel, | ||||||
|  |                 ), | ||||||
|  |                 trio.open_nursery() as tn, | ||||||
|  |             ): | ||||||
|  |                 get_console_log('info') | ||||||
|  |                 log.info('yo starting') | ||||||
|  |                 cs = tn.cancel_scope | ||||||
|  |                 tn.start_soon( | ||||||
|  |                     use_moc, | ||||||
|  |                     cs, | ||||||
|  |                     delay, | ||||||
|  |                     name='child', | ||||||
|  |                 ) | ||||||
|  |                 with trio.CancelScope() as rent_cs: | ||||||
|  |                     await use_moc( | ||||||
|  |                         cs=rent_cs, | ||||||
|  |                         delay=delay, | ||||||
|  |                     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     trio.run(main) |     trio.run(main) | ||||||
|  |  | ||||||
|  | @ -147,8 +147,7 @@ def test_trio_prestarted_task_bubbles( | ||||||
|         await trio.sleep_forever() |         await trio.sleep_forever() | ||||||
| 
 | 
 | ||||||
|     async def _trio_main(): |     async def _trio_main(): | ||||||
|         # with trio.fail_after(2): |         with trio.fail_after(2 if not debug_mode else 999): | ||||||
|         with trio.fail_after(999): |  | ||||||
|             first: str |             first: str | ||||||
|             chan: to_asyncio.LinkedTaskChannel |             chan: to_asyncio.LinkedTaskChannel | ||||||
|             aio_ev = asyncio.Event() |             aio_ev = asyncio.Event() | ||||||
|  | @ -217,32 +216,25 @@ def test_trio_prestarted_task_bubbles( | ||||||
|                         ): |                         ): | ||||||
|                             aio_ev.set() |                             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 |     # ensure the trio-task's error bubbled despite the aio-side | ||||||
|     # having (maybe) errored first. |     # having (maybe) errored first. | ||||||
|     if aio_err_trigger in ( |     if aio_err_trigger in ( | ||||||
|         'after_trio_task_starts', |         'after_trio_task_starts', | ||||||
|         'after_start_point', |         'after_start_point', | ||||||
|     ): |     ): | ||||||
|         assert len(errs := rest_eg.exceptions) == 1 |         patt: str = 'trio-side' | ||||||
|         typerr = errs[0] |         expect_exc = TypeError | ||||||
|         assert ( |  | ||||||
|             type(typerr) is TypeError |  | ||||||
|             and |  | ||||||
|             'trio-side' in typerr.args |  | ||||||
|         ) |  | ||||||
| 
 | 
 | ||||||
|     # when aio errors BEFORE (last) trio task is scheduled, we should |     # when aio errors BEFORE (last) trio task is scheduled, we should | ||||||
|     # never see anythinb but the aio-side. |     # never see anythinb but the aio-side. | ||||||
|     else: |     else: | ||||||
|         assert len(rtes := rte_eg.exceptions) == 1 |         patt: str = 'asyncio-side' | ||||||
|         assert 'asyncio-side' in rtes[0].args[0] |         expect_exc = RuntimeError | ||||||
|  | 
 | ||||||
|  |     with pytest.raises(expect_exc) as excinfo: | ||||||
|  |         tractor.to_asyncio.run_as_asyncio_guest( | ||||||
|  |             trio_main=_trio_main, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     caught_exc = excinfo.value | ||||||
|  |     assert patt in caught_exc.args | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ from contextlib import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| import pytest | import pytest | ||||||
|  | from tractor.trionics import collapse_eg | ||||||
| import trio | import trio | ||||||
| from trio import TaskStatus | from trio import TaskStatus | ||||||
| 
 | 
 | ||||||
|  | @ -64,9 +65,8 @@ def test_stashed_child_nursery(use_start_soon): | ||||||
|     async def main(): |     async def main(): | ||||||
| 
 | 
 | ||||||
|         async with ( |         async with ( | ||||||
|             trio.open_nursery( |             collapse_eg(), | ||||||
|                 strict_exception_groups=False, |             trio.open_nursery() as pn, | ||||||
|             ) as pn, |  | ||||||
|         ): |         ): | ||||||
|             cn = await pn.start(mk_child_nursery) |             cn = await pn.start(mk_child_nursery) | ||||||
|             assert cn |             assert cn | ||||||
|  | @ -112,55 +112,11 @@ def test_acm_embedded_nursery_propagates_enter_err( | ||||||
|     ''' |     ''' | ||||||
|     import tractor |     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 |     @acm | ||||||
|     async def wraps_tn_that_always_cancels(): |     async def wraps_tn_that_always_cancels(): | ||||||
|         async with ( |         async with ( | ||||||
|             trio.open_nursery() as tn, |             trio.open_nursery() as tn, | ||||||
|             maybe_raise_from_masking_exc( |             tractor.trionics.maybe_raise_from_masking_exc( | ||||||
|                 tn=tn, |                 tn=tn, | ||||||
|                 unmask_from=( |                 unmask_from=( | ||||||
|                     trio.Cancelled |                     trio.Cancelled | ||||||
|  | @ -202,3 +158,58 @@ def test_acm_embedded_nursery_propagates_enter_err( | ||||||
|     assert_eg, rest_eg = eg.split(AssertionError) |     assert_eg, rest_eg = eg.split(AssertionError) | ||||||
| 
 | 
 | ||||||
|     assert len(assert_eg.exceptions) == 1 |     assert len(assert_eg.exceptions) == 1 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_gatherctxs_with_memchan_breaks_multicancelled( | ||||||
|  |     debug_mode: bool, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Demo how a using an `async with sndchan` inside a `.trionics.gather_contexts()` task | ||||||
|  |     will break a strict-eg-tn's multi-cancelled absorption.. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     from tractor import ( | ||||||
|  |         trionics, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     @acm | ||||||
|  |     async def open_memchan() -> trio.abc.ReceiveChannel: | ||||||
|  | 
 | ||||||
|  |         task: trio.Task = trio.lowlevel.current_task() | ||||||
|  |         print( | ||||||
|  |             f'Opening {task!r}\n' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # 1 to force eager sending | ||||||
|  |         send, recv = trio.open_memory_channel(16) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             async with send: | ||||||
|  |                 yield recv | ||||||
|  |         finally: | ||||||
|  |             print( | ||||||
|  |                 f'Closed {task!r}\n' | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     async def main(): | ||||||
|  |         async with ( | ||||||
|  |             # XXX should ensure ONLY the KBI | ||||||
|  |             # is relayed upward | ||||||
|  |             collapse_eg(), | ||||||
|  |             trio.open_nursery(), # as tn, | ||||||
|  | 
 | ||||||
|  |             trionics.gather_contexts([ | ||||||
|  |                 open_memchan(), | ||||||
|  |                 open_memchan(), | ||||||
|  |             ]) as recv_chans, | ||||||
|  |         ): | ||||||
|  |             assert len(recv_chans) == 2 | ||||||
|  | 
 | ||||||
|  |             await trio.sleep(1) | ||||||
|  |             raise KeyboardInterrupt | ||||||
|  |             # tn.cancel_scope.cancel() | ||||||
|  | 
 | ||||||
|  |     with pytest.raises(KeyboardInterrupt): | ||||||
|  |         trio.run(main) | ||||||
|  |  | ||||||
|  | @ -55,10 +55,17 @@ async def open_actor_cluster( | ||||||
|         raise ValueError( |         raise ValueError( | ||||||
|             'Number of names is {len(names)} but count it {count}') |             'Number of names is {len(names)} but count it {count}') | ||||||
| 
 | 
 | ||||||
|     async with tractor.open_nursery( |     async with ( | ||||||
|         **runtime_kwargs, |         # tractor.trionics.collapse_eg(), | ||||||
|     ) as an: |         tractor.open_nursery( | ||||||
|         async with trio.open_nursery() as n: |             **runtime_kwargs, | ||||||
|  |         ) as an | ||||||
|  |     ): | ||||||
|  |         async with ( | ||||||
|  |             # tractor.trionics.collapse_eg(), | ||||||
|  |             trio.open_nursery() as tn, | ||||||
|  |             tractor.trionics.maybe_raise_from_masking_exc() | ||||||
|  |         ): | ||||||
|             uid = tractor.current_actor().uid |             uid = tractor.current_actor().uid | ||||||
| 
 | 
 | ||||||
|             async def _start(name: str) -> None: |             async def _start(name: str) -> None: | ||||||
|  | @ -69,9 +76,8 @@ async def open_actor_cluster( | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|             for name in names: |             for name in names: | ||||||
|                 n.start_soon(_start, name) |                 tn.start_soon(_start, name) | ||||||
| 
 | 
 | ||||||
|         assert len(portals) == count |         assert len(portals) == count | ||||||
|         yield portals |         yield portals | ||||||
| 
 |  | ||||||
|         await an.cancel(hard_kill=hard_kill) |         await an.cancel(hard_kill=hard_kill) | ||||||
|  |  | ||||||
|  | @ -101,6 +101,9 @@ from ._state import ( | ||||||
|     debug_mode, |     debug_mode, | ||||||
|     _ctxvar_Context, |     _ctxvar_Context, | ||||||
| ) | ) | ||||||
|  | from .trionics import ( | ||||||
|  |     collapse_eg, | ||||||
|  | ) | ||||||
| # ------ - ------ | # ------ - ------ | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from ._portal import Portal |     from ._portal import Portal | ||||||
|  | @ -151,7 +154,7 @@ class Context: | ||||||
|     2 cancel-scope-linked, communicating and parallel executing |     2 cancel-scope-linked, communicating and parallel executing | ||||||
|     `Task`s. Contexts are allocated on each side of any task |     `Task`s. Contexts are allocated on each side of any task | ||||||
|     RPC-linked msg dialog, i.e. for every request to a remote |     RPC-linked msg dialog, i.e. for every request to a remote | ||||||
|     actor from a `Portal`. On the "callee" side a context is |     actor from a `Portal`. On the "child" side a context is | ||||||
|     always allocated inside `._rpc._invoke()`. |     always allocated inside `._rpc._invoke()`. | ||||||
| 
 | 
 | ||||||
|     TODO: more detailed writeup on cancellation, error and |     TODO: more detailed writeup on cancellation, error and | ||||||
|  | @ -219,8 +222,8 @@ class Context: | ||||||
|     # `._runtime.invoke()`. |     # `._runtime.invoke()`. | ||||||
|     _remote_func_type: str | None = None |     _remote_func_type: str | None = None | ||||||
| 
 | 
 | ||||||
|     # NOTE: (for now) only set (a portal) on the caller side since |     # NOTE: (for now) only set (a portal) on the parent side since | ||||||
|     # the callee doesn't generally need a ref to one and should |     # the child doesn't generally need a ref to one and should | ||||||
|     # normally need to explicitly ask for handle to its peer if |     # normally need to explicitly ask for handle to its peer if | ||||||
|     # more the the `Context` is needed? |     # more the the `Context` is needed? | ||||||
|     _portal: Portal | None = None |     _portal: Portal | None = None | ||||||
|  | @ -249,12 +252,12 @@ class Context: | ||||||
|     _outcome_msg: Return|Error|ContextCancelled = Unresolved |     _outcome_msg: Return|Error|ContextCancelled = Unresolved | ||||||
| 
 | 
 | ||||||
|     # on a clean exit there should be a final value |     # on a clean exit there should be a final value | ||||||
|     # delivered from the far end "callee" task, so |     # delivered from the far end "child" task, so | ||||||
|     # this value is only set on one side. |     # this value is only set on one side. | ||||||
|     # _result: Any | int = None |     # _result: Any | int = None | ||||||
|     _result: PayloadT|Unresolved = Unresolved |     _result: PayloadT|Unresolved = Unresolved | ||||||
| 
 | 
 | ||||||
|     # if the local "caller"  task errors this value is always set |     # if the local "parent"  task errors this value is always set | ||||||
|     # to the error that was captured in the |     # to the error that was captured in the | ||||||
|     # `Portal.open_context().__aexit__()` teardown block OR, in |     # `Portal.open_context().__aexit__()` teardown block OR, in | ||||||
|     # 2 special cases when an (maybe) expected remote error |     # 2 special cases when an (maybe) expected remote error | ||||||
|  | @ -290,9 +293,9 @@ class Context: | ||||||
|     # a `ContextCancelled` due to a call to `.cancel()` triggering |     # a `ContextCancelled` due to a call to `.cancel()` triggering | ||||||
|     # "graceful closure" on either side: |     # "graceful closure" on either side: | ||||||
|     # - `._runtime._invoke()` will check this flag before engaging |     # - `._runtime._invoke()` will check this flag before engaging | ||||||
|     #   the crash handler REPL in such cases where the "callee" |     #   the crash handler REPL in such cases where the "child" | ||||||
|     #   raises the cancellation, |     #   raises the cancellation, | ||||||
|     # - `.devx._debug.lock_stdio_for_peer()` will set it to `False` if |     # - `.devx.debug.lock_stdio_for_peer()` will set it to `False` if | ||||||
|     #   the global tty-lock has been configured to filter out some |     #   the global tty-lock has been configured to filter out some | ||||||
|     #   actors from being able to acquire the debugger lock. |     #   actors from being able to acquire the debugger lock. | ||||||
|     _enter_debugger_on_cancel: bool = True |     _enter_debugger_on_cancel: bool = True | ||||||
|  | @ -304,8 +307,8 @@ class Context: | ||||||
|     _stream_opened: bool = False |     _stream_opened: bool = False | ||||||
|     _stream: MsgStream|None = None |     _stream: MsgStream|None = None | ||||||
| 
 | 
 | ||||||
|     # caller of `Portal.open_context()` for |     # the parent-task's calling-fn's frame-info, the frame above | ||||||
|     # logging purposes mostly |     # `Portal.open_context()`, for introspection/logging. | ||||||
|     _caller_info: CallerInfo|None = None |     _caller_info: CallerInfo|None = None | ||||||
| 
 | 
 | ||||||
|     # overrun handling machinery |     # overrun handling machinery | ||||||
|  | @ -526,11 +529,11 @@ class Context: | ||||||
|         ''' |         ''' | ||||||
|         Exactly the value of `self._scope.cancelled_caught` |         Exactly the value of `self._scope.cancelled_caught` | ||||||
|         (delegation) and should only be (able to be read as) |         (delegation) and should only be (able to be read as) | ||||||
|         `True` for a `.side == "caller"` ctx wherein the |         `True` for a `.side == "parent"` ctx wherein the | ||||||
|         `Portal.open_context()` block was exited due to a call to |         `Portal.open_context()` block was exited due to a call to | ||||||
|         `._scope.cancel()` - which should only ocurr in 2 cases: |         `._scope.cancel()` - which should only ocurr in 2 cases: | ||||||
| 
 | 
 | ||||||
|         - a caller side calls `.cancel()`, the far side cancels |         - a parent side calls `.cancel()`, the far side cancels | ||||||
|           and delivers back a `ContextCancelled` (making |           and delivers back a `ContextCancelled` (making | ||||||
|           `.cancel_acked == True`) and `._scope.cancel()` is |           `.cancel_acked == True`) and `._scope.cancel()` is | ||||||
|           called by `._maybe_cancel_and_set_remote_error()` which |           called by `._maybe_cancel_and_set_remote_error()` which | ||||||
|  | @ -539,20 +542,20 @@ class Context: | ||||||
|           => `._scope.cancelled_caught == True` by normal `trio` |           => `._scope.cancelled_caught == True` by normal `trio` | ||||||
|           cs semantics. |           cs semantics. | ||||||
| 
 | 
 | ||||||
|         - a caller side is delivered a `._remote_error: |         - a parent side is delivered a `._remote_error: | ||||||
|           RemoteActorError` via `._deliver_msg()` and a transitive |           RemoteActorError` via `._deliver_msg()` and a transitive | ||||||
|           call to `_maybe_cancel_and_set_remote_error()` calls |           call to `_maybe_cancel_and_set_remote_error()` calls | ||||||
|           `._scope.cancel()` and that cancellation eventually |           `._scope.cancel()` and that cancellation eventually | ||||||
|           results in `trio.Cancelled`(s) caught in the |           results in `trio.Cancelled`(s) caught in the | ||||||
|           `.open_context()` handling around the @acm's `yield`. |           `.open_context()` handling around the @acm's `yield`. | ||||||
| 
 | 
 | ||||||
|         Only as an FYI, in the "callee" side case it can also be |         Only as an FYI, in the "child" side case it can also be | ||||||
|         set but never is readable by any task outside the RPC |         set but never is readable by any task outside the RPC | ||||||
|         machinery in `._invoke()` since,: |         machinery in `._invoke()` since,: | ||||||
|         - when a callee side calls `.cancel()`, `._scope.cancel()` |         - when a child side calls `.cancel()`, `._scope.cancel()` | ||||||
|           is called immediately and handled specially inside |           is called immediately and handled specially inside | ||||||
|           `._invoke()` to raise a `ContextCancelled` which is then |           `._invoke()` to raise a `ContextCancelled` which is then | ||||||
|           sent to the caller side. |           sent to the parent side. | ||||||
| 
 | 
 | ||||||
|           However, `._scope.cancelled_caught` can NEVER be |           However, `._scope.cancelled_caught` can NEVER be | ||||||
|           accessed/read as `True` by any RPC invoked task since it |           accessed/read as `True` by any RPC invoked task since it | ||||||
|  | @ -663,7 +666,7 @@ class Context: | ||||||
|         when called/closed by actor local task(s). |         when called/closed by actor local task(s). | ||||||
| 
 | 
 | ||||||
|         NOTEs:  |         NOTEs:  | ||||||
|           - It is expected that the caller has previously unwrapped |           - It is expected that the parent has previously unwrapped | ||||||
|             the remote error using a call to `unpack_error()` and |             the remote error using a call to `unpack_error()` and | ||||||
|             provides that output exception value as the input |             provides that output exception value as the input | ||||||
|             `error` argument *here*. |             `error` argument *here*. | ||||||
|  | @ -673,7 +676,7 @@ class Context: | ||||||
|             `Portal.open_context()` (ideally) we want to interrupt |             `Portal.open_context()` (ideally) we want to interrupt | ||||||
|             any ongoing local tasks operating within that |             any ongoing local tasks operating within that | ||||||
|             `Context`'s cancel-scope so as to be notified ASAP of |             `Context`'s cancel-scope so as to be notified ASAP of | ||||||
|             the remote error and engage any caller handling (eg. |             the remote error and engage any parent handling (eg. | ||||||
|             for cross-process task supervision). |             for cross-process task supervision). | ||||||
| 
 | 
 | ||||||
|           - In some cases we may want to raise the remote error |           - In some cases we may want to raise the remote error | ||||||
|  | @ -740,6 +743,8 @@ class Context: | ||||||
|             # cancelled, NOT their reported canceller. IOW in the |             # cancelled, NOT their reported canceller. IOW in the | ||||||
|             # latter case we're cancelled by someone else getting |             # latter case we're cancelled by someone else getting | ||||||
|             # cancelled. |             # cancelled. | ||||||
|  |             # | ||||||
|  |             # !TODO, switching to `Actor.aid` here! | ||||||
|             if (canc := error.canceller) == self._actor.uid: |             if (canc := error.canceller) == self._actor.uid: | ||||||
|                 whom: str = 'us' |                 whom: str = 'us' | ||||||
|                 self._canceller = canc |                 self._canceller = canc | ||||||
|  | @ -881,6 +886,11 @@ class Context: | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def repr_caller(self) -> str: |     def repr_caller(self) -> str: | ||||||
|  |         ''' | ||||||
|  |         Render a "namespace-path" style representation of the calling | ||||||
|  |         task-fn. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|         ci: CallerInfo|None = self._caller_info |         ci: CallerInfo|None = self._caller_info | ||||||
|         if ci: |         if ci: | ||||||
|             return ( |             return ( | ||||||
|  | @ -894,7 +904,7 @@ class Context: | ||||||
|     def repr_api(self) -> str: |     def repr_api(self) -> str: | ||||||
|         return 'Portal.open_context()' |         return 'Portal.open_context()' | ||||||
| 
 | 
 | ||||||
|         # TODO: use `.dev._frame_stack` scanning to find caller! |         # TODO: use `.dev._frame_stack` scanning to find caller fn! | ||||||
|         # ci: CallerInfo|None = self._caller_info |         # ci: CallerInfo|None = self._caller_info | ||||||
|         # if ci: |         # if ci: | ||||||
|         #     return ( |         #     return ( | ||||||
|  | @ -929,7 +939,7 @@ class Context: | ||||||
|         => That is, an IPC `Context` (this) **does not** |         => That is, an IPC `Context` (this) **does not** | ||||||
|            have the same semantics as a `trio.CancelScope`. |            have the same semantics as a `trio.CancelScope`. | ||||||
| 
 | 
 | ||||||
|         If the caller (who entered the `Portal.open_context()`) |         If the parent (who entered the `Portal.open_context()`) | ||||||
|         desires that the internal block's cancel-scope  be |         desires that the internal block's cancel-scope  be | ||||||
|         cancelled it should open its own `trio.CancelScope` and |         cancelled it should open its own `trio.CancelScope` and | ||||||
|         manage it as needed. |         manage it as needed. | ||||||
|  | @ -940,7 +950,7 @@ class Context: | ||||||
|         self.cancel_called = True |         self.cancel_called = True | ||||||
| 
 | 
 | ||||||
|         header: str = ( |         header: str = ( | ||||||
|             f'Cancelling ctx from {side.upper()}-side\n' |             f'Cancelling ctx from {side!r}-side\n' | ||||||
|         ) |         ) | ||||||
|         reminfo: str = ( |         reminfo: str = ( | ||||||
|             # ' =>\n' |             # ' =>\n' | ||||||
|  | @ -948,7 +958,7 @@ class Context: | ||||||
|             f'\n' |             f'\n' | ||||||
|             f'c)=> {self.chan.uid}\n' |             f'c)=> {self.chan.uid}\n' | ||||||
|             f'   |_[{self.dst_maddr}\n' |             f'   |_[{self.dst_maddr}\n' | ||||||
|             f'     >>{self.repr_rpc}\n' |             f'     >> {self.repr_rpc}\n' | ||||||
|             # f'    >> {self._nsf}() -> {codec}[dict]:\n\n' |             # f'    >> {self._nsf}() -> {codec}[dict]:\n\n' | ||||||
|             # TODO: pull msg-type from spec re #320 |             # TODO: pull msg-type from spec re #320 | ||||||
|         ) |         ) | ||||||
|  | @ -1001,7 +1011,6 @@ class Context: | ||||||
|                 else: |                 else: | ||||||
|                     log.cancel( |                     log.cancel( | ||||||
|                         f'Timed out on cancel request of remote task?\n' |                         f'Timed out on cancel request of remote task?\n' | ||||||
|                         f'\n' |  | ||||||
|                         f'{reminfo}' |                         f'{reminfo}' | ||||||
|                     ) |                     ) | ||||||
| 
 | 
 | ||||||
|  | @ -1012,7 +1021,7 @@ class Context: | ||||||
|         # `_invoke()` RPC task. |         # `_invoke()` RPC task. | ||||||
|         # |         # | ||||||
|         # NOTE: on this side we ALWAYS cancel the local scope |         # NOTE: on this side we ALWAYS cancel the local scope | ||||||
|         # since the caller expects a `ContextCancelled` to be sent |         # since the parent expects a `ContextCancelled` to be sent | ||||||
|         # from `._runtime._invoke()` back to the other side. The |         # from `._runtime._invoke()` back to the other side. The | ||||||
|         # logic for catching the result of the below |         # logic for catching the result of the below | ||||||
|         # `._scope.cancel()` is inside the `._runtime._invoke()` |         # `._scope.cancel()` is inside the `._runtime._invoke()` | ||||||
|  | @ -1069,9 +1078,25 @@ class Context: | ||||||
|         |RemoteActorError  # stream overrun caused and ignored by us |         |RemoteActorError  # stream overrun caused and ignored by us | ||||||
|     ): |     ): | ||||||
|         ''' |         ''' | ||||||
|         Maybe raise a remote error depending on the type of error |         Maybe raise a remote error depending on the type of error and | ||||||
|         and *who* (i.e. which task from which actor) requested |         *who*, i.e. which side of the task pair across actors, | ||||||
|         a  cancellation (if any). |         requested a cancellation (if any). | ||||||
|  | 
 | ||||||
|  |         Depending on the input config-params suppress raising | ||||||
|  |         certain remote excs: | ||||||
|  | 
 | ||||||
|  |         - if `remote_error: ContextCancelled` (ctxc) AND this side's | ||||||
|  |           task is the "requester", it at somem point called | ||||||
|  |           `Context.cancel()`, then the peer's ctxc is treated | ||||||
|  |           as a "cancel ack". | ||||||
|  | 
 | ||||||
|  |          |_ this behaves exactly like how `trio.Nursery.cancel_scope` | ||||||
|  |             absorbs any `BaseExceptionGroup[trio.Cancelled]` wherein the | ||||||
|  |             owning parent task never will raise a `trio.Cancelled` | ||||||
|  |             if `CancelScope.cancel_called == True`. | ||||||
|  | 
 | ||||||
|  |         - `remote_error: StreamOverrrun` (overrun) AND | ||||||
|  |            `raise_overrun_from_self` is set. | ||||||
| 
 | 
 | ||||||
|         ''' |         ''' | ||||||
|         __tracebackhide__: bool = hide_tb |         __tracebackhide__: bool = hide_tb | ||||||
|  | @ -1113,18 +1138,19 @@ class Context: | ||||||
|             # for this ^, NO right? |             # for this ^, NO right? | ||||||
| 
 | 
 | ||||||
|         ) or ( |         ) or ( | ||||||
|             # NOTE: whenever this context is the cause of an |             # NOTE: whenever this side is the cause of an | ||||||
|             # overrun on the remote side (aka we sent msgs too |             # overrun on the peer side, i.e. we sent msgs too | ||||||
|             # fast that the remote task was overrun according |             # fast and the peer task was overrun according | ||||||
|             # to `MsgStream` buffer settings) AND the caller |             # to `MsgStream` buffer settings, AND this was | ||||||
|             # has requested to not raise overruns this side |             # called with `raise_overrun_from_self=True` (the | ||||||
|             # caused, we also silently absorb any remotely |             # default), silently absorb any `StreamOverrun`. | ||||||
|             # boxed `StreamOverrun`. This is mostly useful for |             # | ||||||
|             # supressing such faults during |             # XXX, this is namely useful for supressing such faults | ||||||
|             # cancellation/error/final-result handling inside |             # during cancellation/error/final-result handling inside | ||||||
|             # `msg._ops.drain_to_final_msg()` such that we do not |             # `.msg._ops.drain_to_final_msg()` such that we do not | ||||||
|             # raise such errors particularly in the case where |             # raise during a cancellation-request, i.e. when | ||||||
|             # `._cancel_called == True`. |             # `._cancel_called == True`. | ||||||
|  |             # | ||||||
|             not raise_overrun_from_self |             not raise_overrun_from_self | ||||||
|             and isinstance(remote_error, RemoteActorError) |             and isinstance(remote_error, RemoteActorError) | ||||||
|             and remote_error.boxed_type is StreamOverrun |             and remote_error.boxed_type is StreamOverrun | ||||||
|  | @ -1168,8 +1194,8 @@ class Context: | ||||||
| 
 | 
 | ||||||
|     ) -> Any|Exception: |     ) -> Any|Exception: | ||||||
|         ''' |         ''' | ||||||
|         From some (caller) side task, wait for and return the final |         From some (parent) side task, wait for and return the final | ||||||
|         result from the remote (callee) side's task. |         result from the remote (child) side's task. | ||||||
| 
 | 
 | ||||||
|         This provides a mechanism for one task running in some actor to wait |         This provides a mechanism for one task running in some actor to wait | ||||||
|         on another task at the other side, in some other actor, to terminate. |         on another task at the other side, in some other actor, to terminate. | ||||||
|  | @ -1234,8 +1260,8 @@ class Context: | ||||||
| 
 | 
 | ||||||
|             # ?XXX, should already be set in `._deliver_msg()` right? |             # ?XXX, should already be set in `._deliver_msg()` right? | ||||||
|             if self._outcome_msg is not Unresolved: |             if self._outcome_msg is not Unresolved: | ||||||
|                 # from .devx import _debug |                 # from .devx import debug | ||||||
|                 # await _debug.pause() |                 # await debug.pause() | ||||||
|                 assert self._outcome_msg is outcome_msg |                 assert self._outcome_msg is outcome_msg | ||||||
|             else: |             else: | ||||||
|                 self._outcome_msg = outcome_msg |                 self._outcome_msg = outcome_msg | ||||||
|  | @ -1465,6 +1491,12 @@ class Context: | ||||||
|                 ): |                 ): | ||||||
|                     status = 'peer-cancelled' |                     status = 'peer-cancelled' | ||||||
| 
 | 
 | ||||||
|  |             case ( | ||||||
|  |                 Unresolved, | ||||||
|  |                 trio.Cancelled(),  # any error-type | ||||||
|  |             ) if self.canceller: | ||||||
|  |                 status = 'actor-cancelled' | ||||||
|  | 
 | ||||||
|             # (remote) error condition |             # (remote) error condition | ||||||
|             case ( |             case ( | ||||||
|                 Unresolved, |                 Unresolved, | ||||||
|  | @ -1578,7 +1610,7 @@ class Context: | ||||||
|                 raise err |                 raise err | ||||||
| 
 | 
 | ||||||
|         # TODO: maybe a flag to by-pass encode op if already done |         # TODO: maybe a flag to by-pass encode op if already done | ||||||
|         # here in caller? |         # here in parent? | ||||||
|         await self.chan.send(started_msg) |         await self.chan.send(started_msg) | ||||||
| 
 | 
 | ||||||
|         # set msg-related internal runtime-state |         # set msg-related internal runtime-state | ||||||
|  | @ -1654,7 +1686,7 @@ class Context: | ||||||
| 
 | 
 | ||||||
|          XXX RULES XXX |          XXX RULES XXX | ||||||
|         ------ - ------ |         ------ - ------ | ||||||
|         - NEVER raise remote errors from this method; a runtime task caller. |         - NEVER raise remote errors from this method; a calling runtime-task. | ||||||
|           An error "delivered" to a ctx should always be raised by |           An error "delivered" to a ctx should always be raised by | ||||||
|           the corresponding local task operating on the |           the corresponding local task operating on the | ||||||
|           `Portal`/`Context` APIs. |           `Portal`/`Context` APIs. | ||||||
|  | @ -1730,7 +1762,7 @@ class Context: | ||||||
| 
 | 
 | ||||||
|             else: |             else: | ||||||
|                 report = ( |                 report = ( | ||||||
|                     'Queueing OVERRUN msg on caller task:\n\n' |                     'Queueing OVERRUN msg on parent task:\n\n' | ||||||
|                     + report |                     + report | ||||||
|                 ) |                 ) | ||||||
|                 log.debug(report) |                 log.debug(report) | ||||||
|  | @ -1926,12 +1958,12 @@ async def open_context_from_portal( | ||||||
|     IPC protocol. |     IPC protocol. | ||||||
| 
 | 
 | ||||||
|     The yielded `tuple` is a pair delivering a `tractor.Context` |     The yielded `tuple` is a pair delivering a `tractor.Context` | ||||||
|     and any first value "sent" by the "callee" task via a call |     and any first value "sent" by the "child" task via a call | ||||||
|     to `Context.started(<value: Any>)`; this side of the |     to `Context.started(<value: Any>)`; this side of the | ||||||
|     context does not unblock until the "callee" task calls |     context does not unblock until the "child" task calls | ||||||
|     `.started()` in similar style to `trio.Nursery.start()`. |     `.started()` in similar style to `trio.Nursery.start()`. | ||||||
|     When the "callee" (side that is "called"/started by a call |     When the "child" (side that is "called"/started by a call | ||||||
|     to *this* method) returns, the caller side (this) unblocks |     to *this* method) returns, the parent side (this) unblocks | ||||||
|     and any final value delivered from the other end can be |     and any final value delivered from the other end can be | ||||||
|     retrieved using the `Contex.wait_for_result()` api. |     retrieved using the `Contex.wait_for_result()` api. | ||||||
| 
 | 
 | ||||||
|  | @ -1944,7 +1976,7 @@ async def open_context_from_portal( | ||||||
|     __tracebackhide__: bool = hide_tb |     __tracebackhide__: bool = hide_tb | ||||||
| 
 | 
 | ||||||
|     # denote this frame as a "runtime frame" for stack |     # denote this frame as a "runtime frame" for stack | ||||||
|     # introspection where we report the caller code in logging |     # introspection where we report the parent code in logging | ||||||
|     # and error message content. |     # and error message content. | ||||||
|     # NOTE: 2 bc of the wrapping `@acm` |     # NOTE: 2 bc of the wrapping `@acm` | ||||||
|     __runtimeframe__: int = 2  # noqa |     __runtimeframe__: int = 2  # noqa | ||||||
|  | @ -2003,13 +2035,11 @@ async def open_context_from_portal( | ||||||
|     # placeholder for any exception raised in the runtime |     # placeholder for any exception raised in the runtime | ||||||
|     # or by user tasks which cause this context's closure. |     # or by user tasks which cause this context's closure. | ||||||
|     scope_err: BaseException|None = None |     scope_err: BaseException|None = None | ||||||
|     ctxc_from_callee: ContextCancelled|None = None |     ctxc_from_child: ContextCancelled|None = None | ||||||
|     try: |     try: | ||||||
|         async with ( |         async with ( | ||||||
|             trio.open_nursery( |             collapse_eg(), | ||||||
|                 strict_exception_groups=False, |             trio.open_nursery() as tn, | ||||||
|             ) as tn, |  | ||||||
| 
 |  | ||||||
|             msgops.maybe_limit_plds( |             msgops.maybe_limit_plds( | ||||||
|                 ctx=ctx, |                 ctx=ctx, | ||||||
|                 spec=ctx_meta.get('pld_spec'), |                 spec=ctx_meta.get('pld_spec'), | ||||||
|  | @ -2084,7 +2114,7 @@ async def open_context_from_portal( | ||||||
|             # that we can re-use it around the `yield` ^ here |             # that we can re-use it around the `yield` ^ here | ||||||
|             # or vice versa? |             # or vice versa? | ||||||
|             # |             # | ||||||
|             # maybe TODO NOTE: between the caller exiting and |             # maybe TODO NOTE: between the parent exiting and | ||||||
|             # arriving here the far end may have sent a ctxc-msg or |             # arriving here the far end may have sent a ctxc-msg or | ||||||
|             # other error, so the quetion is whether we should check |             # other error, so the quetion is whether we should check | ||||||
|             # for it here immediately and maybe raise so as to engage |             # for it here immediately and maybe raise so as to engage | ||||||
|  | @ -2150,16 +2180,16 @@ async def open_context_from_portal( | ||||||
|     #   request in which case we DO let the error bubble to the |     #   request in which case we DO let the error bubble to the | ||||||
|     #   opener. |     #   opener. | ||||||
|     # |     # | ||||||
|     # 2-THIS "caller" task somewhere invoked `Context.cancel()` |     # 2-THIS "parent" task somewhere invoked `Context.cancel()` | ||||||
|     #   and received a `ContextCanclled` from the "callee" |     #   and received a `ContextCanclled` from the "child" | ||||||
|     #   task, in which case we mask the `ContextCancelled` from |     #   task, in which case we mask the `ContextCancelled` from | ||||||
|     #   bubbling to this "caller" (much like how `trio.Nursery` |     #   bubbling to this "parent" (much like how `trio.Nursery` | ||||||
|     #   swallows any `trio.Cancelled` bubbled by a call to |     #   swallows any `trio.Cancelled` bubbled by a call to | ||||||
|     #   `Nursery.cancel_scope.cancel()`) |     #   `Nursery.cancel_scope.cancel()`) | ||||||
|     except ContextCancelled as ctxc: |     except ContextCancelled as ctxc: | ||||||
|         scope_err = ctxc |         scope_err = ctxc | ||||||
|         ctx._local_error: BaseException = scope_err |         ctx._local_error: BaseException = scope_err | ||||||
|         ctxc_from_callee = ctxc |         ctxc_from_child = ctxc | ||||||
| 
 | 
 | ||||||
|         # XXX TODO XXX: FIX THIS debug_mode BUGGGG!!! |         # XXX TODO XXX: FIX THIS debug_mode BUGGGG!!! | ||||||
|         # using this code and then resuming the REPL will |         # using this code and then resuming the REPL will | ||||||
|  | @ -2170,7 +2200,7 @@ async def open_context_from_portal( | ||||||
|         #   debugging the tractor-runtime itself using it's |         #   debugging the tractor-runtime itself using it's | ||||||
|         #   own `.devx.` tooling! |         #   own `.devx.` tooling! | ||||||
|         #  |         #  | ||||||
|         # await _debug.pause() |         # await debug.pause() | ||||||
| 
 | 
 | ||||||
|         # CASE 2: context was cancelled by local task calling |         # CASE 2: context was cancelled by local task calling | ||||||
|         # `.cancel()`, we don't raise and the exit block should |         # `.cancel()`, we don't raise and the exit block should | ||||||
|  | @ -2196,11 +2226,11 @@ async def open_context_from_portal( | ||||||
|     # the above `._scope` can be cancelled due to: |     # the above `._scope` can be cancelled due to: | ||||||
|     # 1. an explicit self cancel via `Context.cancel()` or |     # 1. an explicit self cancel via `Context.cancel()` or | ||||||
|     #    `Actor.cancel()`, |     #    `Actor.cancel()`, | ||||||
|     # 2. any "callee"-side remote error, possibly also a cancellation |     # 2. any "child"-side remote error, possibly also a cancellation | ||||||
|     #    request by some peer, |     #    request by some peer, | ||||||
|     # 3. any "caller" (aka THIS scope's) local error raised in the above `yield` |     # 3. any "parent" (aka THIS scope's) local error raised in the above `yield` | ||||||
|     except ( |     except ( | ||||||
|         # CASE 3: standard local error in this caller/yieldee |         # CASE 3: standard local error in this parent/yieldee | ||||||
|         Exception, |         Exception, | ||||||
| 
 | 
 | ||||||
|         # CASES 1 & 2: can manifest as a `ctx._scope_nursery` |         # CASES 1 & 2: can manifest as a `ctx._scope_nursery` | ||||||
|  | @ -2214,9 +2244,9 @@ async def open_context_from_portal( | ||||||
|         #   any `Context._maybe_raise_remote_err()` call. |         #   any `Context._maybe_raise_remote_err()` call. | ||||||
|         # |         # | ||||||
|         # 2.-`BaseExceptionGroup[ContextCancelled | RemoteActorError]` |         # 2.-`BaseExceptionGroup[ContextCancelled | RemoteActorError]` | ||||||
|         #    from any error delivered from the "callee" side |         #    from any error delivered from the "child" side | ||||||
|         #    AND a group-exc is only raised if there was > 1 |         #    AND a group-exc is only raised if there was > 1 | ||||||
|         #    tasks started *here* in the "caller" / opener |         #    tasks started *here* in the "parent" / opener | ||||||
|         #    block. If any one of those tasks calls |         #    block. If any one of those tasks calls | ||||||
|         #    `.wait_for_result()` or `MsgStream.receive()` |         #    `.wait_for_result()` or `MsgStream.receive()` | ||||||
|         #    `._maybe_raise_remote_err()` will be transitively |         #    `._maybe_raise_remote_err()` will be transitively | ||||||
|  | @ -2229,18 +2259,18 @@ async def open_context_from_portal( | ||||||
|         trio.Cancelled,  # NOTE: NOT from inside the ctx._scope |         trio.Cancelled,  # NOTE: NOT from inside the ctx._scope | ||||||
|         KeyboardInterrupt, |         KeyboardInterrupt, | ||||||
| 
 | 
 | ||||||
|     ) as caller_err: |     ) as rent_err: | ||||||
|         scope_err = caller_err |         scope_err = rent_err | ||||||
|         ctx._local_error: BaseException = scope_err |         ctx._local_error: BaseException = scope_err | ||||||
| 
 | 
 | ||||||
|         # XXX: ALWAYS request the context to CANCEL ON any ERROR. |         # XXX: ALWAYS request the context to CANCEL ON any ERROR. | ||||||
|         # NOTE: `Context.cancel()` is conversely NEVER CALLED in |         # NOTE: `Context.cancel()` is conversely NEVER CALLED in | ||||||
|         # the `ContextCancelled` "self cancellation absorbed" case |         # the `ContextCancelled` "self cancellation absorbed" case | ||||||
|         # handled in the block above ^^^ !! |         # handled in the block above ^^^ !! | ||||||
|         # await _debug.pause() |         # await debug.pause() | ||||||
|         # log.cancel( |         # log.cancel( | ||||||
|         match scope_err: |         match scope_err: | ||||||
|             case trio.Cancelled: |             case trio.Cancelled(): | ||||||
|                 logmeth = log.cancel |                 logmeth = log.cancel | ||||||
| 
 | 
 | ||||||
|             # XXX explicitly report on any non-graceful-taskc cases |             # XXX explicitly report on any non-graceful-taskc cases | ||||||
|  | @ -2248,15 +2278,15 @@ async def open_context_from_portal( | ||||||
|                 logmeth = log.exception |                 logmeth = log.exception | ||||||
| 
 | 
 | ||||||
|         logmeth( |         logmeth( | ||||||
|             f'ctx {ctx.side!r}-side exited with {ctx.repr_outcome()}\n' |             f'ctx {ctx.side!r}-side exited with {ctx.repr_outcome()!r}\n' | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         if debug_mode(): |         if debug_mode(): | ||||||
|             # async with _debug.acquire_debug_lock(portal.actor.uid): |             # async with debug.acquire_debug_lock(portal.actor.uid): | ||||||
|             #     pass |             #     pass | ||||||
|             # TODO: factor ^ into below for non-root cases? |             # TODO: factor ^ into below for non-root cases? | ||||||
|             # |             # | ||||||
|             from .devx._debug import maybe_wait_for_debugger |             from .devx.debug import maybe_wait_for_debugger | ||||||
|             was_acquired: bool = await maybe_wait_for_debugger( |             was_acquired: bool = await maybe_wait_for_debugger( | ||||||
|                 # header_msg=( |                 # header_msg=( | ||||||
|                 #     'Delaying `ctx.cancel()` until debug lock ' |                 #     'Delaying `ctx.cancel()` until debug lock ' | ||||||
|  | @ -2269,9 +2299,9 @@ async def open_context_from_portal( | ||||||
|                     'Calling `ctx.cancel()`!\n' |                     'Calling `ctx.cancel()`!\n' | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|         # we don't need to cancel the callee if it already |         # we don't need to cancel the child if it already | ||||||
|         # told us it's cancelled ;p |         # told us it's cancelled ;p | ||||||
|         if ctxc_from_callee is None: |         if ctxc_from_child is None: | ||||||
|             try: |             try: | ||||||
|                 await ctx.cancel() |                 await ctx.cancel() | ||||||
|             except ( |             except ( | ||||||
|  | @ -2302,8 +2332,8 @@ async def open_context_from_portal( | ||||||
|             # via a call to |             # via a call to | ||||||
|             # `Context._maybe_cancel_and_set_remote_error()`. |             # `Context._maybe_cancel_and_set_remote_error()`. | ||||||
|             # As per `Context._deliver_msg()`, that error IS |             # As per `Context._deliver_msg()`, that error IS | ||||||
|             # ALWAYS SET any time "callee" side fails and causes "caller |             # ALWAYS SET any time "child" side fails and causes | ||||||
|             # side" cancellation via a `ContextCancelled` here. |             # "parent side" cancellation via a `ContextCancelled` here. | ||||||
|             try: |             try: | ||||||
|                 result_or_err: Exception|Any = await ctx.wait_for_result() |                 result_or_err: Exception|Any = await ctx.wait_for_result() | ||||||
|             except BaseException as berr: |             except BaseException as berr: | ||||||
|  | @ -2319,8 +2349,8 @@ async def open_context_from_portal( | ||||||
|                 raise |                 raise | ||||||
| 
 | 
 | ||||||
|             # yes this worx! |             # yes this worx! | ||||||
|             # from .devx import _debug |             # from .devx import debug | ||||||
|             # await _debug.pause() |             # await debug.pause() | ||||||
| 
 | 
 | ||||||
|             # an exception type boxed in a `RemoteActorError` |             # an exception type boxed in a `RemoteActorError` | ||||||
|             # is returned (meaning it was obvi not raised) |             # is returned (meaning it was obvi not raised) | ||||||
|  | @ -2339,7 +2369,7 @@ async def open_context_from_portal( | ||||||
|                     ) |                     ) | ||||||
|                 case (None, _): |                 case (None, _): | ||||||
|                     log.runtime( |                     log.runtime( | ||||||
|                         'Context returned final result from callee task:\n' |                         'Context returned final result from child task:\n' | ||||||
|                         f'<= peer: {uid}\n' |                         f'<= peer: {uid}\n' | ||||||
|                         f'  |_ {nsf}()\n\n' |                         f'  |_ {nsf}()\n\n' | ||||||
| 
 | 
 | ||||||
|  | @ -2355,7 +2385,7 @@ async def open_context_from_portal( | ||||||
|         # where the root is waiting on the lock to clear but the |         # where the root is waiting on the lock to clear but the | ||||||
|         # child has already cleared it and clobbered IPC. |         # child has already cleared it and clobbered IPC. | ||||||
|         if debug_mode(): |         if debug_mode(): | ||||||
|             from .devx._debug import maybe_wait_for_debugger |             from .devx.debug import maybe_wait_for_debugger | ||||||
|             await maybe_wait_for_debugger() |             await maybe_wait_for_debugger() | ||||||
| 
 | 
 | ||||||
|         # though it should be impossible for any tasks |         # though it should be impossible for any tasks | ||||||
|  | @ -2434,7 +2464,7 @@ async def open_context_from_portal( | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|             # TODO: should we add a `._cancel_req_received` |             # TODO: should we add a `._cancel_req_received` | ||||||
|             # flag to determine if the callee manually called |             # flag to determine if the child manually called | ||||||
|             # `ctx.cancel()`? |             # `ctx.cancel()`? | ||||||
|             # -[ ] going to need a cid check no? |             # -[ ] going to need a cid check no? | ||||||
| 
 | 
 | ||||||
|  | @ -2490,7 +2520,7 @@ def mk_context( | ||||||
|     recv_chan: trio.MemoryReceiveChannel |     recv_chan: trio.MemoryReceiveChannel | ||||||
|     send_chan, recv_chan = trio.open_memory_channel(msg_buffer_size) |     send_chan, recv_chan = trio.open_memory_channel(msg_buffer_size) | ||||||
| 
 | 
 | ||||||
|     # TODO: only scan caller-info if log level so high! |     # TODO: only scan parent-info if log level so high! | ||||||
|     from .devx._frame_stack import find_caller_info |     from .devx._frame_stack import find_caller_info | ||||||
|     caller_info: CallerInfo|None = find_caller_info() |     caller_info: CallerInfo|None = find_caller_info() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,7 +28,10 @@ from typing import ( | ||||||
| from contextlib import asynccontextmanager as acm | from contextlib import asynccontextmanager as acm | ||||||
| 
 | 
 | ||||||
| from tractor.log import get_logger | from tractor.log import get_logger | ||||||
| from .trionics import gather_contexts | from .trionics import ( | ||||||
|  |     gather_contexts, | ||||||
|  |     collapse_eg, | ||||||
|  | ) | ||||||
| from .ipc import _connect_chan, Channel | from .ipc import _connect_chan, Channel | ||||||
| from ._addr import ( | from ._addr import ( | ||||||
|     UnwrappedAddress, |     UnwrappedAddress, | ||||||
|  | @ -48,7 +51,6 @@ from ._state import ( | ||||||
| 
 | 
 | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from ._runtime import Actor |     from ._runtime import Actor | ||||||
|     from .ipc._server import IPCServer |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| log = get_logger(__name__) | log = get_logger(__name__) | ||||||
|  | @ -80,7 +82,7 @@ async def get_registry( | ||||||
|         ) |         ) | ||||||
|     else: |     else: | ||||||
|         # TODO: try to look pre-existing connection from |         # TODO: try to look pre-existing connection from | ||||||
|         # `IPCServer._peers` and use it instead? |         # `Server._peers` and use it instead? | ||||||
|         async with ( |         async with ( | ||||||
|             _connect_chan(addr) as chan, |             _connect_chan(addr) as chan, | ||||||
|             open_portal(chan) as regstr_ptl, |             open_portal(chan) as regstr_ptl, | ||||||
|  | @ -88,7 +90,6 @@ async def get_registry( | ||||||
|             yield regstr_ptl |             yield regstr_ptl | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| @acm | @acm | ||||||
| async def get_root( | async def get_root( | ||||||
|     **kwargs, |     **kwargs, | ||||||
|  | @ -112,18 +113,23 @@ def get_peer_by_name( | ||||||
| ) -> list[Channel]|None:  # at least 1 | ) -> list[Channel]|None:  # at least 1 | ||||||
|     ''' |     ''' | ||||||
|     Scan for an existing connection (set) to a named actor |     Scan for an existing connection (set) to a named actor | ||||||
|     and return any channels from `IPCServer._peers: dict`. |     and return any channels from `Server._peers: dict`. | ||||||
| 
 | 
 | ||||||
|     This is an optimization method over querying the registrar for |     This is an optimization method over querying the registrar for | ||||||
|     the same info. |     the same info. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     actor: Actor = current_actor() |     actor: Actor = current_actor() | ||||||
|     server: IPCServer = actor.ipc_server |     to_scan: dict[tuple, list[Channel]] = actor.ipc_server._peers.copy() | ||||||
|     to_scan: dict[tuple, list[Channel]] = server._peers.copy() | 
 | ||||||
|     pchan: Channel|None = actor._parent_chan |     # TODO: is this ever needed? creates a duplicate channel on actor._peers | ||||||
|     if pchan: |     # when multiple find_actor calls are made to same actor from a single ctx | ||||||
|         to_scan[pchan.uid].append(pchan) |     # which causes actor exit to hang waiting forever on | ||||||
|  |     # `actor._no_more_peers.wait()` in `_runtime.async_main` | ||||||
|  | 
 | ||||||
|  |     # pchan: Channel|None = actor._parent_chan | ||||||
|  |     # if pchan and pchan.uid not in to_scan: | ||||||
|  |     #     to_scan[pchan.uid].append(pchan) | ||||||
| 
 | 
 | ||||||
|     for aid, chans in to_scan.items(): |     for aid, chans in to_scan.items(): | ||||||
|         _, peer_name = aid |         _, peer_name = aid | ||||||
|  | @ -249,9 +255,12 @@ async def find_actor( | ||||||
|         for addr in registry_addrs |         for addr in registry_addrs | ||||||
|     ) |     ) | ||||||
|     portals: list[Portal] |     portals: list[Portal] | ||||||
|     async with gather_contexts( |     async with ( | ||||||
|         mngrs=maybe_portals, |         collapse_eg(), | ||||||
|     ) as portals: |         gather_contexts( | ||||||
|  |             mngrs=maybe_portals, | ||||||
|  |         ) as portals, | ||||||
|  |     ): | ||||||
|         # log.runtime( |         # log.runtime( | ||||||
|         #     'Gathered portals:\n' |         #     'Gathered portals:\n' | ||||||
|         #     f'{portals}' |         #     f'{portals}' | ||||||
|  |  | ||||||
|  | @ -21,7 +21,7 @@ Sub-process entry points. | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| from functools import partial | from functools import partial | ||||||
| import multiprocessing as mp | import multiprocessing as mp | ||||||
| import os | # import os | ||||||
| from typing import ( | from typing import ( | ||||||
|     Any, |     Any, | ||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
|  | @ -35,9 +35,10 @@ from .log import ( | ||||||
| ) | ) | ||||||
| from . import _state | from . import _state | ||||||
| from .devx import ( | from .devx import ( | ||||||
|     _debug, |     _frame_stack, | ||||||
|     pformat, |     pformat, | ||||||
| ) | ) | ||||||
|  | # from .msg import pretty_struct | ||||||
| from .to_asyncio import run_as_asyncio_guest | from .to_asyncio import run_as_asyncio_guest | ||||||
| from ._addr import UnwrappedAddress | from ._addr import UnwrappedAddress | ||||||
| from ._runtime import ( | from ._runtime import ( | ||||||
|  | @ -116,7 +117,7 @@ def _trio_main( | ||||||
|     Entry point for a `trio_run_in_process` subactor. |     Entry point for a `trio_run_in_process` subactor. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     _debug.hide_runtime_frames() |     _frame_stack.hide_runtime_frames() | ||||||
| 
 | 
 | ||||||
|     _state._current_actor = actor |     _state._current_actor = actor | ||||||
|     trio_main = partial( |     trio_main = partial( | ||||||
|  | @ -127,20 +128,13 @@ def _trio_main( | ||||||
| 
 | 
 | ||||||
|     if actor.loglevel is not None: |     if actor.loglevel is not None: | ||||||
|         get_console_log(actor.loglevel) |         get_console_log(actor.loglevel) | ||||||
|         actor_info: str = ( |  | ||||||
|             f'|_{actor}\n' |  | ||||||
|             f'  uid: {actor.uid}\n' |  | ||||||
|             f'  pid: {os.getpid()}\n' |  | ||||||
|             f'  parent_addr: {parent_addr}\n' |  | ||||||
|             f'  loglevel: {actor.loglevel}\n' |  | ||||||
|         ) |  | ||||||
|         log.info( |         log.info( | ||||||
|             'Starting new `trio` subactor:\n' |             f'Starting `trio` subactor from parent @ ' | ||||||
|  |             f'{parent_addr}\n' | ||||||
|             + |             + | ||||||
|             pformat.nest_from_op( |             pformat.nest_from_op( | ||||||
|                 input_op='>(',  # see syntax ideas above |                 input_op='>(',  # see syntax ideas above | ||||||
|                 tree_str=actor_info, |                 text=f'{actor}', | ||||||
|                 back_from_op=2,  # since "complete" |  | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|     logmeth = log.info |     logmeth = log.info | ||||||
|  | @ -149,8 +143,8 @@ def _trio_main( | ||||||
|         + |         + | ||||||
|         pformat.nest_from_op( |         pformat.nest_from_op( | ||||||
|             input_op=')>',  # like a "closed-to-play"-icon from super perspective |             input_op=')>',  # like a "closed-to-play"-icon from super perspective | ||||||
|             tree_str=actor_info, |             text=f'{actor}', | ||||||
|             back_from_op=1, |             nest_indent=1, | ||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
|     try: |     try: | ||||||
|  | @ -167,7 +161,7 @@ def _trio_main( | ||||||
|             + |             + | ||||||
|             pformat.nest_from_op( |             pformat.nest_from_op( | ||||||
|                 input_op='c)>',  # closed due to cancel (see above) |                 input_op='c)>',  # closed due to cancel (see above) | ||||||
|                 tree_str=actor_info, |                 text=f'{actor}', | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|     except BaseException as err: |     except BaseException as err: | ||||||
|  | @ -177,7 +171,7 @@ def _trio_main( | ||||||
|             + |             + | ||||||
|             pformat.nest_from_op( |             pformat.nest_from_op( | ||||||
|                 input_op='x)>',  # closed by error |                 input_op='x)>',  # closed by error | ||||||
|                 tree_str=actor_info, |                 text=f'{actor}', | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|         # NOTE since we raise a tb will already be shown on the |         # NOTE since we raise a tb will already be shown on the | ||||||
|  |  | ||||||
|  | @ -1246,55 +1246,6 @@ def unpack_error( | ||||||
|     return exc |     return exc | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def is_multi_cancelled( |  | ||||||
|     exc: BaseException|BaseExceptionGroup, |  | ||||||
| 
 |  | ||||||
|     ignore_nested: set[BaseException] = set(), |  | ||||||
| 
 |  | ||||||
| ) -> bool|BaseExceptionGroup: |  | ||||||
|     ''' |  | ||||||
|     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.update({trio.Cancelled}) |  | ||||||
| 
 |  | ||||||
|     if isinstance(exc, BaseExceptionGroup): |  | ||||||
|         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 |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def _raise_from_unexpected_msg( | def _raise_from_unexpected_msg( | ||||||
|     ctx: Context, |     ctx: Context, | ||||||
|     msg: MsgType, |     msg: MsgType, | ||||||
|  |  | ||||||
|  | @ -39,7 +39,10 @@ import warnings | ||||||
| 
 | 
 | ||||||
| import trio | import trio | ||||||
| 
 | 
 | ||||||
| from .trionics import maybe_open_nursery | from .trionics import ( | ||||||
|  |     maybe_open_nursery, | ||||||
|  |     collapse_eg, | ||||||
|  | ) | ||||||
| from ._state import ( | from ._state import ( | ||||||
|     current_actor, |     current_actor, | ||||||
| ) | ) | ||||||
|  | @ -115,6 +118,10 @@ class Portal: | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def chan(self) -> Channel: |     def chan(self) -> Channel: | ||||||
|  |         ''' | ||||||
|  |         Ref to this ctx's underlying `tractor.ipc.Channel`. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|         return self._chan |         return self._chan | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|  | @ -174,10 +181,17 @@ class Portal: | ||||||
| 
 | 
 | ||||||
|         # not expecting a "main" result |         # not expecting a "main" result | ||||||
|         if self._expect_result_ctx is None: |         if self._expect_result_ctx is None: | ||||||
|  |             peer_id: str = f'{self.channel.aid.reprol()!r}' | ||||||
|             log.warning( |             log.warning( | ||||||
|                 f"Portal for {self.channel.aid} not expecting a final" |                 f'Portal to peer {peer_id} will not deliver a final result?\n' | ||||||
|                 " result?\nresult() should only be called if subactor" |                 f'\n' | ||||||
|                 " was spawned with `ActorNursery.run_in_actor()`") |                 f'Context.result() can only be called by the parent of ' | ||||||
|  |                 f'a sub-actor when it was spawned with ' | ||||||
|  |                 f'`ActorNursery.run_in_actor()`' | ||||||
|  |                 f'\n' | ||||||
|  |                 f'Further this `ActorNursery`-method-API will deprecated in the' | ||||||
|  |                 f'near fututre!\n' | ||||||
|  |             ) | ||||||
|             return NoResult |             return NoResult | ||||||
| 
 | 
 | ||||||
|         # expecting a "main" result |         # expecting a "main" result | ||||||
|  | @ -210,6 +224,7 @@ class Portal: | ||||||
|         typname: str = type(self).__name__ |         typname: str = type(self).__name__ | ||||||
|         log.warning( |         log.warning( | ||||||
|             f'`{typname}.result()` is DEPRECATED!\n' |             f'`{typname}.result()` is DEPRECATED!\n' | ||||||
|  |             f'\n' | ||||||
|             f'Use `{typname}.wait_for_result()` instead!\n' |             f'Use `{typname}.wait_for_result()` instead!\n' | ||||||
|         ) |         ) | ||||||
|         return await self.wait_for_result( |         return await self.wait_for_result( | ||||||
|  | @ -221,8 +236,10 @@ class Portal: | ||||||
|         # terminate all locally running async generator |         # terminate all locally running async generator | ||||||
|         # IPC calls |         # IPC calls | ||||||
|         if self._streams: |         if self._streams: | ||||||
|             log.cancel( |             peer_id: str = f'{self.channel.aid.reprol()!r}' | ||||||
|                 f"Cancelling all streams with {self.channel.aid}") |             report: str = ( | ||||||
|  |                 f'Cancelling all msg-streams with {peer_id}\n' | ||||||
|  |             ) | ||||||
|             for stream in self._streams.copy(): |             for stream in self._streams.copy(): | ||||||
|                 try: |                 try: | ||||||
|                     await stream.aclose() |                     await stream.aclose() | ||||||
|  | @ -231,10 +248,18 @@ class Portal: | ||||||
|                     # (unless of course at some point down the road we |                     # (unless of course at some point down the road we | ||||||
|                     # won't expect this to always be the case or need to |                     # won't expect this to always be the case or need to | ||||||
|                     # detect it for respawning purposes?) |                     # detect it for respawning purposes?) | ||||||
|                     log.debug(f"{stream} was already closed.") |                     report += ( | ||||||
|  |                         f'->) {stream!r} already closed\n' | ||||||
|  |                     ) | ||||||
|  | 
 | ||||||
|  |             log.cancel(report) | ||||||
| 
 | 
 | ||||||
|     async def aclose(self): |     async def aclose(self): | ||||||
|         log.debug(f"Closing {self}") |         log.debug( | ||||||
|  |             f'Closing portal\n' | ||||||
|  |             f'>}}\n' | ||||||
|  |             f'|_{self}\n' | ||||||
|  |         ) | ||||||
|         # TODO: once we move to implementing our own `ReceiveChannel` |         # TODO: once we move to implementing our own `ReceiveChannel` | ||||||
|         # (including remote task cancellation inside its `.aclose()`) |         # (including remote task cancellation inside its `.aclose()`) | ||||||
|         # we'll need to .aclose all those channels here |         # we'll need to .aclose all those channels here | ||||||
|  | @ -260,23 +285,22 @@ class Portal: | ||||||
|         __runtimeframe__: int = 1  # noqa |         __runtimeframe__: int = 1  # noqa | ||||||
| 
 | 
 | ||||||
|         chan: Channel = self.channel |         chan: Channel = self.channel | ||||||
|  |         peer_id: str = f'{self.channel.aid.reprol()!r}' | ||||||
|         if not chan.connected(): |         if not chan.connected(): | ||||||
|             log.runtime( |             log.runtime( | ||||||
|                 'This channel is already closed, skipping cancel request..' |                 'Peer {peer_id} is already disconnected\n' | ||||||
|  |                 '-> skipping cancel request..\n' | ||||||
|             ) |             ) | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
|         reminfo: str = ( |  | ||||||
|             f'c)=> {self.channel.aid}\n' |  | ||||||
|             f'  |_{chan}\n' |  | ||||||
|         ) |  | ||||||
|         log.cancel( |         log.cancel( | ||||||
|             f'Requesting actor-runtime cancel for peer\n\n' |             f'Sending actor-runtime-cancel-req to peer\n' | ||||||
|             f'{reminfo}' |             f'\n' | ||||||
|  |             f'c)=> {peer_id}\n' | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|         # XXX the one spot we set it? |         # XXX the one spot we set it? | ||||||
|         self.channel._cancel_called: bool = True |         chan._cancel_called: bool = True | ||||||
|         try: |         try: | ||||||
|             # send cancel cmd - might not get response |             # send cancel cmd - might not get response | ||||||
|             # XXX: sure would be nice to make this work with |             # XXX: sure would be nice to make this work with | ||||||
|  | @ -297,8 +321,9 @@ class Portal: | ||||||
|                 # may timeout and we never get an ack (obvi racy) |                 # may timeout and we never get an ack (obvi racy) | ||||||
|                 # but that doesn't mean it wasn't cancelled. |                 # but that doesn't mean it wasn't cancelled. | ||||||
|                 log.debug( |                 log.debug( | ||||||
|                     'May have failed to cancel peer?\n' |                     f'May have failed to cancel peer?\n' | ||||||
|                     f'{reminfo}' |                     f'\n' | ||||||
|  |                     f'c)=?> {peer_id}\n' | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|             # if we get here some weird cancellation case happened |             # if we get here some weird cancellation case happened | ||||||
|  | @ -316,22 +341,22 @@ class Portal: | ||||||
| 
 | 
 | ||||||
|             TransportClosed, |             TransportClosed, | ||||||
|         ) as tpt_err: |         ) as tpt_err: | ||||||
|             report: str = ( |             ipc_borked_report: str = ( | ||||||
|                 f'IPC chan for actor already closed or broken?\n\n' |                 f'IPC for actor already closed/broken?\n\n' | ||||||
|                 f'{self.channel.aid}\n' |                 f'\n' | ||||||
|                 f' |_{self.channel}\n' |                 f'c)=x> {peer_id}\n' | ||||||
|             ) |             ) | ||||||
|             match tpt_err: |             match tpt_err: | ||||||
|                 case TransportClosed(): |                 case TransportClosed(): | ||||||
|                     log.debug(report) |                     log.debug(ipc_borked_report) | ||||||
|                 case _: |                 case _: | ||||||
|                     report += ( |                     ipc_borked_report += ( | ||||||
|                         f'\n' |                         f'\n' | ||||||
|                         f'Unhandled low-level transport-closed/error during\n' |                         f'Unhandled low-level transport-closed/error during\n' | ||||||
|                         f'Portal.cancel_actor()` request?\n' |                         f'Portal.cancel_actor()` request?\n' | ||||||
|                         f'<{type(tpt_err).__name__}( {tpt_err} )>\n' |                         f'<{type(tpt_err).__name__}( {tpt_err} )>\n' | ||||||
|                     ) |                     ) | ||||||
|                     log.warning(report) |                     log.warning(ipc_borked_report) | ||||||
| 
 | 
 | ||||||
|             return False |             return False | ||||||
| 
 | 
 | ||||||
|  | @ -488,10 +513,13 @@ class Portal: | ||||||
|                 with trio.CancelScope(shield=True): |                 with trio.CancelScope(shield=True): | ||||||
|                     await ctx.cancel() |                     await ctx.cancel() | ||||||
| 
 | 
 | ||||||
|             except trio.ClosedResourceError: |             except trio.ClosedResourceError as cre: | ||||||
|                 # if the far end terminates before we send a cancel the |                 # if the far end terminates before we send a cancel the | ||||||
|                 # underlying transport-channel may already be closed. |                 # underlying transport-channel may already be closed. | ||||||
|                 log.cancel(f'Context {ctx} was already closed?') |                 log.cancel( | ||||||
|  |                     f'Context.cancel() -> {cre!r}\n' | ||||||
|  |                     f'cid: {ctx.cid!r} already closed?\n' | ||||||
|  |                 ) | ||||||
| 
 | 
 | ||||||
|             # XXX: should this always be done? |             # XXX: should this always be done? | ||||||
|             # await recv_chan.aclose() |             # await recv_chan.aclose() | ||||||
|  | @ -558,14 +586,13 @@ async def open_portal( | ||||||
|     assert actor |     assert actor | ||||||
|     was_connected: bool = False |     was_connected: bool = False | ||||||
| 
 | 
 | ||||||
|     async with maybe_open_nursery( |     async with ( | ||||||
|         tn, |         collapse_eg(), | ||||||
|         shield=shield, |         maybe_open_nursery( | ||||||
|         strict_exception_groups=False, |             tn, | ||||||
|         # ^XXX^ TODO? soo roll our own then ?? |             shield=shield, | ||||||
|         # -> since we kinda want the "if only one `.exception` then |         ) as tn, | ||||||
|         # just raise that" interface? |     ): | ||||||
|     ) as tn: |  | ||||||
| 
 | 
 | ||||||
|         if not channel.connected(): |         if not channel.connected(): | ||||||
|             await channel.connect() |             await channel.connect() | ||||||
|  | @ -582,8 +609,7 @@ async def open_portal( | ||||||
|             msg_loop_cs = await tn.start( |             msg_loop_cs = await tn.start( | ||||||
|                 partial( |                 partial( | ||||||
|                     _rpc.process_messages, |                     _rpc.process_messages, | ||||||
|                     actor, |                     chan=channel, | ||||||
|                     channel, |  | ||||||
|                     # if the local task is cancelled we want to keep |                     # if the local task is cancelled we want to keep | ||||||
|                     # the msg loop running until our block ends |                     # the msg loop running until our block ends | ||||||
|                     shield=True, |                     shield=True, | ||||||
|  |  | ||||||
							
								
								
									
										200
									
								
								tractor/_root.py
								
								
								
								
							
							
						
						
									
										200
									
								
								tractor/_root.py
								
								
								
								
							|  | @ -37,14 +37,12 @@ import warnings | ||||||
| 
 | 
 | ||||||
| import trio | import trio | ||||||
| 
 | 
 | ||||||
| from ._runtime import ( | from . import _runtime | ||||||
|     Actor, | from .devx import ( | ||||||
|     Arbiter, |     debug, | ||||||
|     # TODO: rename and make a non-actor subtype? |     _frame_stack, | ||||||
|     # Arbiter as Registry, |     pformat as _pformat, | ||||||
|     async_main, |  | ||||||
| ) | ) | ||||||
| from .devx import _debug |  | ||||||
| from . import _spawn | from . import _spawn | ||||||
| from . import _state | from . import _state | ||||||
| from . import log | from . import log | ||||||
|  | @ -58,16 +56,19 @@ from ._addr import ( | ||||||
|     mk_uuid, |     mk_uuid, | ||||||
|     wrap_address, |     wrap_address, | ||||||
| ) | ) | ||||||
|  | from .trionics import ( | ||||||
|  |     is_multi_cancelled, | ||||||
|  |     collapse_eg, | ||||||
|  | ) | ||||||
| from ._exceptions import ( | from ._exceptions import ( | ||||||
|     RuntimeFailure, |     RuntimeFailure, | ||||||
|     is_multi_cancelled, |  | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| logger = log.get_logger('tractor') | logger = log.get_logger('tractor') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # TODO: stick this in a `@acm` defined in `devx._debug`? | # TODO: stick this in a `@acm` defined in `devx.debug`? | ||||||
| # -[ ] also maybe consider making this a `wrapt`-deco to | # -[ ] also maybe consider making this a `wrapt`-deco to | ||||||
| #     save an indent level? | #     save an indent level? | ||||||
| # | # | ||||||
|  | @ -89,17 +90,17 @@ async def maybe_block_bp( | ||||||
|         debug_mode |         debug_mode | ||||||
|         and maybe_enable_greenback |         and maybe_enable_greenback | ||||||
|         and ( |         and ( | ||||||
|             maybe_mod := await _debug.maybe_init_greenback( |             maybe_mod := await debug.maybe_init_greenback( | ||||||
|                 raise_not_found=False, |                 raise_not_found=False, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|     ): |     ): | ||||||
|         logger.info( |         logger.info( | ||||||
|             f'Found `greenback` installed @ {maybe_mod}\n' |             f'Found `greenback` installed @ {maybe_mod}\n' | ||||||
|             'Enabling `tractor.pause_from_sync()` support!\n' |             f'Enabling `tractor.pause_from_sync()` support!\n' | ||||||
|         ) |         ) | ||||||
|         os.environ['PYTHONBREAKPOINT'] = ( |         os.environ['PYTHONBREAKPOINT'] = ( | ||||||
|             'tractor.devx._debug._sync_pause_from_builtin' |             'tractor.devx.debug._sync_pause_from_builtin' | ||||||
|         ) |         ) | ||||||
|         _state._runtime_vars['use_greenback'] = True |         _state._runtime_vars['use_greenback'] = True | ||||||
|         bp_blocked = False |         bp_blocked = False | ||||||
|  | @ -163,7 +164,9 @@ async def open_root_actor( | ||||||
| 
 | 
 | ||||||
|     # enables the multi-process debugger support |     # enables the multi-process debugger support | ||||||
|     debug_mode: bool = False, |     debug_mode: bool = False, | ||||||
|     maybe_enable_greenback: bool = True,  # `.pause_from_sync()/breakpoint()` support |     maybe_enable_greenback: bool = False,  # `.pause_from_sync()/breakpoint()` support | ||||||
|  |     # ^XXX NOTE^ the perf implications of use, | ||||||
|  |     # https://greenback.readthedocs.io/en/latest/principle.html#performance | ||||||
|     enable_stack_on_sig: bool = False, |     enable_stack_on_sig: bool = False, | ||||||
| 
 | 
 | ||||||
|     # internal logging |     # internal logging | ||||||
|  | @ -178,7 +181,7 @@ async def open_root_actor( | ||||||
| 
 | 
 | ||||||
|     hide_tb: bool = True, |     hide_tb: bool = True, | ||||||
| 
 | 
 | ||||||
|     # XXX, proxied directly to `.devx._debug._maybe_enter_pm()` |     # XXX, proxied directly to `.devx.debug._maybe_enter_pm()` | ||||||
|     # for REPL-entry logic. |     # for REPL-entry logic. | ||||||
|     debug_filter: Callable[ |     debug_filter: Callable[ | ||||||
|         [BaseException|BaseExceptionGroup], |         [BaseException|BaseExceptionGroup], | ||||||
|  | @ -189,13 +192,19 @@ async def open_root_actor( | ||||||
|     # read-only state to sublayers? |     # read-only state to sublayers? | ||||||
|     # extra_rt_vars: dict|None = None, |     # extra_rt_vars: dict|None = None, | ||||||
| 
 | 
 | ||||||
| ) -> Actor: | ) -> _runtime.Actor: | ||||||
|     ''' |     ''' | ||||||
|     Runtime init entry point for ``tractor``. |     Initialize the `tractor` runtime by starting a "root actor" in | ||||||
|  |     a parent-most Python process. | ||||||
|  | 
 | ||||||
|  |     All (disjoint) actor-process-trees-as-programs are created via | ||||||
|  |     this entrypoint. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     # XXX NEVER allow nested actor-trees! |     # XXX NEVER allow nested actor-trees! | ||||||
|     if already_actor := _state.current_actor(err_on_no_runtime=False): |     if already_actor := _state.current_actor( | ||||||
|  |         err_on_no_runtime=False, | ||||||
|  |     ): | ||||||
|         rtvs: dict[str, Any] = _state._runtime_vars |         rtvs: dict[str, Any] = _state._runtime_vars | ||||||
|         root_mailbox: list[str, int] = rtvs['_root_mailbox'] |         root_mailbox: list[str, int] = rtvs['_root_mailbox'] | ||||||
|         registry_addrs: list[list[str, int]] = rtvs['_registry_addrs'] |         registry_addrs: list[list[str, int]] = rtvs['_registry_addrs'] | ||||||
|  | @ -217,18 +226,23 @@ async def open_root_actor( | ||||||
|     ): |     ): | ||||||
|         if enable_transports is None: |         if enable_transports is None: | ||||||
|             enable_transports: list[str] = _state.current_ipc_protos() |             enable_transports: list[str] = _state.current_ipc_protos() | ||||||
|  |         else: | ||||||
|  |             _state._runtime_vars['_enable_tpts'] = enable_transports | ||||||
| 
 | 
 | ||||||
|             # TODO! support multi-tpts per actor! Bo |         # TODO! support multi-tpts per actor! | ||||||
|             assert ( |         # Bo | ||||||
|                 len(enable_transports) == 1 |         if not len(enable_transports) == 1: | ||||||
|             ), 'No multi-tpt support yet!' |             raise RuntimeError( | ||||||
|  |                 f'No multi-tpt support yet!\n' | ||||||
|  |                 f'enable_transports={enable_transports!r}\n' | ||||||
|  |             ) | ||||||
| 
 | 
 | ||||||
|         _debug.hide_runtime_frames() |         _frame_stack.hide_runtime_frames() | ||||||
|         __tracebackhide__: bool = hide_tb |         __tracebackhide__: bool = hide_tb | ||||||
| 
 | 
 | ||||||
|         # attempt to retreive ``trio``'s sigint handler and stash it |         # attempt to retreive ``trio``'s sigint handler and stash it | ||||||
|         # on our debugger lock state. |         # on our debugger lock state. | ||||||
|         _debug.DebugStatus._trio_handler = signal.getsignal(signal.SIGINT) |         debug.DebugStatus._trio_handler = signal.getsignal(signal.SIGINT) | ||||||
| 
 | 
 | ||||||
|         # mark top most level process as root actor |         # mark top most level process as root actor | ||||||
|         _state._runtime_vars['_is_root'] = True |         _state._runtime_vars['_is_root'] = True | ||||||
|  | @ -260,14 +274,20 @@ async def open_root_actor( | ||||||
|                 DeprecationWarning, |                 DeprecationWarning, | ||||||
|                 stacklevel=2, |                 stacklevel=2, | ||||||
|             ) |             ) | ||||||
|             registry_addrs = [arbiter_addr] |             uw_reg_addrs = [arbiter_addr] | ||||||
| 
 | 
 | ||||||
|         if not registry_addrs: |         uw_reg_addrs = registry_addrs | ||||||
|             registry_addrs: list[UnwrappedAddress] = default_lo_addrs( |         if not uw_reg_addrs: | ||||||
|  |             uw_reg_addrs: list[UnwrappedAddress] = default_lo_addrs( | ||||||
|                 enable_transports |                 enable_transports | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         assert registry_addrs |         # must exist by now since all below code is dependent | ||||||
|  |         assert uw_reg_addrs | ||||||
|  |         registry_addrs: list[Address] = [ | ||||||
|  |             wrap_address(uw_addr) | ||||||
|  |             for uw_addr in uw_reg_addrs | ||||||
|  |         ] | ||||||
| 
 | 
 | ||||||
|         loglevel = ( |         loglevel = ( | ||||||
|             loglevel |             loglevel | ||||||
|  | @ -283,7 +303,7 @@ async def open_root_actor( | ||||||
| 
 | 
 | ||||||
|             # expose internal debug module to every actor allowing for |             # expose internal debug module to every actor allowing for | ||||||
|             # use of ``await tractor.pause()`` |             # use of ``await tractor.pause()`` | ||||||
|             enable_modules.append('tractor.devx._debug') |             enable_modules.append('tractor.devx.debug._tty_lock') | ||||||
| 
 | 
 | ||||||
|             # if debug mode get's enabled *at least* use that level of |             # if debug mode get's enabled *at least* use that level of | ||||||
|             # logging for some informative console prompts. |             # logging for some informative console prompts. | ||||||
|  | @ -316,10 +336,10 @@ async def open_root_actor( | ||||||
|             enable_stack_on_sig() |             enable_stack_on_sig() | ||||||
| 
 | 
 | ||||||
|         # closed into below ping task-func |         # closed into below ping task-func | ||||||
|         ponged_addrs: list[UnwrappedAddress] = [] |         ponged_addrs: list[Address] = [] | ||||||
| 
 | 
 | ||||||
|         async def ping_tpt_socket( |         async def ping_tpt_socket( | ||||||
|             addr: UnwrappedAddress, |             addr: Address, | ||||||
|             timeout: float = 1, |             timeout: float = 1, | ||||||
|         ) -> None: |         ) -> None: | ||||||
|             ''' |             ''' | ||||||
|  | @ -339,17 +359,22 @@ async def open_root_actor( | ||||||
|                 # be better to eventually have a "discovery" protocol |                 # be better to eventually have a "discovery" protocol | ||||||
|                 # with basic handshake instead? |                 # with basic handshake instead? | ||||||
|                 with trio.move_on_after(timeout): |                 with trio.move_on_after(timeout): | ||||||
|                     async with _connect_chan(addr): |                     async with _connect_chan(addr.unwrap()): | ||||||
|                         ponged_addrs.append(addr) |                         ponged_addrs.append(addr) | ||||||
| 
 | 
 | ||||||
|             except OSError: |             except OSError: | ||||||
|                 # TODO: make this a "discovery" log level? |                 # ?TODO, make this a "discovery" log level? | ||||||
|                 logger.info( |                 logger.info( | ||||||
|                     f'No actor registry found @ {addr}\n' |                     f'No root-actor registry found @ {addr!r}\n' | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|  |         # !TODO, this is basically just another (abstract) | ||||||
|  |         # happy-eyeballs, so we should try for formalize it somewhere | ||||||
|  |         # in a `.[_]discovery` ya? | ||||||
|  |         # | ||||||
|         async with trio.open_nursery() as tn: |         async with trio.open_nursery() as tn: | ||||||
|             for addr in registry_addrs: |             for uw_addr in uw_reg_addrs: | ||||||
|  |                 addr: Address = wrap_address(uw_addr) | ||||||
|                 tn.start_soon( |                 tn.start_soon( | ||||||
|                     ping_tpt_socket, |                     ping_tpt_socket, | ||||||
|                     addr, |                     addr, | ||||||
|  | @ -371,31 +396,35 @@ async def open_root_actor( | ||||||
|                 f'Registry(s) seem(s) to exist @ {ponged_addrs}' |                 f'Registry(s) seem(s) to exist @ {ponged_addrs}' | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             actor = Actor( |             actor = _runtime.Actor( | ||||||
|                 name=name or 'anonymous', |                 name=name or 'anonymous', | ||||||
|                 uuid=mk_uuid(), |                 uuid=mk_uuid(), | ||||||
|                 registry_addrs=ponged_addrs, |                 registry_addrs=ponged_addrs, | ||||||
|                 loglevel=loglevel, |                 loglevel=loglevel, | ||||||
|                 enable_modules=enable_modules, |                 enable_modules=enable_modules, | ||||||
|             ) |             ) | ||||||
|             # DO NOT use the registry_addrs as the transport server |             # **DO NOT** use the registry_addrs as the | ||||||
|             # addrs for this new non-registar, root-actor. |             # ipc-transport-server's bind-addrs as this is | ||||||
|  |             # a new NON-registrar, ROOT-actor. | ||||||
|  |             # | ||||||
|  |             # XXX INSTEAD, bind random addrs using the same tpt | ||||||
|  |             # proto. | ||||||
|             for addr in ponged_addrs: |             for addr in ponged_addrs: | ||||||
|                 waddr: Address = wrap_address(addr) |  | ||||||
|                 trans_bind_addrs.append( |                 trans_bind_addrs.append( | ||||||
|                     waddr.get_random(bindspace=waddr.bindspace) |                     addr.get_random( | ||||||
|  |                         bindspace=addr.bindspace, | ||||||
|  |                     ) | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|         # Start this local actor as the "registrar", aka a regular |         # Start this local actor as the "registrar", aka a regular | ||||||
|         # actor who manages the local registry of "mailboxes" of |         # actor who manages the local registry of "mailboxes" of | ||||||
|         # other process-tree-local sub-actors. |         # other process-tree-local sub-actors. | ||||||
|         else: |         else: | ||||||
| 
 |  | ||||||
|             # NOTE that if the current actor IS THE REGISTAR, the |             # NOTE that if the current actor IS THE REGISTAR, the | ||||||
|             # following init steps are taken: |             # following init steps are taken: | ||||||
|             # - the tranport layer server is bound to each addr |             # - the tranport layer server is bound to each addr | ||||||
|             #   pair defined in provided registry_addrs, or the default. |             #   pair defined in provided registry_addrs, or the default. | ||||||
|             trans_bind_addrs = registry_addrs |             trans_bind_addrs = uw_reg_addrs | ||||||
| 
 | 
 | ||||||
|             # - it is normally desirable for any registrar to stay up |             # - it is normally desirable for any registrar to stay up | ||||||
|             #   indefinitely until either all registered (child/sub) |             #   indefinitely until either all registered (child/sub) | ||||||
|  | @ -406,7 +435,8 @@ async def open_root_actor( | ||||||
|             # https://github.com/goodboy/tractor/pull/348 |             # https://github.com/goodboy/tractor/pull/348 | ||||||
|             # https://github.com/goodboy/tractor/issues/296 |             # https://github.com/goodboy/tractor/issues/296 | ||||||
| 
 | 
 | ||||||
|             actor = Arbiter( |             # TODO: rename as `RootActor` or is that even necessary? | ||||||
|  |             actor = _runtime.Arbiter( | ||||||
|                 name=name or 'registrar', |                 name=name or 'registrar', | ||||||
|                 uuid=mk_uuid(), |                 uuid=mk_uuid(), | ||||||
|                 registry_addrs=registry_addrs, |                 registry_addrs=registry_addrs, | ||||||
|  | @ -418,6 +448,16 @@ async def open_root_actor( | ||||||
|             # `.trio.run()`. |             # `.trio.run()`. | ||||||
|             actor._infected_aio = _state._runtime_vars['_is_infected_aio'] |             actor._infected_aio = _state._runtime_vars['_is_infected_aio'] | ||||||
| 
 | 
 | ||||||
|  |         # NOTE, only set the loopback addr for the | ||||||
|  |         # process-tree-global "root" mailbox since all sub-actors | ||||||
|  |         # should be able to speak to their root actor over that | ||||||
|  |         # channel. | ||||||
|  |         raddrs: list[Address] = _state._runtime_vars['_root_addrs'] | ||||||
|  |         raddrs.extend(trans_bind_addrs) | ||||||
|  |         # TODO, remove once we have also removed all usage; | ||||||
|  |         # eventually all (root-)registry apis should expect > 1 addr. | ||||||
|  |         _state._runtime_vars['_root_mailbox'] = raddrs[0] | ||||||
|  | 
 | ||||||
|         # Start up main task set via core actor-runtime nurseries. |         # Start up main task set via core actor-runtime nurseries. | ||||||
|         try: |         try: | ||||||
|             # assign process-local actor |             # assign process-local actor | ||||||
|  | @ -425,21 +465,28 @@ async def open_root_actor( | ||||||
| 
 | 
 | ||||||
|             # start local channel-server and fake the portal API |             # start local channel-server and fake the portal API | ||||||
|             # NOTE: this won't block since we provide the nursery |             # NOTE: this won't block since we provide the nursery | ||||||
|             ml_addrs_str: str = '\n'.join( |             report: str = f'Starting actor-runtime for {actor.aid.reprol()!r}\n' | ||||||
|                 f'@{addr}' for addr in trans_bind_addrs |             if reg_addrs := actor.registry_addrs: | ||||||
|             ) |                 report += ( | ||||||
|             logger.info( |                     '-> Opening new registry @ ' | ||||||
|                 f'Starting local {actor.uid} on the following transport addrs:\n' |                     + | ||||||
|                 f'{ml_addrs_str}' |                     '\n'.join( | ||||||
|             ) |                         f'{addr}' for addr in reg_addrs | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |             logger.info(f'{report}\n') | ||||||
| 
 | 
 | ||||||
|             # start the actor runtime in a new task |             # start runtime in a bg sub-task, yield to caller. | ||||||
|             async with trio.open_nursery( |             async with ( | ||||||
|                 strict_exception_groups=False, |                 collapse_eg(), | ||||||
|                 # ^XXX^ TODO? instead unpack any RAE as per "loose" style? |                 trio.open_nursery() as root_tn, | ||||||
|             ) as nursery: |  | ||||||
| 
 | 
 | ||||||
|                 # ``_runtime.async_main()`` creates an internal nursery |                 # ?TODO? finally-footgun below? | ||||||
|  |                 # -> see note on why shielding. | ||||||
|  |                 # maybe_raise_from_masking_exc(), | ||||||
|  |             ): | ||||||
|  |                 actor._root_tn = root_tn | ||||||
|  |                 # `_runtime.async_main()` creates an internal nursery | ||||||
|                 # and blocks here until any underlying actor(-process) |                 # and blocks here until any underlying actor(-process) | ||||||
|                 # tree has terminated thereby conducting so called |                 # tree has terminated thereby conducting so called | ||||||
|                 # "end-to-end" structured concurrency throughout an |                 # "end-to-end" structured concurrency throughout an | ||||||
|  | @ -447,9 +494,9 @@ async def open_root_actor( | ||||||
|                 # "actor runtime" primitives are SC-compat and thus all |                 # "actor runtime" primitives are SC-compat and thus all | ||||||
|                 # transitively spawned actors/processes must be as |                 # transitively spawned actors/processes must be as | ||||||
|                 # well. |                 # well. | ||||||
|                 await nursery.start( |                 await root_tn.start( | ||||||
|                     partial( |                     partial( | ||||||
|                         async_main, |                         _runtime.async_main, | ||||||
|                         actor, |                         actor, | ||||||
|                         accept_addrs=trans_bind_addrs, |                         accept_addrs=trans_bind_addrs, | ||||||
|                         parent_addr=None |                         parent_addr=None | ||||||
|  | @ -465,7 +512,7 @@ async def open_root_actor( | ||||||
|                     # TODO, in beginning to handle the subsubactor with |                     # TODO, in beginning to handle the subsubactor with | ||||||
|                     # crashed grandparent cases.. |                     # crashed grandparent cases.. | ||||||
|                     # |                     # | ||||||
|                     # was_locked: bool = await _debug.maybe_wait_for_debugger( |                     # was_locked: bool = await debug.maybe_wait_for_debugger( | ||||||
|                     #     child_in_debug=True, |                     #     child_in_debug=True, | ||||||
|                     # ) |                     # ) | ||||||
|                     # XXX NOTE XXX see equiv note inside |                     # XXX NOTE XXX see equiv note inside | ||||||
|  | @ -473,10 +520,15 @@ async def open_root_actor( | ||||||
|                     # non-root or root-that-opened-this-mahually case we |                     # non-root or root-that-opened-this-mahually case we | ||||||
|                     # wait for the local actor-nursery to exit before |                     # wait for the local actor-nursery to exit before | ||||||
|                     # exiting the transport channel handler. |                     # exiting the transport channel handler. | ||||||
|                     entered: bool = await _debug._maybe_enter_pm( |                     entered: bool = await debug._maybe_enter_pm( | ||||||
|                         err, |                         err, | ||||||
|                         api_frame=inspect.currentframe(), |                         api_frame=inspect.currentframe(), | ||||||
|                         debug_filter=debug_filter, |                         debug_filter=debug_filter, | ||||||
|  | 
 | ||||||
|  |                         # XXX NOTE, required to debug root-actor | ||||||
|  |                         # crashes under cancellation conditions; so | ||||||
|  |                         # most of them! | ||||||
|  |                         shield=root_tn.cancel_scope.cancel_called, | ||||||
|                     ) |                     ) | ||||||
| 
 | 
 | ||||||
|                     if ( |                     if ( | ||||||
|  | @ -497,7 +549,7 @@ async def open_root_actor( | ||||||
|                     raise |                     raise | ||||||
| 
 | 
 | ||||||
|                 finally: |                 finally: | ||||||
|                     # NOTE: not sure if we'll ever need this but it's |                     # NOTE/TODO?, not sure if we'll ever need this but it's | ||||||
|                     # possibly better for even more determinism? |                     # possibly better for even more determinism? | ||||||
|                     # logger.cancel( |                     # logger.cancel( | ||||||
|                     #     f'Waiting on {len(nurseries)} nurseries in root..') |                     #     f'Waiting on {len(nurseries)} nurseries in root..') | ||||||
|  | @ -506,12 +558,22 @@ async def open_root_actor( | ||||||
|                     #     for an in nurseries: |                     #     for an in nurseries: | ||||||
|                     #         tempn.start_soon(an.exited.wait) |                     #         tempn.start_soon(an.exited.wait) | ||||||
| 
 | 
 | ||||||
|  |                     op_nested_actor_repr: str = _pformat.nest_from_op( | ||||||
|  |                         input_op='>) ', | ||||||
|  |                         text=actor.pformat(), | ||||||
|  |                         nest_prefix='|_', | ||||||
|  |                     ) | ||||||
|                     logger.info( |                     logger.info( | ||||||
|                         f'Closing down root actor\n' |                         f'Closing down root actor\n' | ||||||
|                         f'>)\n' |                         f'{op_nested_actor_repr}' | ||||||
|                         f'|_{actor}\n' |  | ||||||
|                     ) |                     ) | ||||||
|                     await actor.cancel(None)  # self cancel |                     # XXX, THIS IS A *finally-footgun*! | ||||||
|  |                     # (also mentioned in with-block above) | ||||||
|  |                     # -> though already shields iternally it can | ||||||
|  |                     # taskc here and mask underlying errors raised in | ||||||
|  |                     # the try-block above? | ||||||
|  |                     with trio.CancelScope(shield=True): | ||||||
|  |                         await actor.cancel(None)  # self cancel | ||||||
|         finally: |         finally: | ||||||
|             # revert all process-global runtime state |             # revert all process-global runtime state | ||||||
|             if ( |             if ( | ||||||
|  | @ -524,10 +586,16 @@ async def open_root_actor( | ||||||
|             _state._current_actor = None |             _state._current_actor = None | ||||||
|             _state._last_actor_terminated = actor |             _state._last_actor_terminated = actor | ||||||
| 
 | 
 | ||||||
|             logger.runtime( |             sclang_repr: str = _pformat.nest_from_op( | ||||||
|  |                 input_op=')>', | ||||||
|  |                 text=actor.pformat(), | ||||||
|  |                 nest_prefix='|_', | ||||||
|  |                 nest_indent=1, | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             logger.info( | ||||||
|                 f'Root actor terminated\n' |                 f'Root actor terminated\n' | ||||||
|                 f')>\n' |                 f'{sclang_repr}' | ||||||
|                 f' |_{actor}\n' |  | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										279
									
								
								tractor/_rpc.py
								
								
								
								
							
							
						
						
									
										279
									
								
								tractor/_rpc.py
								
								
								
								
							|  | @ -37,6 +37,7 @@ import warnings | ||||||
| 
 | 
 | ||||||
| import trio | import trio | ||||||
| from trio import ( | from trio import ( | ||||||
|  |     Cancelled, | ||||||
|     CancelScope, |     CancelScope, | ||||||
|     Nursery, |     Nursery, | ||||||
|     TaskStatus, |     TaskStatus, | ||||||
|  | @ -52,13 +53,18 @@ from ._exceptions import ( | ||||||
|     ModuleNotExposed, |     ModuleNotExposed, | ||||||
|     MsgTypeError, |     MsgTypeError, | ||||||
|     TransportClosed, |     TransportClosed, | ||||||
|     is_multi_cancelled, |  | ||||||
|     pack_error, |     pack_error, | ||||||
|     unpack_error, |     unpack_error, | ||||||
| ) | ) | ||||||
|  | from .trionics import ( | ||||||
|  |     collapse_eg, | ||||||
|  |     is_multi_cancelled, | ||||||
|  |     maybe_raise_from_masking_exc, | ||||||
|  | ) | ||||||
| from .devx import ( | from .devx import ( | ||||||
|     _debug, |     debug, | ||||||
|     add_div, |     add_div, | ||||||
|  |     pformat as _pformat, | ||||||
| ) | ) | ||||||
| from . import _state | from . import _state | ||||||
| from .log import get_logger | from .log import get_logger | ||||||
|  | @ -67,7 +73,7 @@ from .msg import ( | ||||||
|     MsgCodec, |     MsgCodec, | ||||||
|     PayloadT, |     PayloadT, | ||||||
|     NamespacePath, |     NamespacePath, | ||||||
|     # pretty_struct, |     pretty_struct, | ||||||
|     _ops as msgops, |     _ops as msgops, | ||||||
| ) | ) | ||||||
| from tractor.msg.types import ( | from tractor.msg.types import ( | ||||||
|  | @ -215,11 +221,18 @@ async def _invoke_non_context( | ||||||
|             task_status.started(ctx) |             task_status.started(ctx) | ||||||
|             result = await coro |             result = await coro | ||||||
|             fname: str = func.__name__ |             fname: str = func.__name__ | ||||||
|  | 
 | ||||||
|  |             op_nested_task: str = _pformat.nest_from_op( | ||||||
|  |                 input_op=f')> cid: {ctx.cid!r}', | ||||||
|  |                 text=f'{ctx._task}', | ||||||
|  |                 nest_indent=1,  # under > | ||||||
|  |             ) | ||||||
|             log.runtime( |             log.runtime( | ||||||
|                 'RPC complete:\n' |                 f'RPC task complete\n' | ||||||
|                 f'task: {ctx._task}\n' |                 f'\n' | ||||||
|                 f'|_cid={ctx.cid}\n' |                 f'{op_nested_task}\n' | ||||||
|                 f'|_{fname}() -> {pformat(result)}\n' |                 f'\n' | ||||||
|  |                 f')> {fname}() -> {pformat(result)}\n' | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             # NOTE: only send result if we know IPC isn't down |             # NOTE: only send result if we know IPC isn't down | ||||||
|  | @ -250,7 +263,7 @@ async def _errors_relayed_via_ipc( | ||||||
|     ctx: Context, |     ctx: Context, | ||||||
|     is_rpc: bool, |     is_rpc: bool, | ||||||
| 
 | 
 | ||||||
|     hide_tb: bool = False, |     hide_tb: bool = True, | ||||||
|     debug_kbis: bool = False, |     debug_kbis: bool = False, | ||||||
|     task_status: TaskStatus[ |     task_status: TaskStatus[ | ||||||
|         Context | BaseException |         Context | BaseException | ||||||
|  | @ -266,7 +279,7 @@ async def _errors_relayed_via_ipc( | ||||||
| 
 | 
 | ||||||
|     # TODO: a debug nursery when in debug mode! |     # TODO: a debug nursery when in debug mode! | ||||||
|     # async with maybe_open_debugger_nursery() as debug_tn: |     # async with maybe_open_debugger_nursery() as debug_tn: | ||||||
|     # => see matching comment in side `._debug._pause()` |     # => see matching comment in side `.debug._pause()` | ||||||
|     rpc_err: BaseException|None = None |     rpc_err: BaseException|None = None | ||||||
|     try: |     try: | ||||||
|         yield  # run RPC invoke body |         yield  # run RPC invoke body | ||||||
|  | @ -318,7 +331,7 @@ async def _errors_relayed_via_ipc( | ||||||
|                     'RPC task crashed, attempting to enter debugger\n' |                     'RPC task crashed, attempting to enter debugger\n' | ||||||
|                     f'|_{ctx}' |                     f'|_{ctx}' | ||||||
|                 ) |                 ) | ||||||
|                 entered_debug = await _debug._maybe_enter_pm( |                 entered_debug = await debug._maybe_enter_pm( | ||||||
|                     err, |                     err, | ||||||
|                     api_frame=inspect.currentframe(), |                     api_frame=inspect.currentframe(), | ||||||
|                 ) |                 ) | ||||||
|  | @ -371,13 +384,13 @@ async def _errors_relayed_via_ipc( | ||||||
| 
 | 
 | ||||||
|     # RPC task bookeeping. |     # RPC task bookeeping. | ||||||
|     # since RPC tasks are scheduled inside a flat |     # since RPC tasks are scheduled inside a flat | ||||||
|     # `Actor._service_n`, we add "handles" to each such that |     # `Actor._service_tn`, we add "handles" to each such that | ||||||
|     # they can be individually ccancelled. |     # they can be individually ccancelled. | ||||||
|     finally: |     finally: | ||||||
| 
 | 
 | ||||||
|         # if the error is not from user code and instead a failure |         # if the error is not from user code and instead a failure of | ||||||
|         # of a runtime RPC or transport failure we do prolly want to |         # an internal-runtime-RPC or IPC-connection, we do (prolly) want | ||||||
|         # show this frame |         # to show this frame! | ||||||
|         if ( |         if ( | ||||||
|             rpc_err |             rpc_err | ||||||
|             and ( |             and ( | ||||||
|  | @ -449,7 +462,7 @@ async def _invoke( | ||||||
|     connected IPC channel. |     connected IPC channel. | ||||||
| 
 | 
 | ||||||
|     This is the core "RPC" `trio.Task` scheduling machinery used to start every |     This is the core "RPC" `trio.Task` scheduling machinery used to start every | ||||||
|     remotely invoked function, normally in `Actor._service_n: Nursery`. |     remotely invoked function, normally in `Actor._service_tn: Nursery`. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     __tracebackhide__: bool = hide_tb |     __tracebackhide__: bool = hide_tb | ||||||
|  | @ -462,7 +475,7 @@ async def _invoke( | ||||||
|     ): |     ): | ||||||
|         # XXX for .pause_from_sync()` usage we need to make sure |         # XXX for .pause_from_sync()` usage we need to make sure | ||||||
|         # `greenback` is boostrapped in the subactor! |         # `greenback` is boostrapped in the subactor! | ||||||
|         await _debug.maybe_init_greenback() |         await debug.maybe_init_greenback() | ||||||
| 
 | 
 | ||||||
|     # TODO: possibly a specially formatted traceback |     # TODO: possibly a specially formatted traceback | ||||||
|     # (not sure what typing is for this..)? |     # (not sure what typing is for this..)? | ||||||
|  | @ -616,32 +629,40 @@ async def _invoke( | ||||||
|         #  -> the below scope is never exposed to the |         #  -> the below scope is never exposed to the | ||||||
|         #     `@context` marked RPC function. |         #     `@context` marked RPC function. | ||||||
|         # - `._portal` is never set. |         # - `._portal` is never set. | ||||||
|  |         scope_err: BaseException|None = None | ||||||
|         try: |         try: | ||||||
|             tn: trio.Nursery |             # TODO: better `trionics` primitive/tooling usage here! | ||||||
|  |             # -[ ] should would be nice to have our `TaskMngr` | ||||||
|  |             #   nursery here! | ||||||
|  |             # -[ ] payload value checking like we do with | ||||||
|  |             #   `.started()` such that the debbuger can engage | ||||||
|  |             #   here in the child task instead of waiting for the | ||||||
|  |             #   parent to crash with it's own MTE.. | ||||||
|  |             # | ||||||
|  |             tn: Nursery | ||||||
|             rpc_ctx_cs: CancelScope |             rpc_ctx_cs: CancelScope | ||||||
|             async with ( |             async with ( | ||||||
|                 trio.open_nursery( |                 collapse_eg(hide_tb=False), | ||||||
|                     strict_exception_groups=False, |                 trio.open_nursery() as tn, | ||||||
|                     # ^XXX^ TODO? instead unpack any RAE as per "loose" style? |  | ||||||
| 
 |  | ||||||
|                 ) as tn, |  | ||||||
|                 msgops.maybe_limit_plds( |                 msgops.maybe_limit_plds( | ||||||
|                     ctx=ctx, |                     ctx=ctx, | ||||||
|                     spec=ctx_meta.get('pld_spec'), |                     spec=ctx_meta.get('pld_spec'), | ||||||
|                     dec_hook=ctx_meta.get('dec_hook'), |                     dec_hook=ctx_meta.get('dec_hook'), | ||||||
|                 ), |                 ), | ||||||
|  | 
 | ||||||
|  |                 # XXX NOTE, this being the "most embedded" | ||||||
|  |                 # scope ensures unasking of the `await coro` below | ||||||
|  |                 # *should* never be interfered with!! | ||||||
|  |                 maybe_raise_from_masking_exc( | ||||||
|  |                     tn=tn, | ||||||
|  |                     unmask_from=Cancelled, | ||||||
|  |                 ) as _mbme,  # maybe boxed masked exc | ||||||
|             ): |             ): | ||||||
|                 ctx._scope_nursery = tn |                 ctx._scope_nursery = tn | ||||||
|                 rpc_ctx_cs = ctx._scope = tn.cancel_scope |                 rpc_ctx_cs = ctx._scope = tn.cancel_scope | ||||||
|                 task_status.started(ctx) |                 task_status.started(ctx) | ||||||
| 
 | 
 | ||||||
|                 # TODO: better `trionics` tooling: |                 # invoke user endpoint fn. | ||||||
|                 # -[ ] should would be nice to have our `TaskMngr` |  | ||||||
|                 #   nursery here! |  | ||||||
|                 # -[ ] payload value checking like we do with |  | ||||||
|                 #   `.started()` such that the debbuger can engage |  | ||||||
|                 #   here in the child task instead of waiting for the |  | ||||||
|                 #   parent to crash with it's own MTE.. |  | ||||||
|                 res: Any|PayloadT = await coro |                 res: Any|PayloadT = await coro | ||||||
|                 return_msg: Return|CancelAck = return_msg_type( |                 return_msg: Return|CancelAck = return_msg_type( | ||||||
|                     cid=cid, |                     cid=cid, | ||||||
|  | @ -651,7 +672,8 @@ async def _invoke( | ||||||
|                 ctx._result = res |                 ctx._result = res | ||||||
|                 log.runtime( |                 log.runtime( | ||||||
|                     f'Sending result msg and exiting {ctx.side!r}\n' |                     f'Sending result msg and exiting {ctx.side!r}\n' | ||||||
|                     f'{return_msg}\n' |                     f'\n' | ||||||
|  |                     f'{pretty_struct.pformat(return_msg)}\n' | ||||||
|                 ) |                 ) | ||||||
|                 await chan.send(return_msg) |                 await chan.send(return_msg) | ||||||
| 
 | 
 | ||||||
|  | @ -743,43 +765,52 @@ async def _invoke( | ||||||
|             BaseExceptionGroup, |             BaseExceptionGroup, | ||||||
|             BaseException, |             BaseException, | ||||||
|             trio.Cancelled, |             trio.Cancelled, | ||||||
| 
 |         ) as _scope_err: | ||||||
|         ) as scope_error: |             scope_err = _scope_err | ||||||
|             if ( |             if ( | ||||||
|                 isinstance(scope_error, RuntimeError) |                 isinstance(scope_err, RuntimeError) | ||||||
|                 and scope_error.args |                 and | ||||||
|                 and 'Cancel scope stack corrupted' in scope_error.args[0] |                 scope_err.args | ||||||
|  |                 and | ||||||
|  |                 'Cancel scope stack corrupted' in scope_err.args[0] | ||||||
|             ): |             ): | ||||||
|                 log.exception('Cancel scope stack corrupted!?\n') |                 log.exception('Cancel scope stack corrupted!?\n') | ||||||
|                 # _debug.mk_pdb().set_trace() |                 # debug.mk_pdb().set_trace() | ||||||
| 
 | 
 | ||||||
|             # always set this (child) side's exception as the |             # always set this (child) side's exception as the | ||||||
|             # local error on the context |             # local error on the context | ||||||
|             ctx._local_error: BaseException = scope_error |             ctx._local_error: BaseException = scope_err | ||||||
|             # ^-TODO-^ question, |             # ^-TODO-^ question, | ||||||
|             # does this matter other then for |             # does this matter other then for | ||||||
|             # consistentcy/testing? |             # consistentcy/testing? | ||||||
|             # |_ no user code should be in this scope at this point |             # |_ no user code should be in this scope at this point | ||||||
|             #    AND we already set this in the block below? |             #    AND we already set this in the block below? | ||||||
| 
 | 
 | ||||||
|             # if a remote error was set then likely the |             # XXX if a remote error was set then likely the | ||||||
|             # exception group was raised due to that, so |             # exc group was raised due to that, so | ||||||
|             # and we instead raise that error immediately! |             # and we instead raise that error immediately! | ||||||
|             ctx.maybe_raise() |             maybe_re: ( | ||||||
|  |                 ContextCancelled|RemoteActorError | ||||||
|  |             ) = ctx.maybe_raise() | ||||||
|  |             if maybe_re: | ||||||
|  |                 log.cancel( | ||||||
|  |                     f'Suppressing remote-exc from peer,\n' | ||||||
|  |                     f'{maybe_re!r}\n' | ||||||
|  |                 ) | ||||||
| 
 | 
 | ||||||
|             # maybe TODO: pack in come kinda |             # maybe TODO: pack in come kinda | ||||||
|             # `trio.Cancelled.__traceback__` here so they can be |             # `trio.Cancelled.__traceback__` here so they can be | ||||||
|             # unwrapped and displayed on the caller side? no se.. |             # unwrapped and displayed on the caller side? no se.. | ||||||
|             raise |             raise scope_err | ||||||
| 
 | 
 | ||||||
|         # `@context` entrypoint task bookeeping. |         # `@context` entrypoint task bookeeping. | ||||||
|         # i.e. only pop the context tracking if used ;) |         # i.e. only pop the context tracking if used ;) | ||||||
|         finally: |         finally: | ||||||
|             assert chan.uid |             assert chan.aid | ||||||
| 
 | 
 | ||||||
|             # don't pop the local context until we know the |             # don't pop the local context until we know the | ||||||
|             # associated child isn't in debug any more |             # associated child isn't in debug any more | ||||||
|             await _debug.maybe_wait_for_debugger() |             await debug.maybe_wait_for_debugger() | ||||||
|             ctx: Context = actor._contexts.pop(( |             ctx: Context = actor._contexts.pop(( | ||||||
|                 chan.uid, |                 chan.uid, | ||||||
|                 cid, |                 cid, | ||||||
|  | @ -792,26 +823,49 @@ async def _invoke( | ||||||
|                 f'after having {ctx.repr_state!r}\n' |                 f'after having {ctx.repr_state!r}\n' | ||||||
|             ) |             ) | ||||||
|             if merr: |             if merr: | ||||||
| 
 |  | ||||||
|                 logmeth: Callable = log.error |                 logmeth: Callable = log.error | ||||||
|                 if isinstance(merr, ContextCancelled): |                 if ( | ||||||
|                     logmeth: Callable = log.runtime |                     # ctxc: by `Context.cancel()` | ||||||
|  |                     isinstance(merr, ContextCancelled) | ||||||
| 
 | 
 | ||||||
|                 if not isinstance(merr, RemoteActorError): |                     # out-of-layer cancellation, one of: | ||||||
|                     tb_str: str = ''.join(traceback.format_exception(merr)) |                     # - actorc: by `Portal.cancel_actor()` | ||||||
|  |                     # - OSc: by SIGINT or `Process.signal()` | ||||||
|  |                     or ( | ||||||
|  |                         isinstance(merr, trio.Cancelled) | ||||||
|  |                         and | ||||||
|  |                         ctx.canceller | ||||||
|  |                     ) | ||||||
|  |                 ): | ||||||
|  |                     logmeth: Callable = log.cancel | ||||||
|  |                     descr_str += ( | ||||||
|  |                         f' with {merr!r}\n' | ||||||
|  |                     ) | ||||||
|  | 
 | ||||||
|  |                 elif ( | ||||||
|  |                     not isinstance(merr, RemoteActorError) | ||||||
|  |                 ): | ||||||
|  |                     tb_str: str = ''.join( | ||||||
|  |                         traceback.format_exception(merr) | ||||||
|  |                     ) | ||||||
|                     descr_str += ( |                     descr_str += ( | ||||||
|                         f'\n{merr!r}\n'  # needed? |                         f'\n{merr!r}\n'  # needed? | ||||||
|                         f'{tb_str}\n' |                         f'{tb_str}\n' | ||||||
|                     ) |                     ) | ||||||
|                 else: |                 else: | ||||||
|                     descr_str += f'\n{merr!r}\n' |                     descr_str += ( | ||||||
|  |                         f'{merr!r}\n' | ||||||
|  |                     ) | ||||||
|             else: |             else: | ||||||
|                 descr_str += f'\nand final result {ctx.outcome!r}\n' |                 descr_str += ( | ||||||
|  |                     f'\n' | ||||||
|  |                     f'with final result {ctx.outcome!r}\n' | ||||||
|  |                 ) | ||||||
| 
 | 
 | ||||||
|             logmeth( |             logmeth( | ||||||
|                 message |                 f'{message}\n' | ||||||
|                 + |                 f'\n' | ||||||
|                 descr_str |                 f'{descr_str}\n' | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -869,7 +923,6 @@ async def try_ship_error_to_remote( | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| async def process_messages( | async def process_messages( | ||||||
|     actor: Actor, |  | ||||||
|     chan: Channel, |     chan: Channel, | ||||||
|     shield: bool = False, |     shield: bool = False, | ||||||
|     task_status: TaskStatus[CancelScope] = trio.TASK_STATUS_IGNORED, |     task_status: TaskStatus[CancelScope] = trio.TASK_STATUS_IGNORED, | ||||||
|  | @ -883,7 +936,7 @@ async def process_messages( | ||||||
| 
 | 
 | ||||||
|     Receive (multiplexed) per-`Channel` RPC requests as msgs from |     Receive (multiplexed) per-`Channel` RPC requests as msgs from | ||||||
|     remote processes; schedule target async funcs as local |     remote processes; schedule target async funcs as local | ||||||
|     `trio.Task`s inside the `Actor._service_n: Nursery`. |     `trio.Task`s inside the `Actor._service_tn: Nursery`. | ||||||
| 
 | 
 | ||||||
|     Depending on msg type, non-`cmd` (task spawning/starting) |     Depending on msg type, non-`cmd` (task spawning/starting) | ||||||
|     request payloads (eg. `started`, `yield`, `return`, `error`) |     request payloads (eg. `started`, `yield`, `return`, `error`) | ||||||
|  | @ -907,7 +960,8 @@ async def process_messages( | ||||||
|       (as utilized inside `Portal.cancel_actor()` ). |       (as utilized inside `Portal.cancel_actor()` ). | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     assert actor._service_n  # runtime state sanity |     actor: Actor = _state.current_actor() | ||||||
|  |     assert actor._service_tn  # runtime state sanity | ||||||
| 
 | 
 | ||||||
|     # TODO: once `trio` get's an "obvious way" for req/resp we |     # TODO: once `trio` get's an "obvious way" for req/resp we | ||||||
|     # should use it? |     # should use it? | ||||||
|  | @ -978,12 +1032,10 @@ async def process_messages( | ||||||
|                         cid=cid, |                         cid=cid, | ||||||
|                         kwargs=kwargs, |                         kwargs=kwargs, | ||||||
|                     ): |                     ): | ||||||
|                         kwargs |= {'req_chan': chan} |  | ||||||
| 
 |  | ||||||
|                         # XXX NOTE XXX don't start entire actor |                         # XXX NOTE XXX don't start entire actor | ||||||
|                         # runtime cancellation if this actor is |                         # runtime cancellation if this actor is | ||||||
|                         # currently in debug mode! |                         # currently in debug mode! | ||||||
|                         pdb_complete: trio.Event|None = _debug.DebugStatus.repl_release |                         pdb_complete: trio.Event|None = debug.DebugStatus.repl_release | ||||||
|                         if pdb_complete: |                         if pdb_complete: | ||||||
|                             await pdb_complete.wait() |                             await pdb_complete.wait() | ||||||
| 
 | 
 | ||||||
|  | @ -998,14 +1050,14 @@ async def process_messages( | ||||||
|                                 cid, |                                 cid, | ||||||
|                                 chan, |                                 chan, | ||||||
|                                 actor.cancel, |                                 actor.cancel, | ||||||
|                                 kwargs, |                                 kwargs | {'req_chan': chan}, | ||||||
|                                 is_rpc=False, |                                 is_rpc=False, | ||||||
|                                 return_msg_type=CancelAck, |                                 return_msg_type=CancelAck, | ||||||
|                             ) |                             ) | ||||||
| 
 | 
 | ||||||
|                         log.runtime( |                         log.runtime( | ||||||
|                             'Cancelling IPC transport msg-loop with peer:\n' |                             'Cancelling RPC-msg-loop with peer\n' | ||||||
|                             f'|_{chan}\n' |                             f'->c}} {chan.aid.reprol()}@[{chan.maddr}]\n' | ||||||
|                         ) |                         ) | ||||||
|                         loop_cs.cancel() |                         loop_cs.cancel() | ||||||
|                         break |                         break | ||||||
|  | @ -1018,7 +1070,7 @@ async def process_messages( | ||||||
|                     ): |                     ): | ||||||
|                         target_cid: str = kwargs['cid'] |                         target_cid: str = kwargs['cid'] | ||||||
|                         kwargs |= { |                         kwargs |= { | ||||||
|                             'requesting_uid': chan.uid, |                             'requesting_aid': chan.aid, | ||||||
|                             'ipc_msg': msg, |                             'ipc_msg': msg, | ||||||
| 
 | 
 | ||||||
|                             # XXX NOTE! ONLY the rpc-task-owning |                             # XXX NOTE! ONLY the rpc-task-owning | ||||||
|  | @ -1054,21 +1106,34 @@ async def process_messages( | ||||||
|                         ns=ns, |                         ns=ns, | ||||||
|                         func=funcname, |                         func=funcname, | ||||||
|                         kwargs=kwargs,  # type-spec this? see `msg.types` |                         kwargs=kwargs,  # type-spec this? see `msg.types` | ||||||
|                         uid=actorid, |                         uid=actor_uuid, | ||||||
|                     ): |                     ): | ||||||
|  |                         if actor_uuid != chan.aid.uid: | ||||||
|  |                             raise RuntimeError( | ||||||
|  |                                 f'IPC <Start> msg <-> chan.aid mismatch!?\n' | ||||||
|  |                                 f'Channel.aid = {chan.aid!r}\n' | ||||||
|  |                                 f'Start.uid = {actor_uuid!r}\n' | ||||||
|  |                             ) | ||||||
|  |                         # await debug.pause() | ||||||
|  |                         op_repr: str = 'Start <=) ' | ||||||
|  |                         req_repr: str = _pformat.nest_from_op( | ||||||
|  |                             input_op=op_repr, | ||||||
|  |                             op_suffix='', | ||||||
|  |                             nest_prefix='', | ||||||
|  |                             text=f'{chan}', | ||||||
|  | 
 | ||||||
|  |                             nest_indent=len(op_repr)-1, | ||||||
|  |                             rm_from_first_ln='<', | ||||||
|  |                             # ^XXX, subtract -1 to account for | ||||||
|  |                             # <Channel | ||||||
|  |                             # ^_chevron to be stripped | ||||||
|  |                         ) | ||||||
|                         start_status: str = ( |                         start_status: str = ( | ||||||
|                             'Handling RPC `Start` request\n' |                             'Handling RPC request\n' | ||||||
|                             f'<= peer: {actorid}\n\n' |                             f'{req_repr}\n' | ||||||
|                             f'  |_{chan}\n' |                             f'\n' | ||||||
|                             f'  |_cid: {cid}\n\n' |                             f'->{{ ipc-context-id: {cid!r}\n' | ||||||
|                             # f'  |_{ns}.{funcname}({kwargs})\n' |                             f'->{{ nsp for fn: `{ns}.{funcname}({kwargs})`\n' | ||||||
|                             f'>> {actor.uid}\n' |  | ||||||
|                             f'  |_{actor}\n' |  | ||||||
|                             f'   -> nsp: `{ns}.{funcname}({kwargs})`\n' |  | ||||||
| 
 |  | ||||||
|                             # f'  |_{ns}.{funcname}({kwargs})\n\n' |  | ||||||
| 
 |  | ||||||
|                             # f'{pretty_struct.pformat(msg)}\n' |  | ||||||
|                         ) |                         ) | ||||||
| 
 | 
 | ||||||
|                         # runtime-internal endpoint: `Actor.<funcname>` |                         # runtime-internal endpoint: `Actor.<funcname>` | ||||||
|  | @ -1097,10 +1162,6 @@ async def process_messages( | ||||||
|                                 await chan.send(err_msg) |                                 await chan.send(err_msg) | ||||||
|                                 continue |                                 continue | ||||||
| 
 | 
 | ||||||
|                         start_status += ( |  | ||||||
|                             f'   -> func: {func}\n' |  | ||||||
|                         ) |  | ||||||
| 
 |  | ||||||
|                         # schedule a task for the requested RPC function |                         # schedule a task for the requested RPC function | ||||||
|                         # in the actor's main "service nursery". |                         # in the actor's main "service nursery". | ||||||
|                         # |                         # | ||||||
|  | @ -1108,10 +1169,10 @@ async def process_messages( | ||||||
|                         # supervision isolation? would avoid having to |                         # supervision isolation? would avoid having to | ||||||
|                         # manage RPC tasks individually in `._rpc_tasks` |                         # manage RPC tasks individually in `._rpc_tasks` | ||||||
|                         # table? |                         # table? | ||||||
|                         start_status += '   -> scheduling new task..\n' |                         start_status += '->( scheduling new task..\n' | ||||||
|                         log.runtime(start_status) |                         log.runtime(start_status) | ||||||
|                         try: |                         try: | ||||||
|                             ctx: Context = await actor._service_n.start( |                             ctx: Context = await actor._service_tn.start( | ||||||
|                                 partial( |                                 partial( | ||||||
|                                     _invoke, |                                     _invoke, | ||||||
|                                     actor, |                                     actor, | ||||||
|  | @ -1192,12 +1253,24 @@ async def process_messages( | ||||||
|             # END-OF `async for`: |             # END-OF `async for`: | ||||||
|             # IPC disconnected via `trio.EndOfChannel`, likely |             # IPC disconnected via `trio.EndOfChannel`, likely | ||||||
|             # due to a (graceful) `Channel.aclose()`. |             # due to a (graceful) `Channel.aclose()`. | ||||||
|  | 
 | ||||||
|  |             chan_op_repr: str = '<=x] ' | ||||||
|  |             chan_repr: str = _pformat.nest_from_op( | ||||||
|  |                 input_op=chan_op_repr, | ||||||
|  |                 op_suffix='', | ||||||
|  |                 nest_prefix='', | ||||||
|  |                 text=chan.pformat(), | ||||||
|  |                 nest_indent=len(chan_op_repr)-1, | ||||||
|  |                 rm_from_first_ln='<', | ||||||
|  |             ) | ||||||
|             log.runtime( |             log.runtime( | ||||||
|                 f'channel for {chan.uid} disconnected, cancelling RPC tasks\n' |                 f'IPC channel disconnected\n' | ||||||
|                 f'|_{chan}\n' |                 f'{chan_repr}\n' | ||||||
|  |                 f'\n' | ||||||
|  |                 f'->c) cancelling RPC tasks.\n' | ||||||
|             ) |             ) | ||||||
|             await actor.cancel_rpc_tasks( |             await actor.cancel_rpc_tasks( | ||||||
|                 req_uid=actor.uid, |                 req_aid=actor.aid, | ||||||
|                 # a "self cancel" in terms of the lifetime of the |                 # a "self cancel" in terms of the lifetime of the | ||||||
|                 # IPC connection which is presumed to be the |                 # IPC connection which is presumed to be the | ||||||
|                 # source of any requests for spawned tasks. |                 # source of any requests for spawned tasks. | ||||||
|  | @ -1239,7 +1312,7 @@ async def process_messages( | ||||||
|     ) as err: |     ) as err: | ||||||
| 
 | 
 | ||||||
|         if nursery_cancelled_before_task: |         if nursery_cancelled_before_task: | ||||||
|             sn: Nursery = actor._service_n |             sn: Nursery = actor._service_tn | ||||||
|             assert sn and sn.cancel_scope.cancel_called  # sanity |             assert sn and sn.cancel_scope.cancel_called  # sanity | ||||||
|             log.cancel( |             log.cancel( | ||||||
|                 f'Service nursery cancelled before it handled {funcname}' |                 f'Service nursery cancelled before it handled {funcname}' | ||||||
|  | @ -1269,13 +1342,37 @@ async def process_messages( | ||||||
|     finally: |     finally: | ||||||
|         # msg debugging for when he machinery is brokey |         # msg debugging for when he machinery is brokey | ||||||
|         if msg is None: |         if msg is None: | ||||||
|             message: str = 'Exiting IPC msg loop without receiving a msg?' |             message: str = 'Exiting RPC-loop without receiving a msg?' | ||||||
|         else: |         else: | ||||||
|  |             task_op_repr: str = ')>' | ||||||
|  |             task: trio.Task = trio.lowlevel.current_task() | ||||||
|  | 
 | ||||||
|  |             # maybe add cancelled opt prefix | ||||||
|  |             if task._cancel_status.effectively_cancelled: | ||||||
|  |                 task_op_repr = 'c' + task_op_repr | ||||||
|  | 
 | ||||||
|  |             task_repr: str = _pformat.nest_from_op( | ||||||
|  |                 input_op=task_op_repr, | ||||||
|  |                 text=f'{task!r}', | ||||||
|  |                 nest_indent=1, | ||||||
|  |             ) | ||||||
|  |             # chan_op_repr: str = '<=} ' | ||||||
|  |             # chan_repr: str = _pformat.nest_from_op( | ||||||
|  |             #     input_op=chan_op_repr, | ||||||
|  |             #     op_suffix='', | ||||||
|  |             #     nest_prefix='', | ||||||
|  |             #     text=chan.pformat(), | ||||||
|  |             #     nest_indent=len(chan_op_repr)-1, | ||||||
|  |             #     rm_from_first_ln='<', | ||||||
|  |             # ) | ||||||
|             message: str = ( |             message: str = ( | ||||||
|                 'Exiting IPC msg loop with final msg\n\n' |                 f'Exiting RPC-loop with final msg\n' | ||||||
|                 f'<= peer: {chan.uid}\n' |                 f'\n' | ||||||
|                 f'  |_{chan}\n\n' |                 # f'{chan_repr}\n' | ||||||
|                 # f'{pretty_struct.pformat(msg)}' |                 f'{task_repr}\n' | ||||||
|  |                 f'\n' | ||||||
|  |                 f'{pretty_struct.pformat(msg)}' | ||||||
|  |                 f'\n' | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|         log.runtime(message) |         log.runtime(message) | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -34,9 +34,9 @@ from typing import ( | ||||||
| import trio | import trio | ||||||
| from trio import TaskStatus | from trio import TaskStatus | ||||||
| 
 | 
 | ||||||
| from .devx._debug import ( | from .devx import ( | ||||||
|     maybe_wait_for_debugger, |     debug, | ||||||
|     acquire_debug_lock, |     pformat as _pformat | ||||||
| ) | ) | ||||||
| from tractor._state import ( | from tractor._state import ( | ||||||
|     current_actor, |     current_actor, | ||||||
|  | @ -51,14 +51,17 @@ from tractor._portal import Portal | ||||||
| from tractor._runtime import Actor | from tractor._runtime import Actor | ||||||
| from tractor._entry import _mp_main | from tractor._entry import _mp_main | ||||||
| from tractor._exceptions import ActorFailure | from tractor._exceptions import ActorFailure | ||||||
| from tractor.msg.types import ( | from tractor.msg import ( | ||||||
|     Aid, |     types as msgtypes, | ||||||
|     SpawnSpec, |     pretty_struct, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| if TYPE_CHECKING: | if TYPE_CHECKING: | ||||||
|     from ipc import IPCServer |     from ipc import ( | ||||||
|  |         _server, | ||||||
|  |         Channel, | ||||||
|  |     ) | ||||||
|     from ._supervise import ActorNursery |     from ._supervise import ActorNursery | ||||||
|     ProcessType = TypeVar('ProcessType', mp.Process, trio.Process) |     ProcessType = TypeVar('ProcessType', mp.Process, trio.Process) | ||||||
| 
 | 
 | ||||||
|  | @ -233,10 +236,6 @@ async def hard_kill( | ||||||
|     # whilst also hacking on it XD |     # whilst also hacking on it XD | ||||||
|     # terminate_after: int = 99999, |     # terminate_after: int = 99999, | ||||||
| 
 | 
 | ||||||
|     # NOTE: for mucking with `.pause()`-ing inside the runtime |  | ||||||
|     # whilst also hacking on it XD |  | ||||||
|     # terminate_after: int = 99999, |  | ||||||
| 
 |  | ||||||
| ) -> None: | ) -> None: | ||||||
|     ''' |     ''' | ||||||
|     Un-gracefully terminate an OS level `trio.Process` after timeout. |     Un-gracefully terminate an OS level `trio.Process` after timeout. | ||||||
|  | @ -298,6 +297,23 @@ async def hard_kill( | ||||||
|     # zombies (as a feature) we ask the OS to do send in the |     # zombies (as a feature) we ask the OS to do send in the | ||||||
|     # removal swad as the last resort. |     # removal swad as the last resort. | ||||||
|     if cs.cancelled_caught: |     if cs.cancelled_caught: | ||||||
|  | 
 | ||||||
|  |         # TODO? attempt at intermediary-rent-sub | ||||||
|  |         # with child in debug lock? | ||||||
|  |         # |_https://github.com/goodboy/tractor/issues/320 | ||||||
|  |         # | ||||||
|  |         # if not is_root_process(): | ||||||
|  |         #     log.warning( | ||||||
|  |         #         'Attempting to acquire debug-REPL-lock before zombie reap!' | ||||||
|  |         #     ) | ||||||
|  |         #     with trio.CancelScope(shield=True): | ||||||
|  |         #         async with debug.acquire_debug_lock( | ||||||
|  |         #             subactor_uid=current_actor().uid, | ||||||
|  |         #         ) as _ctx: | ||||||
|  |         #             log.warning( | ||||||
|  |         #                 'Acquired debug lock, child ready to be killed ??\n' | ||||||
|  |         #             ) | ||||||
|  | 
 | ||||||
|         # TODO: toss in the skynet-logo face as ascii art? |         # TODO: toss in the skynet-logo face as ascii art? | ||||||
|         log.critical( |         log.critical( | ||||||
|             # 'Well, the #ZOMBIE_LORD_IS_HERE# to collect\n' |             # 'Well, the #ZOMBIE_LORD_IS_HERE# to collect\n' | ||||||
|  | @ -328,20 +344,21 @@ async def soft_kill( | ||||||
|     see `.hard_kill()`). |     see `.hard_kill()`). | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     peer_aid: Aid = portal.channel.aid |     chan: Channel = portal.channel | ||||||
|  |     peer_aid: msgtypes.Aid = chan.aid | ||||||
|     try: |     try: | ||||||
|         log.cancel( |         log.cancel( | ||||||
|             f'Soft killing sub-actor via portal request\n' |             f'Soft killing sub-actor via portal request\n' | ||||||
|             f'\n' |             f'\n' | ||||||
|             f'(c=> {peer_aid}\n' |             f'c)=> {peer_aid.reprol()}@[{chan.maddr}]\n' | ||||||
|             f'  |_{proc}\n' |             f'   |_{proc}\n' | ||||||
|         ) |         ) | ||||||
|         # wait on sub-proc to signal termination |         # wait on sub-proc to signal termination | ||||||
|         await wait_func(proc) |         await wait_func(proc) | ||||||
| 
 | 
 | ||||||
|     except trio.Cancelled: |     except trio.Cancelled: | ||||||
|         with trio.CancelScope(shield=True): |         with trio.CancelScope(shield=True): | ||||||
|             await maybe_wait_for_debugger( |             await debug.maybe_wait_for_debugger( | ||||||
|                 child_in_debug=_runtime_vars.get( |                 child_in_debug=_runtime_vars.get( | ||||||
|                     '_debug_mode', False |                     '_debug_mode', False | ||||||
|                 ), |                 ), | ||||||
|  | @ -465,7 +482,7 @@ async def trio_proc( | ||||||
|         "--uid", |         "--uid", | ||||||
|         # TODO, how to pass this over "wire" encodings like |         # TODO, how to pass this over "wire" encodings like | ||||||
|         # cmdline args? |         # cmdline args? | ||||||
|         # -[ ] maybe we can add an `Aid.min_tuple()` ? |         # -[ ] maybe we can add an `msgtypes.Aid.min_tuple()` ? | ||||||
|         str(subactor.uid), |         str(subactor.uid), | ||||||
|         # Address the child must connect to on startup |         # Address the child must connect to on startup | ||||||
|         "--parent_addr", |         "--parent_addr", | ||||||
|  | @ -483,13 +500,14 @@ async def trio_proc( | ||||||
| 
 | 
 | ||||||
|     cancelled_during_spawn: bool = False |     cancelled_during_spawn: bool = False | ||||||
|     proc: trio.Process|None = None |     proc: trio.Process|None = None | ||||||
|     ipc_server: IPCServer = actor_nursery._actor.ipc_server |     ipc_server: _server.Server = actor_nursery._actor.ipc_server | ||||||
|     try: |     try: | ||||||
|         try: |         try: | ||||||
|             proc: trio.Process = await trio.lowlevel.open_process(spawn_cmd, **proc_kwargs) |             proc: trio.Process = await trio.lowlevel.open_process(spawn_cmd, **proc_kwargs) | ||||||
|             log.runtime( |             log.runtime( | ||||||
|                 'Started new child\n' |                 f'Started new child subproc\n' | ||||||
|                 f'|_{proc}\n' |                 f'(>\n' | ||||||
|  |                 f' |_{proc}\n' | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             # wait for actor to spawn and connect back to us |             # wait for actor to spawn and connect back to us | ||||||
|  | @ -507,10 +525,10 @@ async def trio_proc( | ||||||
|                 with trio.CancelScope(shield=True): |                 with trio.CancelScope(shield=True): | ||||||
|                     # don't clobber an ongoing pdb |                     # don't clobber an ongoing pdb | ||||||
|                     if is_root_process(): |                     if is_root_process(): | ||||||
|                         await maybe_wait_for_debugger() |                         await debug.maybe_wait_for_debugger() | ||||||
| 
 | 
 | ||||||
|                     elif proc is not None: |                     elif proc is not None: | ||||||
|                         async with acquire_debug_lock(subactor.uid): |                         async with debug.acquire_debug_lock(subactor.uid): | ||||||
|                             # soft wait on the proc to terminate |                             # soft wait on the proc to terminate | ||||||
|                             with trio.move_on_after(0.5): |                             with trio.move_on_after(0.5): | ||||||
|                                 await proc.wait() |                                 await proc.wait() | ||||||
|  | @ -528,14 +546,19 @@ async def trio_proc( | ||||||
| 
 | 
 | ||||||
|         # send a "spawning specification" which configures the |         # send a "spawning specification" which configures the | ||||||
|         # initial runtime state of the child. |         # initial runtime state of the child. | ||||||
|         sspec = SpawnSpec( |         sspec = msgtypes.SpawnSpec( | ||||||
|             _parent_main_data=subactor._parent_main_data, |             _parent_main_data=subactor._parent_main_data, | ||||||
|             enable_modules=subactor.enable_modules, |             enable_modules=subactor.enable_modules, | ||||||
|             reg_addrs=subactor.reg_addrs, |             reg_addrs=subactor.reg_addrs, | ||||||
|             bind_addrs=bind_addrs, |             bind_addrs=bind_addrs, | ||||||
|             _runtime_vars=_runtime_vars, |             _runtime_vars=_runtime_vars, | ||||||
|         ) |         ) | ||||||
|         log.runtime(f'Sending spawn spec: {str(sspec)}') |         log.runtime( | ||||||
|  |             f'Sending spawn spec to child\n' | ||||||
|  |             f'{{}}=> {chan.aid.reprol()!r}\n' | ||||||
|  |             f'\n' | ||||||
|  |             f'{pretty_struct.pformat(sspec)}\n' | ||||||
|  |         ) | ||||||
|         await chan.send(sspec) |         await chan.send(sspec) | ||||||
| 
 | 
 | ||||||
|         # track subactor in current nursery |         # track subactor in current nursery | ||||||
|  | @ -563,7 +586,7 @@ async def trio_proc( | ||||||
|             # condition. |             # condition. | ||||||
|             await soft_kill( |             await soft_kill( | ||||||
|                 proc, |                 proc, | ||||||
|                 trio.Process.wait, |                 trio.Process.wait,  # XXX, uses `pidfd_open()` below. | ||||||
|                 portal |                 portal | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -571,8 +594,7 @@ async def trio_proc( | ||||||
|             # tandem if not done already |             # tandem if not done already | ||||||
|             log.cancel( |             log.cancel( | ||||||
|                 'Cancelling portal result reaper task\n' |                 'Cancelling portal result reaper task\n' | ||||||
|                 f'>c)\n' |                 f'c)> {subactor.aid.reprol()!r}\n' | ||||||
|                 f' |_{subactor.uid}\n' |  | ||||||
|             ) |             ) | ||||||
|             nursery.cancel_scope.cancel() |             nursery.cancel_scope.cancel() | ||||||
| 
 | 
 | ||||||
|  | @ -581,21 +603,24 @@ async def trio_proc( | ||||||
|         # allowed! Do this **after** cancellation/teardown to avoid |         # allowed! Do this **after** cancellation/teardown to avoid | ||||||
|         # killing the process too early. |         # killing the process too early. | ||||||
|         if proc: |         if proc: | ||||||
|  |             reap_repr: str = _pformat.nest_from_op( | ||||||
|  |                 input_op='>x)', | ||||||
|  |                 text=subactor.pformat(), | ||||||
|  |             ) | ||||||
|             log.cancel( |             log.cancel( | ||||||
|                 f'Hard reap sequence starting for subactor\n' |                 f'Hard reap sequence starting for subactor\n' | ||||||
|                 f'>x)\n' |                 f'{reap_repr}' | ||||||
|                 f' |_{subactor}@{subactor.uid}\n' |  | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|             with trio.CancelScope(shield=True): |             with trio.CancelScope(shield=True): | ||||||
|                 # don't clobber an ongoing pdb |                 # don't clobber an ongoing pdb | ||||||
|                 if cancelled_during_spawn: |                 if cancelled_during_spawn: | ||||||
|                     # Try again to avoid TTY clobbering. |                     # Try again to avoid TTY clobbering. | ||||||
|                     async with acquire_debug_lock(subactor.uid): |                     async with debug.acquire_debug_lock(subactor.uid): | ||||||
|                         with trio.move_on_after(0.5): |                         with trio.move_on_after(0.5): | ||||||
|                             await proc.wait() |                             await proc.wait() | ||||||
| 
 | 
 | ||||||
|                 await maybe_wait_for_debugger( |                 await debug.maybe_wait_for_debugger( | ||||||
|                     child_in_debug=_runtime_vars.get( |                     child_in_debug=_runtime_vars.get( | ||||||
|                         '_debug_mode', False |                         '_debug_mode', False | ||||||
|                     ), |                     ), | ||||||
|  | @ -624,7 +649,7 @@ async def trio_proc( | ||||||
|                 #     acquire the lock and get notified of who has it, |                 #     acquire the lock and get notified of who has it, | ||||||
|                 #     check that uid against our known children? |                 #     check that uid against our known children? | ||||||
|                 # this_uid: tuple[str, str] = current_actor().uid |                 # this_uid: tuple[str, str] = current_actor().uid | ||||||
|                 # await acquire_debug_lock(this_uid) |                 # await debug.acquire_debug_lock(this_uid) | ||||||
| 
 | 
 | ||||||
|                 if proc.poll() is None: |                 if proc.poll() is None: | ||||||
|                     log.cancel(f"Attempting to hard kill {proc}") |                     log.cancel(f"Attempting to hard kill {proc}") | ||||||
|  | @ -727,7 +752,7 @@ async def mp_proc( | ||||||
| 
 | 
 | ||||||
|     log.runtime(f"Started {proc}") |     log.runtime(f"Started {proc}") | ||||||
| 
 | 
 | ||||||
|     ipc_server: IPCServer = actor_nursery._actor.ipc_server |     ipc_server: _server.Server = actor_nursery._actor.ipc_server | ||||||
|     try: |     try: | ||||||
|         # wait for actor to spawn and connect back to us |         # wait for actor to spawn and connect back to us | ||||||
|         # channel should have handshake completed by the |         # channel should have handshake completed by the | ||||||
|  |  | ||||||
|  | @ -37,20 +37,39 @@ if TYPE_CHECKING: | ||||||
|     from ._context import Context |     from ._context import Context | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | # default IPC transport protocol settings | ||||||
|  | TransportProtocolKey = Literal[ | ||||||
|  |     'tcp', | ||||||
|  |     'uds', | ||||||
|  | ] | ||||||
|  | _def_tpt_proto: TransportProtocolKey = 'tcp' | ||||||
|  | 
 | ||||||
| _current_actor: Actor|None = None  # type: ignore # noqa | _current_actor: Actor|None = None  # type: ignore # noqa | ||||||
| _last_actor_terminated: Actor|None = None | _last_actor_terminated: Actor|None = None | ||||||
| 
 | 
 | ||||||
| # TODO: mk this a `msgspec.Struct`! | # TODO: mk this a `msgspec.Struct`! | ||||||
|  | # -[ ] type out all fields obvi! | ||||||
|  | # -[ ] (eventually) mk wire-ready for monitoring? | ||||||
| _runtime_vars: dict[str, Any] = { | _runtime_vars: dict[str, Any] = { | ||||||
|     '_debug_mode': False, |     # root of actor-process tree info | ||||||
|     '_is_root': False, |     '_is_root': False,  # bool | ||||||
|     '_root_mailbox': (None, None), |     '_root_mailbox': (None, None),  # tuple[str|None, str|None] | ||||||
|  |     '_root_addrs': [],  # tuple[str|None, str|None] | ||||||
|  | 
 | ||||||
|  |     # parent->chld ipc protocol caps | ||||||
|  |     '_enable_tpts': [_def_tpt_proto], | ||||||
|  | 
 | ||||||
|  |     # registrar info | ||||||
|     '_registry_addrs': [], |     '_registry_addrs': [], | ||||||
| 
 | 
 | ||||||
|     '_is_infected_aio': False, |     # `debug_mode: bool` settings | ||||||
| 
 |     '_debug_mode': False,  # bool | ||||||
|  |     'repl_fixture': False,  # |AbstractContextManager[bool] | ||||||
|     # for `tractor.pause_from_sync()` & `breakpoint()` support |     # for `tractor.pause_from_sync()` & `breakpoint()` support | ||||||
|     'use_greenback': False, |     'use_greenback': False, | ||||||
|  | 
 | ||||||
|  |     # infected-`asyncio`-mode: `trio` running as guest. | ||||||
|  |     '_is_infected_aio': False, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -172,14 +191,6 @@ def get_rt_dir( | ||||||
|     return rtdir |     return rtdir | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # default IPC transport protocol settings |  | ||||||
| TransportProtocolKey = Literal[ |  | ||||||
|     'tcp', |  | ||||||
|     'uds', |  | ||||||
| ] |  | ||||||
| _def_tpt_proto: TransportProtocolKey = 'tcp' |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def current_ipc_protos() -> list[str]: | def current_ipc_protos() -> list[str]: | ||||||
|     ''' |     ''' | ||||||
|     Return the list of IPC transport protocol keys currently |     Return the list of IPC transport protocol keys currently | ||||||
|  | @ -189,4 +200,4 @@ def current_ipc_protos() -> list[str]: | ||||||
|     concrete-backend sub-types defined throughout `tractor.ipc`. |     concrete-backend sub-types defined throughout `tractor.ipc`. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     return [_def_tpt_proto] |     return _runtime_vars['_enable_tpts'] | ||||||
|  |  | ||||||
|  | @ -426,8 +426,8 @@ class MsgStream(trio.abc.Channel): | ||||||
|             self._closed = re |             self._closed = re | ||||||
| 
 | 
 | ||||||
|         # if caught_eoc: |         # if caught_eoc: | ||||||
|         #     # from .devx import _debug |         #     # from .devx import debug | ||||||
|         #     # await _debug.pause() |         #     # await debug.pause() | ||||||
|         #     with trio.CancelScope(shield=True): |         #     with trio.CancelScope(shield=True): | ||||||
|         #         await rx_chan.aclose() |         #         await rx_chan.aclose() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -21,7 +21,6 @@ | ||||||
| from contextlib import asynccontextmanager as acm | from contextlib import asynccontextmanager as acm | ||||||
| from functools import partial | from functools import partial | ||||||
| import inspect | import inspect | ||||||
| from pprint import pformat |  | ||||||
| from typing import ( | from typing import ( | ||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
| ) | ) | ||||||
|  | @ -31,7 +30,10 @@ import warnings | ||||||
| import trio | import trio | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| from .devx._debug import maybe_wait_for_debugger | from .devx import ( | ||||||
|  |     debug, | ||||||
|  |     pformat as _pformat, | ||||||
|  | ) | ||||||
| from ._addr import ( | from ._addr import ( | ||||||
|     UnwrappedAddress, |     UnwrappedAddress, | ||||||
|     mk_uuid, |     mk_uuid, | ||||||
|  | @ -40,8 +42,11 @@ from ._state import current_actor, is_main_process | ||||||
| from .log import get_logger, get_loglevel | from .log import get_logger, get_loglevel | ||||||
| from ._runtime import Actor | from ._runtime import Actor | ||||||
| from ._portal import Portal | from ._portal import Portal | ||||||
| from ._exceptions import ( | from .trionics import ( | ||||||
|     is_multi_cancelled, |     is_multi_cancelled, | ||||||
|  |     collapse_eg, | ||||||
|  | ) | ||||||
|  | from ._exceptions import ( | ||||||
|     ContextCancelled, |     ContextCancelled, | ||||||
| ) | ) | ||||||
| from ._root import ( | from ._root import ( | ||||||
|  | @ -112,7 +117,6 @@ class ActorNursery: | ||||||
|             ] |             ] | ||||||
|         ] = {} |         ] = {} | ||||||
| 
 | 
 | ||||||
|         self.cancelled: bool = False |  | ||||||
|         self._join_procs = trio.Event() |         self._join_procs = trio.Event() | ||||||
|         self._at_least_one_child_in_debug: bool = False |         self._at_least_one_child_in_debug: bool = False | ||||||
|         self.errors = errors |         self.errors = errors | ||||||
|  | @ -130,10 +134,53 @@ class ActorNursery: | ||||||
|         # TODO: remove the `.run_in_actor()` API and thus this 2ndary |         # TODO: remove the `.run_in_actor()` API and thus this 2ndary | ||||||
|         # nursery when that API get's moved outside this primitive! |         # nursery when that API get's moved outside this primitive! | ||||||
|         self._ria_nursery = ria_nursery |         self._ria_nursery = ria_nursery | ||||||
|  | 
 | ||||||
|  |         # TODO, factor this into a .hilevel api! | ||||||
|  |         # | ||||||
|         # portals spawned with ``run_in_actor()`` are |         # portals spawned with ``run_in_actor()`` are | ||||||
|         # cancelled when their "main" result arrives |         # cancelled when their "main" result arrives | ||||||
|         self._cancel_after_result_on_exit: set = set() |         self._cancel_after_result_on_exit: set = set() | ||||||
| 
 | 
 | ||||||
|  |         # trio.Nursery-like cancel (request) statuses | ||||||
|  |         self._cancelled_caught: bool = False | ||||||
|  |         self._cancel_called: bool = False | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def cancel_called(self) -> bool: | ||||||
|  |         ''' | ||||||
|  |         Records whether cancellation has been requested for this | ||||||
|  |         actor-nursery by a call to  `.cancel()` either due to, | ||||||
|  |         - an explicit call by some actor-local-task, | ||||||
|  |         - an implicit call due to an error/cancel emited inside | ||||||
|  |           the `tractor.open_nursery()` block. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         return self._cancel_called | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def cancelled_caught(self) -> bool: | ||||||
|  |         ''' | ||||||
|  |         Set when this nursery was able to cance all spawned subactors | ||||||
|  |         gracefully via an (implicit) call to `.cancel()`. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         return self._cancelled_caught | ||||||
|  | 
 | ||||||
|  |     # TODO! remove internal/test-suite usage! | ||||||
|  |     @property | ||||||
|  |     def cancelled(self) -> bool: | ||||||
|  |         warnings.warn( | ||||||
|  |             "`ActorNursery.cancelled` is now deprecated, use " | ||||||
|  |             " `.cancel_called` instead.", | ||||||
|  |             DeprecationWarning, | ||||||
|  |             stacklevel=2, | ||||||
|  |         ) | ||||||
|  |         return ( | ||||||
|  |             self._cancel_called | ||||||
|  |             # and | ||||||
|  |             # self._cancelled_caught | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|     async def start_actor( |     async def start_actor( | ||||||
|         self, |         self, | ||||||
|         name: str, |         name: str, | ||||||
|  | @ -197,7 +244,7 @@ class ActorNursery: | ||||||
|             loglevel=loglevel, |             loglevel=loglevel, | ||||||
| 
 | 
 | ||||||
|             # verbatim relay this actor's registrar addresses |             # verbatim relay this actor's registrar addresses | ||||||
|             registry_addrs=current_actor().reg_addrs, |             registry_addrs=current_actor().registry_addrs, | ||||||
|         ) |         ) | ||||||
|         parent_addr: UnwrappedAddress = self._actor.accept_addr |         parent_addr: UnwrappedAddress = self._actor.accept_addr | ||||||
|         assert parent_addr |         assert parent_addr | ||||||
|  | @ -311,7 +358,7 @@ class ActorNursery: | ||||||
| 
 | 
 | ||||||
|         ''' |         ''' | ||||||
|         __runtimeframe__: int = 1  # noqa |         __runtimeframe__: int = 1  # noqa | ||||||
|         self.cancelled = True |         self._cancel_called = True | ||||||
| 
 | 
 | ||||||
|         # TODO: impl a repr for spawn more compact |         # TODO: impl a repr for spawn more compact | ||||||
|         # then `._children`.. |         # then `._children`.. | ||||||
|  | @ -322,9 +369,10 @@ class ActorNursery: | ||||||
|         server: IPCServer = self._actor.ipc_server |         server: IPCServer = self._actor.ipc_server | ||||||
| 
 | 
 | ||||||
|         with trio.move_on_after(3) as cs: |         with trio.move_on_after(3) as cs: | ||||||
|             async with trio.open_nursery( |             async with ( | ||||||
|                 strict_exception_groups=False, |                 collapse_eg(), | ||||||
|             ) as tn: |                 trio.open_nursery() as tn, | ||||||
|  |             ): | ||||||
| 
 | 
 | ||||||
|                 subactor: Actor |                 subactor: Actor | ||||||
|                 proc: trio.Process |                 proc: trio.Process | ||||||
|  | @ -388,6 +436,8 @@ class ActorNursery: | ||||||
|             ) in children.values(): |             ) in children.values(): | ||||||
|                 log.warning(f"Hard killing process {proc}") |                 log.warning(f"Hard killing process {proc}") | ||||||
|                 proc.terminate() |                 proc.terminate() | ||||||
|  |         else: | ||||||
|  |             self._cancelled_caught | ||||||
| 
 | 
 | ||||||
|         # mark ourselves as having (tried to have) cancelled all subactors |         # mark ourselves as having (tried to have) cancelled all subactors | ||||||
|         self._join_procs.set() |         self._join_procs.set() | ||||||
|  | @ -417,10 +467,10 @@ async def _open_and_supervise_one_cancels_all_nursery( | ||||||
|     # `ActorNursery.start_actor()`). |     # `ActorNursery.start_actor()`). | ||||||
| 
 | 
 | ||||||
|     # errors from this daemon actor nursery bubble up to caller |     # errors from this daemon actor nursery bubble up to caller | ||||||
|     async with trio.open_nursery( |     async with ( | ||||||
|         strict_exception_groups=False, |         collapse_eg(), | ||||||
|         # ^XXX^ TODO? instead unpack any RAE as per "loose" style? |         trio.open_nursery() as da_nursery, | ||||||
|     ) as da_nursery: |     ): | ||||||
|         try: |         try: | ||||||
|             # This is the inner level "run in actor" nursery. It is |             # This is the inner level "run in actor" nursery. It is | ||||||
|             # awaited first since actors spawned in this way (using |             # awaited first since actors spawned in this way (using | ||||||
|  | @ -430,11 +480,10 @@ async def _open_and_supervise_one_cancels_all_nursery( | ||||||
|             # immediately raised for handling by a supervisor strategy. |             # immediately raised for handling by a supervisor strategy. | ||||||
|             # As such if the strategy propagates any error(s) upwards |             # As such if the strategy propagates any error(s) upwards | ||||||
|             # the above "daemon actor" nursery will be notified. |             # the above "daemon actor" nursery will be notified. | ||||||
|             async with trio.open_nursery( |             async with ( | ||||||
|                 strict_exception_groups=False, |                 collapse_eg(), | ||||||
|                 # ^XXX^ TODO? instead unpack any RAE as per "loose" style? |                 trio.open_nursery() as ria_nursery, | ||||||
|             ) as ria_nursery: |             ): | ||||||
| 
 |  | ||||||
|                 an = ActorNursery( |                 an = ActorNursery( | ||||||
|                     actor, |                     actor, | ||||||
|                     ria_nursery, |                     ria_nursery, | ||||||
|  | @ -451,7 +500,7 @@ async def _open_and_supervise_one_cancels_all_nursery( | ||||||
|                     # the "hard join phase". |                     # the "hard join phase". | ||||||
|                     log.runtime( |                     log.runtime( | ||||||
|                         'Waiting on subactors to complete:\n' |                         'Waiting on subactors to complete:\n' | ||||||
|                         f'{pformat(an._children)}\n' |                         f'>}} {len(an._children)}\n' | ||||||
|                     ) |                     ) | ||||||
|                     an._join_procs.set() |                     an._join_procs.set() | ||||||
| 
 | 
 | ||||||
|  | @ -465,7 +514,7 @@ async def _open_and_supervise_one_cancels_all_nursery( | ||||||
|                     # will make the pdb repl unusable. |                     # will make the pdb repl unusable. | ||||||
|                     # Instead try to wait for pdb to be released before |                     # Instead try to wait for pdb to be released before | ||||||
|                     # tearing down. |                     # tearing down. | ||||||
|                     await maybe_wait_for_debugger( |                     await debug.maybe_wait_for_debugger( | ||||||
|                         child_in_debug=an._at_least_one_child_in_debug |                         child_in_debug=an._at_least_one_child_in_debug | ||||||
|                     ) |                     ) | ||||||
| 
 | 
 | ||||||
|  | @ -541,7 +590,7 @@ async def _open_and_supervise_one_cancels_all_nursery( | ||||||
| 
 | 
 | ||||||
|             # XXX: yet another guard before allowing the cancel |             # XXX: yet another guard before allowing the cancel | ||||||
|             # sequence in case a (single) child is in debug. |             # sequence in case a (single) child is in debug. | ||||||
|             await maybe_wait_for_debugger( |             await debug.maybe_wait_for_debugger( | ||||||
|                 child_in_debug=an._at_least_one_child_in_debug |                 child_in_debug=an._at_least_one_child_in_debug | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -590,9 +639,15 @@ async def _open_and_supervise_one_cancels_all_nursery( | ||||||
|     # final exit |     # final exit | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | _shutdown_msg: str = ( | ||||||
|  |     'Actor-runtime-shutdown' | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @acm | @acm | ||||||
| # @api_frame | # @api_frame | ||||||
| async def open_nursery( | async def open_nursery( | ||||||
|  |     *,  # named params only! | ||||||
|     hide_tb: bool = True, |     hide_tb: bool = True, | ||||||
|     **kwargs, |     **kwargs, | ||||||
|     # ^TODO, paramspec for `open_root_actor()` |     # ^TODO, paramspec for `open_root_actor()` | ||||||
|  | @ -677,17 +732,26 @@ async def open_nursery( | ||||||
|         ): |         ): | ||||||
|             __tracebackhide__: bool = False |             __tracebackhide__: bool = False | ||||||
| 
 | 
 | ||||||
|         msg: str = ( | 
 | ||||||
|             'Actor-nursery exited\n' |         op_nested_an_repr: str = _pformat.nest_from_op( | ||||||
|             f'|_{an}\n' |             input_op=')>', | ||||||
|  |             text=f'{an}', | ||||||
|  |             # nest_prefix='|_', | ||||||
|  |             nest_indent=1,  # under > | ||||||
|         ) |         ) | ||||||
|  |         an_msg: str = ( | ||||||
|  |             f'Actor-nursery exited\n' | ||||||
|  |             f'{op_nested_an_repr}\n' | ||||||
|  |         ) | ||||||
|  |         # keep noise low during std operation. | ||||||
|  |         log.runtime(an_msg) | ||||||
| 
 | 
 | ||||||
|         if implicit_runtime: |         if implicit_runtime: | ||||||
|             # shutdown runtime if it was started and report noisly |             # shutdown runtime if it was started and report noisly | ||||||
|             # that we're did so. |             # that we're did so. | ||||||
|             msg += '=> Shutting down actor runtime <=\n' |             msg: str = ( | ||||||
|  |                 '\n' | ||||||
|  |                 '\n' | ||||||
|  |                 f'{_shutdown_msg} )>\n' | ||||||
|  |             ) | ||||||
|             log.info(msg) |             log.info(msg) | ||||||
| 
 |  | ||||||
|         else: |  | ||||||
|             # keep noise low during std operation. |  | ||||||
|             log.runtime(msg) |  | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ import os | ||||||
| import pathlib | import pathlib | ||||||
| 
 | 
 | ||||||
| import tractor | import tractor | ||||||
| from tractor.devx._debug import ( | from tractor.devx.debug import ( | ||||||
|     BoxedMaybeException, |     BoxedMaybeException, | ||||||
| ) | ) | ||||||
| from .pytest import ( | from .pytest import ( | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ Runtime "developer experience" utils and addons to aid our | ||||||
| and working with/on the actor runtime. | and working with/on the actor runtime. | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| from ._debug import ( | from .debug import ( | ||||||
|     maybe_wait_for_debugger as maybe_wait_for_debugger, |     maybe_wait_for_debugger as maybe_wait_for_debugger, | ||||||
|     acquire_debug_lock as acquire_debug_lock, |     acquire_debug_lock as acquire_debug_lock, | ||||||
|     breakpoint as breakpoint, |     breakpoint as breakpoint, | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -20,13 +20,18 @@ as it pertains to improving the grok-ability of our runtime! | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  | from contextlib import ( | ||||||
|  |     _GeneratorContextManager, | ||||||
|  |     _AsyncGeneratorContextManager, | ||||||
|  | ) | ||||||
| from functools import partial | from functools import partial | ||||||
| import inspect | import inspect | ||||||
|  | import textwrap | ||||||
| from types import ( | from types import ( | ||||||
|     FrameType, |     FrameType, | ||||||
|     FunctionType, |     FunctionType, | ||||||
|     MethodType, |     MethodType, | ||||||
|     # CodeType, |     CodeType, | ||||||
| ) | ) | ||||||
| from typing import ( | from typing import ( | ||||||
|     Any, |     Any, | ||||||
|  | @ -34,6 +39,9 @@ from typing import ( | ||||||
|     Type, |     Type, | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | import pdbp | ||||||
|  | from tractor.log import get_logger | ||||||
|  | import trio | ||||||
| from tractor.msg import ( | from tractor.msg import ( | ||||||
|     pretty_struct, |     pretty_struct, | ||||||
|     NamespacePath, |     NamespacePath, | ||||||
|  | @ -41,6 +49,8 @@ from tractor.msg import ( | ||||||
| import wrapt | import wrapt | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
| # TODO: yeah, i don't love this and we should prolly just | # TODO: yeah, i don't love this and we should prolly just | ||||||
| # write a decorator that actually keeps a stupid ref to the func | # write a decorator that actually keeps a stupid ref to the func | ||||||
| # obj.. | # obj.. | ||||||
|  | @ -301,3 +311,70 @@ def api_frame( | ||||||
| #     error_set: set[BaseException], | #     error_set: set[BaseException], | ||||||
| # ) -> TracebackType: | # ) -> TracebackType: | ||||||
| #     ... | #     ... | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def hide_runtime_frames() -> dict[FunctionType, CodeType]: | ||||||
|  |     ''' | ||||||
|  |     Hide call-stack frames for various std-lib and `trio`-API primitives | ||||||
|  |     such that the tracebacks presented from our runtime are as minimized | ||||||
|  |     as possible, particularly from inside a `PdbREPL`. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     # XXX HACKZONE XXX | ||||||
|  |     #  hide exit stack frames on nurseries and cancel-scopes! | ||||||
|  |     # |_ so avoid seeing it when the `pdbp` REPL is first engaged from | ||||||
|  |     #    inside a `trio.open_nursery()` scope (with no line after it | ||||||
|  |     #    in before the block end??). | ||||||
|  |     # | ||||||
|  |     # TODO: FINALLY got this workin originally with | ||||||
|  |     #  `@pdbp.hideframe` around the `wrapper()` def embedded inside | ||||||
|  |     #  `_ki_protection_decoratior()`.. which is in the module: | ||||||
|  |     #  /home/goodboy/.virtualenvs/tractor311/lib/python3.11/site-packages/trio/_core/_ki.py | ||||||
|  |     # | ||||||
|  |     # -[ ] make an issue and patch for `trio` core? maybe linked | ||||||
|  |     #    to the long outstanding `pdb` one below? | ||||||
|  |     #   |_ it's funny that there's frame hiding throughout `._run.py` | ||||||
|  |     #      but not where it matters on the below exit funcs.. | ||||||
|  |     # | ||||||
|  |     # -[ ] provide a patchset for the lonstanding | ||||||
|  |     #   |_ https://github.com/python-trio/trio/issues/1155 | ||||||
|  |     # | ||||||
|  |     # -[ ] make a linked issue to ^ and propose allowing all the | ||||||
|  |     #     `._core._run` code to have their `__tracebackhide__` value | ||||||
|  |     #     configurable by a `RunVar` to allow getting scheduler frames | ||||||
|  |     #     if desired through configuration? | ||||||
|  |     # | ||||||
|  |     # -[ ] maybe dig into the core `pdb` issue why the extra frame is shown | ||||||
|  |     #      at all? | ||||||
|  |     # | ||||||
|  |     funcs: list[FunctionType] = [ | ||||||
|  |         trio._core._run.NurseryManager.__aexit__, | ||||||
|  |         trio._core._run.CancelScope.__exit__, | ||||||
|  |          _GeneratorContextManager.__exit__, | ||||||
|  |          _AsyncGeneratorContextManager.__aexit__, | ||||||
|  |          _AsyncGeneratorContextManager.__aenter__, | ||||||
|  |          trio.Event.wait, | ||||||
|  |     ] | ||||||
|  |     func_list_str: str = textwrap.indent( | ||||||
|  |         "\n".join(f.__qualname__ for f in funcs), | ||||||
|  |         prefix=' |_ ', | ||||||
|  |     ) | ||||||
|  |     log.devx( | ||||||
|  |         'Hiding the following runtime frames by default:\n' | ||||||
|  |         f'{func_list_str}\n' | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     codes: dict[FunctionType, CodeType] = {} | ||||||
|  |     for ref in funcs: | ||||||
|  |         # stash a pre-modified version of each ref's code-obj | ||||||
|  |         # so it can be reverted later if needed. | ||||||
|  |         codes[ref] = ref.__code__ | ||||||
|  |         pdbp.hideframe(ref) | ||||||
|  |     # | ||||||
|  |     # pdbp.hideframe(trio._core._run.NurseryManager.__aexit__) | ||||||
|  |     # pdbp.hideframe(trio._core._run.CancelScope.__exit__) | ||||||
|  |     # pdbp.hideframe(_GeneratorContextManager.__exit__) | ||||||
|  |     # pdbp.hideframe(_AsyncGeneratorContextManager.__aexit__) | ||||||
|  |     # pdbp.hideframe(_AsyncGeneratorContextManager.__aenter__) | ||||||
|  |     # pdbp.hideframe(trio.Event.wait) | ||||||
|  |     return codes | ||||||
|  |  | ||||||
|  | @ -49,7 +49,7 @@ from tractor import ( | ||||||
|     _state, |     _state, | ||||||
|     log as logmod, |     log as logmod, | ||||||
| ) | ) | ||||||
| from tractor.devx import _debug | from tractor.devx import debug | ||||||
| 
 | 
 | ||||||
| log = logmod.get_logger(__name__) | log = logmod.get_logger(__name__) | ||||||
| 
 | 
 | ||||||
|  | @ -82,7 +82,7 @@ def dump_task_tree() -> None: | ||||||
|     if ( |     if ( | ||||||
|         current_sigint_handler |         current_sigint_handler | ||||||
|         is not |         is not | ||||||
|         _debug.DebugStatus._trio_handler |         debug.DebugStatus._trio_handler | ||||||
|     ): |     ): | ||||||
|         sigint_handler_report: str = ( |         sigint_handler_report: str = ( | ||||||
|             'The default `trio` SIGINT handler was replaced?!' |             'The default `trio` SIGINT handler was replaced?!' | ||||||
|  | @ -238,7 +238,8 @@ def enable_stack_on_sig( | ||||||
|         import stackscope |         import stackscope | ||||||
|     except ImportError: |     except ImportError: | ||||||
|         log.warning( |         log.warning( | ||||||
|             '`stackscope` not installed for use in debug mode!' |             'The `stackscope` lib is not installed!\n' | ||||||
|  |             '`Ignoring enable_stack_on_sig() call!\n' | ||||||
|         ) |         ) | ||||||
|         return None |         return None | ||||||
| 
 | 
 | ||||||
|  | @ -255,8 +256,8 @@ def enable_stack_on_sig( | ||||||
|         dump_tree_on_sig, |         dump_tree_on_sig, | ||||||
|     ) |     ) | ||||||
|     log.devx( |     log.devx( | ||||||
|         'Enabling trace-trees on `SIGUSR1` ' |         f'Enabling trace-trees on `SIGUSR1` ' | ||||||
|         'since `stackscope` is installed @ \n' |         f'since `stackscope` is installed @ \n' | ||||||
|         f'{stackscope!r}\n\n' |         f'{stackscope!r}\n\n' | ||||||
|         f'With `SIGUSR1` handler\n' |         f'With `SIGUSR1` handler\n' | ||||||
|         f'|_{dump_tree_on_sig}\n' |         f'|_{dump_tree_on_sig}\n' | ||||||
|  |  | ||||||
|  | @ -0,0 +1,100 @@ | ||||||
|  | # tractor: structured concurrent "actors". | ||||||
|  | # Copyright 2018-eternity Tyler Goodlet. | ||||||
|  | 
 | ||||||
|  | # This program is free software: you can redistribute it and/or | ||||||
|  | # modify it under the terms of the GNU Affero General Public License | ||||||
|  | # as published by the Free Software Foundation, either version 3 of | ||||||
|  | # the License, or (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | # This program is distributed in the hope that it will be useful, but | ||||||
|  | # WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | ||||||
|  | # Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | # You should have received a copy of the GNU Affero General Public | ||||||
|  | # License along with this program.  If not, see | ||||||
|  | # <https://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | Multi-actor debugging for da peeps! | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | from tractor.log import get_logger | ||||||
|  | from ._repl import ( | ||||||
|  |     PdbREPL as PdbREPL, | ||||||
|  |     mk_pdb as mk_pdb, | ||||||
|  |     TractorConfig as TractorConfig, | ||||||
|  | ) | ||||||
|  | from ._tty_lock import ( | ||||||
|  |     DebugStatus as DebugStatus, | ||||||
|  |     DebugStateError as DebugStateError, | ||||||
|  | ) | ||||||
|  | from ._trace import ( | ||||||
|  |     Lock as Lock, | ||||||
|  |     _pause_msg as _pause_msg, | ||||||
|  |     _repl_fail_msg as _repl_fail_msg, | ||||||
|  |     _set_trace as _set_trace, | ||||||
|  |     _sync_pause_from_builtin as _sync_pause_from_builtin, | ||||||
|  |     breakpoint as breakpoint, | ||||||
|  |     maybe_init_greenback as maybe_init_greenback, | ||||||
|  |     maybe_import_greenback as maybe_import_greenback, | ||||||
|  |     pause as pause, | ||||||
|  |     pause_from_sync as pause_from_sync, | ||||||
|  | ) | ||||||
|  | from ._post_mortem import ( | ||||||
|  |     BoxedMaybeException as BoxedMaybeException, | ||||||
|  |     maybe_open_crash_handler as maybe_open_crash_handler, | ||||||
|  |     open_crash_handler as open_crash_handler, | ||||||
|  |     post_mortem as post_mortem, | ||||||
|  |     _crash_msg as _crash_msg, | ||||||
|  |     _maybe_enter_pm as _maybe_enter_pm, | ||||||
|  | ) | ||||||
|  | from ._sync import ( | ||||||
|  |     maybe_wait_for_debugger as maybe_wait_for_debugger, | ||||||
|  |     acquire_debug_lock as acquire_debug_lock, | ||||||
|  | ) | ||||||
|  | from ._sigint import ( | ||||||
|  |     sigint_shield as sigint_shield, | ||||||
|  |     _ctlc_ignore_header as _ctlc_ignore_header | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | # ---------------- | ||||||
|  | # XXX PKG TODO XXX | ||||||
|  | # ---------------- | ||||||
|  | # refine the internal impl and APIs! | ||||||
|  | # | ||||||
|  | # -[ ] rework `._pause()` and it's branch-cases for root vs. | ||||||
|  | #     subactor: | ||||||
|  | #  -[ ] `._pause_from_root()` + `_pause_from_subactor()`? | ||||||
|  | #  -[ ]  do the de-factor based on bg-thread usage in | ||||||
|  | #    `.pause_from_sync()` & `_pause_from_bg_root_thread()`. | ||||||
|  | #  -[ ] drop `debug_func == None` case which is confusing af.. | ||||||
|  | #  -[ ]  factor out `_enter_repl_sync()` into a util func for calling | ||||||
|  | #    the `_set_trace()` / `_post_mortem()` APIs? | ||||||
|  | # | ||||||
|  | # -[ ] figure out if we need `acquire_debug_lock()` and/or re-implement | ||||||
|  | #    it as part of the `.pause_from_sync()` rework per above? | ||||||
|  | # | ||||||
|  | # -[ ] pair the `._pause_from_subactor()` impl with a "debug nursery" | ||||||
|  | #   that's dynamically allocated inside the `._rpc` task thus | ||||||
|  | #   avoiding the `._service_n.start()` usage for the IPC request? | ||||||
|  | #  -[ ] see the TODO inside `._rpc._errors_relayed_via_ipc()` | ||||||
|  | # | ||||||
|  | # -[ ] impl a `open_debug_request()` which encaps all | ||||||
|  | #   `request_root_stdio_lock()` task scheduling deats | ||||||
|  | #   + `DebugStatus` state mgmt; which should prolly be re-branded as | ||||||
|  | #   a `DebugRequest` type anyway AND with suppoort for bg-thread | ||||||
|  | #   (from root actor) usage? | ||||||
|  | # | ||||||
|  | # -[ ] handle the `xonsh` case for bg-root-threads in the SIGINT | ||||||
|  | #     handler! | ||||||
|  | #   -[ ] do we need to do the same for subactors? | ||||||
|  | #   -[ ] make the failing tests finally pass XD | ||||||
|  | # | ||||||
|  | # -[ ] simplify `maybe_wait_for_debugger()` to be a root-task only | ||||||
|  | #     API? | ||||||
|  | #   -[ ] currently it's implemented as that so might as well make it | ||||||
|  | #     formal? | ||||||
|  | @ -0,0 +1,412 @@ | ||||||
|  | # tractor: structured concurrent "actors". | ||||||
|  | # Copyright 2018-eternity Tyler Goodlet. | ||||||
|  | 
 | ||||||
|  | # This program is free software: you can redistribute it and/or | ||||||
|  | # modify it under the terms of the GNU Affero General Public License | ||||||
|  | # as published by the Free Software Foundation, either version 3 of | ||||||
|  | # the License, or (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | # This program is distributed in the hope that it will be useful, but | ||||||
|  | # WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | ||||||
|  | # Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | # You should have received a copy of the GNU Affero General Public | ||||||
|  | # License along with this program.  If not, see | ||||||
|  | # <https://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | Post-mortem debugging APIs and surrounding machinery for both | ||||||
|  | sync and async contexts. | ||||||
|  | 
 | ||||||
|  | Generally we maintain the same semantics a `pdb.post.mortem()` but | ||||||
|  | with actor-tree-wide sync/cooperation around any (sub)actor's use of | ||||||
|  | the root's TTY. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | import bdb | ||||||
|  | from contextlib import ( | ||||||
|  |     AbstractContextManager, | ||||||
|  |     contextmanager as cm, | ||||||
|  |     nullcontext, | ||||||
|  | ) | ||||||
|  | from functools import ( | ||||||
|  |     partial, | ||||||
|  | ) | ||||||
|  | import inspect | ||||||
|  | import sys | ||||||
|  | import traceback | ||||||
|  | from typing import ( | ||||||
|  |     Callable, | ||||||
|  |     Sequence, | ||||||
|  |     Type, | ||||||
|  |     TYPE_CHECKING, | ||||||
|  | ) | ||||||
|  | from types import ( | ||||||
|  |     TracebackType, | ||||||
|  |     FrameType, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | from msgspec import Struct | ||||||
|  | import trio | ||||||
|  | from tractor._exceptions import ( | ||||||
|  |     NoRuntime, | ||||||
|  | ) | ||||||
|  | from tractor import _state | ||||||
|  | from tractor._state import ( | ||||||
|  |     current_actor, | ||||||
|  |     debug_mode, | ||||||
|  | ) | ||||||
|  | from tractor.log import get_logger | ||||||
|  | from tractor.trionics import ( | ||||||
|  |     is_multi_cancelled, | ||||||
|  | ) | ||||||
|  | from ._trace import ( | ||||||
|  |     _pause, | ||||||
|  | ) | ||||||
|  | from ._tty_lock import ( | ||||||
|  |     DebugStatus, | ||||||
|  | ) | ||||||
|  | from ._repl import ( | ||||||
|  |     PdbREPL, | ||||||
|  |     mk_pdb, | ||||||
|  |     TractorConfig as TractorConfig, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from trio.lowlevel import Task | ||||||
|  |     from tractor._runtime import ( | ||||||
|  |         Actor, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | _crash_msg: str = ( | ||||||
|  |     'Opening a pdb REPL in crashed actor' | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | log = get_logger(__package__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class BoxedMaybeException(Struct): | ||||||
|  |     ''' | ||||||
|  |     Box a maybe-exception for post-crash introspection usage | ||||||
|  |     from the body of a `open_crash_handler()` scope. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     value: BaseException|None = None | ||||||
|  | 
 | ||||||
|  |     # handler can suppress crashes dynamically | ||||||
|  |     raise_on_exit: bool|Sequence[Type[BaseException]] = True | ||||||
|  | 
 | ||||||
|  |     def pformat(self) -> str: | ||||||
|  |         ''' | ||||||
|  |         Repr the boxed `.value` error in more-than-string | ||||||
|  |         repr form. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         if not self.value: | ||||||
|  |             return f'<{type(self).__name__}( .value=None )>' | ||||||
|  | 
 | ||||||
|  |         return ( | ||||||
|  |             f'<{type(self.value).__name__}(\n' | ||||||
|  |             f' |_.value = {self.value}\n' | ||||||
|  |             f')>\n' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     __repr__ = pformat | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _post_mortem( | ||||||
|  |     repl: PdbREPL,  # normally passed by `_pause()` | ||||||
|  | 
 | ||||||
|  |     # XXX all `partial`-ed in by `post_mortem()` below! | ||||||
|  |     tb: TracebackType, | ||||||
|  |     api_frame: FrameType, | ||||||
|  | 
 | ||||||
|  |     shield: bool = False, | ||||||
|  |     hide_tb: bool = True, | ||||||
|  | 
 | ||||||
|  |     # maybe pre/post REPL entry | ||||||
|  |     repl_fixture: ( | ||||||
|  |         AbstractContextManager[bool] | ||||||
|  |         |None | ||||||
|  |     ) = None, | ||||||
|  | 
 | ||||||
|  |     boxed_maybe_exc: BoxedMaybeException|None = None, | ||||||
|  | 
 | ||||||
|  | ) -> None: | ||||||
|  |     ''' | ||||||
|  |     Enter the ``pdbpp`` port mortem entrypoint using our custom | ||||||
|  |     debugger instance. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     __tracebackhide__: bool = hide_tb | ||||||
|  | 
 | ||||||
|  |     # maybe enter any user fixture | ||||||
|  |     enter_repl: bool = DebugStatus.maybe_enter_repl_fixture( | ||||||
|  |         repl=repl, | ||||||
|  |         repl_fixture=repl_fixture, | ||||||
|  |         boxed_maybe_exc=boxed_maybe_exc, | ||||||
|  |     ) | ||||||
|  |     try: | ||||||
|  |         if not enter_repl: | ||||||
|  |             # XXX, trigger `.release()` below immediately! | ||||||
|  |             return | ||||||
|  |         try: | ||||||
|  |             actor: Actor = current_actor() | ||||||
|  |             actor_repr: str = str(actor.uid) | ||||||
|  |             # ^TODO, instead a nice runtime-info + maddr + uid? | ||||||
|  |             # -[ ] impl a `Actor.__repr()__`?? | ||||||
|  |             #  |_ <task>:<thread> @ <actor> | ||||||
|  | 
 | ||||||
|  |         except NoRuntime: | ||||||
|  |             actor_repr: str = '<no-actor-runtime?>' | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             task_repr: Task = trio.lowlevel.current_task() | ||||||
|  |         except RuntimeError: | ||||||
|  |             task_repr: str = '<unknown-Task>' | ||||||
|  | 
 | ||||||
|  |         # TODO: print the actor supervion tree up to the root | ||||||
|  |         # here! Bo | ||||||
|  |         log.pdb( | ||||||
|  |             f'{_crash_msg}\n' | ||||||
|  |             f'x>(\n' | ||||||
|  |             f' |_ {task_repr} @ {actor_repr}\n' | ||||||
|  | 
 | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # XXX NOTE(s) on `pdbp.xpm()` version.. | ||||||
|  |         # | ||||||
|  |         # - seems to lose the up-stack tb-info? | ||||||
|  |         # - currently we're (only) replacing this from `pdbp.xpm()` | ||||||
|  |         #   to add the `end=''` to the print XD | ||||||
|  |         # | ||||||
|  |         print(traceback.format_exc(), end='') | ||||||
|  |         caller_frame: FrameType = api_frame.f_back | ||||||
|  | 
 | ||||||
|  |         # NOTE, see the impl details of these in the lib to | ||||||
|  |         # understand usage: | ||||||
|  |         # - `pdbp.post_mortem()` | ||||||
|  |         # - `pdbp.xps()` | ||||||
|  |         # - `bdb.interaction()` | ||||||
|  |         repl.reset() | ||||||
|  |         repl.interaction( | ||||||
|  |             frame=caller_frame, | ||||||
|  |             # frame=None, | ||||||
|  |             traceback=tb, | ||||||
|  |         ) | ||||||
|  |     finally: | ||||||
|  |         # XXX NOTE XXX: this is abs required to avoid hangs! | ||||||
|  |         # | ||||||
|  |         # Since we presume the post-mortem was enaged to | ||||||
|  |         # a task-ending error, we MUST release the local REPL request | ||||||
|  |         # so that not other local task nor the root remains blocked! | ||||||
|  |         DebugStatus.release() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def post_mortem( | ||||||
|  |     *, | ||||||
|  |     tb: TracebackType|None = None, | ||||||
|  |     api_frame: FrameType|None = None, | ||||||
|  |     hide_tb: bool = False, | ||||||
|  | 
 | ||||||
|  |     # TODO: support shield here just like in `pause()`? | ||||||
|  |     # shield: bool = False, | ||||||
|  | 
 | ||||||
|  |     **_pause_kwargs, | ||||||
|  | 
 | ||||||
|  | ) -> None: | ||||||
|  |     ''' | ||||||
|  |     Our builtin async equivalient of `pdb.post_mortem()` which can be | ||||||
|  |     used inside exception handlers. | ||||||
|  | 
 | ||||||
|  |     It's also used for the crash handler when `debug_mode == True` ;) | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     __tracebackhide__: bool = hide_tb | ||||||
|  | 
 | ||||||
|  |     tb: TracebackType = tb or sys.exc_info()[2] | ||||||
|  | 
 | ||||||
|  |     # TODO: do upward stack scan for highest @api_frame and | ||||||
|  |     # use its parent frame as the expected user-app code | ||||||
|  |     # interact point. | ||||||
|  |     api_frame: FrameType = api_frame or inspect.currentframe() | ||||||
|  | 
 | ||||||
|  |     # TODO, move to submod `._pausing` or ._api? _trace | ||||||
|  |     await _pause( | ||||||
|  |         debug_func=partial( | ||||||
|  |             _post_mortem, | ||||||
|  |             api_frame=api_frame, | ||||||
|  |             tb=tb, | ||||||
|  |         ), | ||||||
|  |         hide_tb=hide_tb, | ||||||
|  |         **_pause_kwargs | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def _maybe_enter_pm( | ||||||
|  |     err: BaseException, | ||||||
|  |     *, | ||||||
|  |     tb: TracebackType|None = None, | ||||||
|  |     api_frame: FrameType|None = None, | ||||||
|  |     hide_tb: bool = True, | ||||||
|  | 
 | ||||||
|  |     # only enter debugger REPL when returns `True` | ||||||
|  |     debug_filter: Callable[ | ||||||
|  |         [BaseException|BaseExceptionGroup], | ||||||
|  |         bool, | ||||||
|  |     ] = lambda err: not is_multi_cancelled(err), | ||||||
|  |     **_pause_kws, | ||||||
|  | ): | ||||||
|  |     if ( | ||||||
|  |         debug_mode() | ||||||
|  | 
 | ||||||
|  |         # NOTE: don't enter debug mode recursively after quitting pdb | ||||||
|  |         # Iow, don't re-enter the repl if the `quit` command was issued | ||||||
|  |         # by the user. | ||||||
|  |         and not isinstance(err, bdb.BdbQuit) | ||||||
|  | 
 | ||||||
|  |         # XXX: if the error is the likely result of runtime-wide | ||||||
|  |         # cancellation, we don't want to enter the debugger since | ||||||
|  |         # there's races between when the parent actor has killed all | ||||||
|  |         # comms and when the child tries to contact said parent to | ||||||
|  |         # acquire the tty lock. | ||||||
|  | 
 | ||||||
|  |         # Really we just want to mostly avoid catching KBIs here so there | ||||||
|  |         # might be a simpler check we can do? | ||||||
|  |         and | ||||||
|  |         debug_filter(err) | ||||||
|  |     ): | ||||||
|  |         api_frame: FrameType = api_frame or inspect.currentframe() | ||||||
|  |         tb: TracebackType = tb or sys.exc_info()[2] | ||||||
|  |         await post_mortem( | ||||||
|  |             api_frame=api_frame, | ||||||
|  |             tb=tb, | ||||||
|  |             **_pause_kws, | ||||||
|  |         ) | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     else: | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # TODO: better naming and what additionals? | ||||||
|  | # - [ ] optional runtime plugging? | ||||||
|  | # - [ ] detection for sync vs. async code? | ||||||
|  | # - [ ] specialized REPL entry when in distributed mode? | ||||||
|  | # -[x] hide tb by def | ||||||
|  | # - [x] allow ignoring kbi Bo | ||||||
|  | @cm | ||||||
|  | def open_crash_handler( | ||||||
|  |     catch: set[BaseException] = { | ||||||
|  |         BaseException, | ||||||
|  |     }, | ||||||
|  |     ignore: set[BaseException] = { | ||||||
|  |         KeyboardInterrupt, | ||||||
|  |         trio.Cancelled, | ||||||
|  |     }, | ||||||
|  |     hide_tb: bool = True, | ||||||
|  | 
 | ||||||
|  |     repl_fixture: ( | ||||||
|  |         AbstractContextManager[bool]  # pre/post REPL entry | ||||||
|  |         |None | ||||||
|  |     ) = None, | ||||||
|  |     raise_on_exit: bool|Sequence[Type[BaseException]] = True, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Generic "post mortem" crash handler using `pdbp` REPL debugger. | ||||||
|  | 
 | ||||||
|  |     We expose this as a CLI framework addon to both `click` and | ||||||
|  |     `typer` users so they can quickly wrap cmd endpoints which get | ||||||
|  |     automatically wrapped to use the runtime's `debug_mode: bool` | ||||||
|  |     AND `pdbp.pm()` around any code that is PRE-runtime entry | ||||||
|  |     - any sync code which runs BEFORE the main call to | ||||||
|  |       `trio.run()`. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     __tracebackhide__: bool = hide_tb | ||||||
|  | 
 | ||||||
|  |     # 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( | ||||||
|  |         raise_on_exit=raise_on_exit, | ||||||
|  |     ) | ||||||
|  |     err: BaseException | ||||||
|  |     try: | ||||||
|  |         yield boxed_maybe_exc | ||||||
|  |     except tuple(catch) as err: | ||||||
|  |         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 of `pdbp.xpm()` | ||||||
|  |                 _post_mortem( | ||||||
|  |                     repl=mk_pdb(), | ||||||
|  |                     tb=sys.exc_info()[2], | ||||||
|  |                     api_frame=inspect.currentframe().f_back, | ||||||
|  |                     hide_tb=hide_tb, | ||||||
|  | 
 | ||||||
|  |                     repl_fixture=repl_fixture, | ||||||
|  |                     boxed_maybe_exc=boxed_maybe_exc, | ||||||
|  |                 ) | ||||||
|  |             except bdb.BdbQuit: | ||||||
|  |                 __tracebackhide__: bool = False | ||||||
|  |                 raise err | ||||||
|  | 
 | ||||||
|  |         if ( | ||||||
|  |             raise_on_exit is True | ||||||
|  |             or ( | ||||||
|  |                 raise_on_exit is not False | ||||||
|  |                 and ( | ||||||
|  |                     set(raise_on_exit) | ||||||
|  |                     and | ||||||
|  |                     type(err) in raise_on_exit | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |             and | ||||||
|  |             boxed_maybe_exc.raise_on_exit == raise_on_exit | ||||||
|  |         ): | ||||||
|  |             raise err | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @cm | ||||||
|  | def maybe_open_crash_handler( | ||||||
|  |     pdb: bool|None = None, | ||||||
|  |     hide_tb: bool = True, | ||||||
|  | 
 | ||||||
|  |     **kwargs, | ||||||
|  | ): | ||||||
|  |     ''' | ||||||
|  |     Same as `open_crash_handler()` but with bool input flag | ||||||
|  |     to allow conditional handling. | ||||||
|  | 
 | ||||||
|  |     Normally this is used with CLI endpoints such that if the --pdb | ||||||
|  |     flag is passed the pdb REPL is engaed on any crashes B) | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     __tracebackhide__: bool = hide_tb | ||||||
|  | 
 | ||||||
|  |     if pdb is None: | ||||||
|  |         pdb: bool = _state.is_debug_mode() | ||||||
|  | 
 | ||||||
|  |     rtctx = nullcontext( | ||||||
|  |         enter_result=BoxedMaybeException() | ||||||
|  |     ) | ||||||
|  |     if pdb: | ||||||
|  |         rtctx = open_crash_handler( | ||||||
|  |             hide_tb=hide_tb, | ||||||
|  |             **kwargs, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     with rtctx as boxed_maybe_exc: | ||||||
|  |         yield boxed_maybe_exc | ||||||
|  | @ -0,0 +1,207 @@ | ||||||
|  | # tractor: structured concurrent "actors". | ||||||
|  | # Copyright 2018-eternity Tyler Goodlet. | ||||||
|  | 
 | ||||||
|  | # This program is free software: you can redistribute it and/or | ||||||
|  | # modify it under the terms of the GNU Affero General Public License | ||||||
|  | # as published by the Free Software Foundation, either version 3 of | ||||||
|  | # the License, or (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | # This program is distributed in the hope that it will be useful, but | ||||||
|  | # WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | ||||||
|  | # Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | # You should have received a copy of the GNU Affero General Public | ||||||
|  | # License along with this program.  If not, see | ||||||
|  | # <https://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | `pdpp.Pdb` extentions/customization and other delegate usage. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from functools import ( | ||||||
|  |     cached_property, | ||||||
|  | ) | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | import pdbp | ||||||
|  | from tractor._state import ( | ||||||
|  |     is_root_process, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | from ._tty_lock import ( | ||||||
|  |     Lock, | ||||||
|  |     DebugStatus, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TractorConfig(pdbp.DefaultConfig): | ||||||
|  |     ''' | ||||||
|  |     Custom `pdbp` config which tries to use the best tradeoff | ||||||
|  |     between pretty and minimal. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     use_pygments: bool = True | ||||||
|  |     sticky_by_default: bool = False | ||||||
|  |     enable_hidden_frames: bool = True | ||||||
|  | 
 | ||||||
|  |     # much thanks @mdmintz for the hot tip! | ||||||
|  |     # fixes line spacing issue when resizing terminal B) | ||||||
|  |     truncate_long_lines: bool = False | ||||||
|  | 
 | ||||||
|  |     # ------ - ------ | ||||||
|  |     # our own custom config vars mostly | ||||||
|  |     # for syncing with the actor tree's singleton | ||||||
|  |     # TTY `Lock`. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PdbREPL(pdbp.Pdb): | ||||||
|  |     ''' | ||||||
|  |     Add teardown hooks and local state describing any | ||||||
|  |     ongoing TTY `Lock` request dialog. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     # override the pdbp config with our coolio one | ||||||
|  |     # NOTE: this is only loaded when no `~/.pdbrc` exists | ||||||
|  |     # so we should prolly pass it into the .__init__() instead? | ||||||
|  |     # i dunno, see the `DefaultFactory` and `pdb.Pdb` impls. | ||||||
|  |     DefaultConfig = TractorConfig | ||||||
|  | 
 | ||||||
|  |     status = DebugStatus | ||||||
|  | 
 | ||||||
|  |     # NOTE: see details in stdlib's `bdb.py` | ||||||
|  |     # def user_exception(self, frame, exc_info): | ||||||
|  |     #     ''' | ||||||
|  |     #     Called when we stop on an exception. | ||||||
|  |     #     ''' | ||||||
|  |     #     log.warning( | ||||||
|  |     #         'Exception during REPL sesh\n\n' | ||||||
|  |     #         f'{frame}\n\n' | ||||||
|  |     #         f'{exc_info}\n\n' | ||||||
|  |     #     ) | ||||||
|  | 
 | ||||||
|  |     # NOTE: this actually hooks but i don't see anyway to detect | ||||||
|  |     # if an error was caught.. this is why currently we just always | ||||||
|  |     # call `DebugStatus.release` inside `_post_mortem()`. | ||||||
|  |     # def preloop(self): | ||||||
|  |     #     print('IN PRELOOP') | ||||||
|  |     #     super().preloop() | ||||||
|  | 
 | ||||||
|  |     # TODO: cleaner re-wrapping of all this? | ||||||
|  |     # -[ ] figure out how to disallow recursive .set_trace() entry | ||||||
|  |     #     since that'll cause deadlock for us. | ||||||
|  |     # -[ ] maybe a `@cm` to call `super().<same_meth_name>()`? | ||||||
|  |     # -[ ] look at hooking into the `pp` hook specially with our | ||||||
|  |     #     own set of pretty-printers? | ||||||
|  |     #    * `.pretty_struct.Struct.pformat()` | ||||||
|  |     #    * `.pformat(MsgType.pld)` | ||||||
|  |     #    * `.pformat(Error.tb_str)`? | ||||||
|  |     #    * .. maybe more? | ||||||
|  |     # | ||||||
|  |     def set_continue(self): | ||||||
|  |         try: | ||||||
|  |             super().set_continue() | ||||||
|  |         finally: | ||||||
|  |             # NOTE: for subactors the stdio lock is released via the | ||||||
|  |             # allocated RPC locker task, so for root we have to do it | ||||||
|  |             # manually. | ||||||
|  |             if ( | ||||||
|  |                 is_root_process() | ||||||
|  |                 and | ||||||
|  |                 Lock._debug_lock.locked() | ||||||
|  |                 and | ||||||
|  |                 DebugStatus.is_main_trio_thread() | ||||||
|  |             ): | ||||||
|  |                 # Lock.release(raise_on_thread=False) | ||||||
|  |                 Lock.release() | ||||||
|  | 
 | ||||||
|  |             # XXX AFTER `Lock.release()` for root local repl usage | ||||||
|  |             DebugStatus.release() | ||||||
|  | 
 | ||||||
|  |     def set_quit(self): | ||||||
|  |         try: | ||||||
|  |             super().set_quit() | ||||||
|  |         finally: | ||||||
|  |             if ( | ||||||
|  |                 is_root_process() | ||||||
|  |                 and | ||||||
|  |                 Lock._debug_lock.locked() | ||||||
|  |                 and | ||||||
|  |                 DebugStatus.is_main_trio_thread() | ||||||
|  |             ): | ||||||
|  |                 # Lock.release(raise_on_thread=False) | ||||||
|  |                 Lock.release() | ||||||
|  | 
 | ||||||
|  |             # XXX after `Lock.release()` for root local repl usage | ||||||
|  |             DebugStatus.release() | ||||||
|  | 
 | ||||||
|  |     # XXX NOTE: we only override this because apparently the stdlib pdb | ||||||
|  |     # bois likes to touch the SIGINT handler as much as i like to touch | ||||||
|  |     # my d$%&. | ||||||
|  |     def _cmdloop(self): | ||||||
|  |         self.cmdloop() | ||||||
|  | 
 | ||||||
|  |     @cached_property | ||||||
|  |     def shname(self) -> str | None: | ||||||
|  |         ''' | ||||||
|  |         Attempt to return the login shell name with a special check for | ||||||
|  |         the infamous `xonsh` since it seems to have some issues much | ||||||
|  |         different from std shells when it comes to flushing the prompt? | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         # SUPER HACKY and only really works if `xonsh` is not used | ||||||
|  |         # before spawning further sub-shells.. | ||||||
|  |         shpath = os.getenv('SHELL', None) | ||||||
|  | 
 | ||||||
|  |         if shpath: | ||||||
|  |             if ( | ||||||
|  |                 os.getenv('XONSH_LOGIN', default=False) | ||||||
|  |                 or 'xonsh' in shpath | ||||||
|  |             ): | ||||||
|  |                 return 'xonsh' | ||||||
|  | 
 | ||||||
|  |             return os.path.basename(shpath) | ||||||
|  | 
 | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def mk_pdb() -> PdbREPL: | ||||||
|  |     ''' | ||||||
|  |     Deliver a new `PdbREPL`: a multi-process safe `pdbp.Pdb`-variant | ||||||
|  |     using the magic of `tractor`'s SC-safe IPC. | ||||||
|  | 
 | ||||||
|  |     B) | ||||||
|  | 
 | ||||||
|  |     Our `pdb.Pdb` subtype accomplishes multi-process safe debugging | ||||||
|  |     by: | ||||||
|  | 
 | ||||||
|  |     - mutexing access to the root process' std-streams (& thus parent | ||||||
|  |       process TTY) via an IPC managed `Lock` singleton per | ||||||
|  |       actor-process tree. | ||||||
|  | 
 | ||||||
|  |     - temporarily overriding any subactor's SIGINT handler to shield | ||||||
|  |       during live REPL sessions in sub-actors such that cancellation | ||||||
|  |       is never (mistakenly) triggered by a ctrl-c and instead only by | ||||||
|  |       explicit runtime API requests or after the | ||||||
|  |       `pdb.Pdb.interaction()` call has returned. | ||||||
|  | 
 | ||||||
|  |     FURTHER, the `pdbp.Pdb` instance is configured to be `trio` | ||||||
|  |     "compatible" from a SIGINT handling perspective; we mask out | ||||||
|  |     the default `pdb` handler and instead apply `trio`s default | ||||||
|  |     which mostly addresses all issues described in: | ||||||
|  | 
 | ||||||
|  |      - https://github.com/python-trio/trio/issues/1155 | ||||||
|  | 
 | ||||||
|  |     The instance returned from this factory should always be | ||||||
|  |     preferred over the default `pdb[p].set_trace()` whenever using | ||||||
|  |     a `pdb` REPL inside a `trio` based runtime. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     pdb = PdbREPL() | ||||||
|  | 
 | ||||||
|  |     # XXX: These are the important flags mentioned in | ||||||
|  |     # https://github.com/python-trio/trio/issues/1155 | ||||||
|  |     # which resolve the traceback spews to console. | ||||||
|  |     pdb.allow_kbdint = True | ||||||
|  |     pdb.nosigint = True | ||||||
|  |     return pdb | ||||||
|  | @ -0,0 +1,333 @@ | ||||||
|  | # tractor: structured concurrent "actors". | ||||||
|  | # Copyright 2018-eternity Tyler Goodlet. | ||||||
|  | 
 | ||||||
|  | # This program is free software: you can redistribute it and/or | ||||||
|  | # modify it under the terms of the GNU Affero General Public License | ||||||
|  | # as published by the Free Software Foundation, either version 3 of | ||||||
|  | # the License, or (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | # This program is distributed in the hope that it will be useful, but | ||||||
|  | # WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | ||||||
|  | # Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | # You should have received a copy of the GNU Affero General Public | ||||||
|  | # License along with this program.  If not, see | ||||||
|  | # <https://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | A custom SIGINT handler which mainly shields actor (task) | ||||||
|  | cancellation during REPL interaction. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | from typing import ( | ||||||
|  |     TYPE_CHECKING, | ||||||
|  | ) | ||||||
|  | import trio | ||||||
|  | from tractor.log import get_logger | ||||||
|  | from tractor._state import ( | ||||||
|  |     current_actor, | ||||||
|  |     is_root_process, | ||||||
|  | ) | ||||||
|  | from ._repl import ( | ||||||
|  |     PdbREPL, | ||||||
|  | ) | ||||||
|  | from ._tty_lock import ( | ||||||
|  |     any_connected_locker_child, | ||||||
|  |     DebugStatus, | ||||||
|  |     Lock, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from tractor.ipc import ( | ||||||
|  |         Channel, | ||||||
|  |     ) | ||||||
|  |     from tractor._runtime import ( | ||||||
|  |         Actor, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | _ctlc_ignore_header: str = ( | ||||||
|  |     'Ignoring SIGINT while debug REPL in use' | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def sigint_shield( | ||||||
|  |     signum: int, | ||||||
|  |     frame: 'frame',  # type: ignore # noqa | ||||||
|  |     *args, | ||||||
|  | 
 | ||||||
|  | ) -> None: | ||||||
|  |     ''' | ||||||
|  |     Specialized, debugger-aware SIGINT handler. | ||||||
|  | 
 | ||||||
|  |     In childred we always ignore/shield for SIGINT to avoid | ||||||
|  |     deadlocks since cancellation should always be managed by the | ||||||
|  |     supervising parent actor. The root actor-proces is always | ||||||
|  |     cancelled on ctrl-c. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     __tracebackhide__: bool = True | ||||||
|  |     actor: Actor = current_actor() | ||||||
|  | 
 | ||||||
|  |     def do_cancel(): | ||||||
|  |         # If we haven't tried to cancel the runtime then do that instead | ||||||
|  |         # of raising a KBI (which may non-gracefully destroy | ||||||
|  |         # a ``trio.run()``). | ||||||
|  |         if not actor._cancel_called: | ||||||
|  |             actor.cancel_soon() | ||||||
|  | 
 | ||||||
|  |         # If the runtime is already cancelled it likely means the user | ||||||
|  |         # hit ctrl-c again because teardown didn't fully take place in | ||||||
|  |         # which case we do the "hard" raising of a local KBI. | ||||||
|  |         else: | ||||||
|  |             raise KeyboardInterrupt | ||||||
|  | 
 | ||||||
|  |     # only set in the actor actually running the REPL | ||||||
|  |     repl: PdbREPL|None = DebugStatus.repl | ||||||
|  | 
 | ||||||
|  |     # TODO: maybe we should flatten out all these cases using | ||||||
|  |     # a match/case? | ||||||
|  |     # | ||||||
|  |     # root actor branch that reports whether or not a child | ||||||
|  |     # has locked debugger. | ||||||
|  |     if is_root_process(): | ||||||
|  |         # log.warning( | ||||||
|  |         log.devx( | ||||||
|  |             'Handling SIGINT in root actor\n' | ||||||
|  |             f'{Lock.repr()}' | ||||||
|  |             f'{DebugStatus.repr()}\n' | ||||||
|  |         ) | ||||||
|  |         # try to see if the supposed (sub)actor in debug still | ||||||
|  |         # has an active connection to *this* actor, and if not | ||||||
|  |         # it's likely they aren't using the TTY lock / debugger | ||||||
|  |         # and we should propagate SIGINT normally. | ||||||
|  |         any_connected: bool = any_connected_locker_child() | ||||||
|  | 
 | ||||||
|  |         problem = ( | ||||||
|  |             f'root {actor.uid} handling SIGINT\n' | ||||||
|  |             f'any_connected: {any_connected}\n\n' | ||||||
|  | 
 | ||||||
|  |             f'{Lock.repr()}\n' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         if ( | ||||||
|  |             (ctx := Lock.ctx_in_debug) | ||||||
|  |             and | ||||||
|  |             (uid_in_debug := ctx.chan.uid) # "someone" is (ostensibly) using debug `Lock` | ||||||
|  |         ): | ||||||
|  |             name_in_debug: str = uid_in_debug[0] | ||||||
|  |             assert not repl | ||||||
|  |             # if not repl:  # but it's NOT us, the root actor. | ||||||
|  |             # sanity: since no repl ref is set, we def shouldn't | ||||||
|  |             # be the lock owner! | ||||||
|  |             assert name_in_debug != 'root' | ||||||
|  | 
 | ||||||
|  |             # IDEAL CASE: child has REPL as expected | ||||||
|  |             if any_connected:  # there are subactors we can contact | ||||||
|  |                 # XXX: only if there is an existing connection to the | ||||||
|  |                 # (sub-)actor in debug do we ignore SIGINT in this | ||||||
|  |                 # parent! Otherwise we may hang waiting for an actor | ||||||
|  |                 # which has already terminated to unlock. | ||||||
|  |                 # | ||||||
|  |                 # NOTE: don't emit this with `.pdb()` level in | ||||||
|  |                 # root without a higher level. | ||||||
|  |                 log.runtime( | ||||||
|  |                     _ctlc_ignore_header | ||||||
|  |                     + | ||||||
|  |                     f' by child ' | ||||||
|  |                     f'{uid_in_debug}\n' | ||||||
|  |                 ) | ||||||
|  |                 problem = None | ||||||
|  | 
 | ||||||
|  |             else: | ||||||
|  |                 problem += ( | ||||||
|  |                     '\n' | ||||||
|  |                     f'A `pdb` REPL is SUPPOSEDLY in use by child {uid_in_debug}\n' | ||||||
|  |                     f'BUT, no child actors are IPC contactable!?!?\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |         # IDEAL CASE: root has REPL as expected | ||||||
|  |         else: | ||||||
|  |             # root actor still has this SIGINT handler active without | ||||||
|  |             # an actor using the `Lock` (a bug state) ?? | ||||||
|  |             # => so immediately cancel any stale lock cs and revert | ||||||
|  |             # the handler! | ||||||
|  |             if not DebugStatus.repl: | ||||||
|  |                 # TODO: WHEN should we revert back to ``trio`` | ||||||
|  |                 # handler if this one is stale? | ||||||
|  |                 # -[ ] maybe after a counts work of ctl-c mashes? | ||||||
|  |                 # -[ ] use a state var like `stale_handler: bool`? | ||||||
|  |                 problem += ( | ||||||
|  |                     'No subactor is using a `pdb` REPL according `Lock.ctx_in_debug`?\n' | ||||||
|  |                     'BUT, the root should be using it, WHY this handler ??\n\n' | ||||||
|  |                     'So either..\n' | ||||||
|  |                     '- some root-thread is using it but has no `.repl` set?, OR\n' | ||||||
|  |                     '- something else weird is going on outside the runtime!?\n' | ||||||
|  |                 ) | ||||||
|  |             else: | ||||||
|  |                 # NOTE: since we emit this msg on ctl-c, we should | ||||||
|  |                 # also always re-print the prompt the tail block! | ||||||
|  |                 log.pdb( | ||||||
|  |                     _ctlc_ignore_header | ||||||
|  |                     + | ||||||
|  |                     f' by root actor..\n' | ||||||
|  |                     f'{DebugStatus.repl_task}\n' | ||||||
|  |                     f' |_{repl}\n' | ||||||
|  |                 ) | ||||||
|  |                 problem = None | ||||||
|  | 
 | ||||||
|  |         # XXX if one is set it means we ARE NOT operating an ideal | ||||||
|  |         # case where a child subactor or us (the root) has the | ||||||
|  |         # lock without any other detected problems. | ||||||
|  |         if problem: | ||||||
|  | 
 | ||||||
|  |             # detect, report and maybe clear a stale lock request | ||||||
|  |             # cancel scope. | ||||||
|  |             lock_cs: trio.CancelScope = Lock.get_locking_task_cs() | ||||||
|  |             maybe_stale_lock_cs: bool = ( | ||||||
|  |                 lock_cs is not None | ||||||
|  |                 and not lock_cs.cancel_called | ||||||
|  |             ) | ||||||
|  |             if maybe_stale_lock_cs: | ||||||
|  |                 problem += ( | ||||||
|  |                     '\n' | ||||||
|  |                     'Stale `Lock.ctx_in_debug._scope: CancelScope` detected?\n' | ||||||
|  |                     f'{Lock.ctx_in_debug}\n\n' | ||||||
|  | 
 | ||||||
|  |                     '-> Calling ctx._scope.cancel()!\n' | ||||||
|  |                 ) | ||||||
|  |                 lock_cs.cancel() | ||||||
|  | 
 | ||||||
|  |             # TODO: wen do we actually want/need this, see above. | ||||||
|  |             # DebugStatus.unshield_sigint() | ||||||
|  |             log.warning(problem) | ||||||
|  | 
 | ||||||
|  |     # child actor that has locked the debugger | ||||||
|  |     elif not is_root_process(): | ||||||
|  |         log.debug( | ||||||
|  |             f'Subactor {actor.uid} handling SIGINT\n\n' | ||||||
|  |             f'{Lock.repr()}\n' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         rent_chan: Channel = actor._parent_chan | ||||||
|  |         if ( | ||||||
|  |             rent_chan is None | ||||||
|  |             or | ||||||
|  |             not rent_chan.connected() | ||||||
|  |         ): | ||||||
|  |             log.warning( | ||||||
|  |                 'This sub-actor thinks it is debugging ' | ||||||
|  |                 'but it has no connection to its parent ??\n' | ||||||
|  |                 f'{actor.uid}\n' | ||||||
|  |                 'Allowing SIGINT propagation..' | ||||||
|  |             ) | ||||||
|  |             DebugStatus.unshield_sigint() | ||||||
|  | 
 | ||||||
|  |         repl_task: str|None = DebugStatus.repl_task | ||||||
|  |         req_task: str|None = DebugStatus.req_task | ||||||
|  |         if ( | ||||||
|  |             repl_task | ||||||
|  |             and | ||||||
|  |             repl | ||||||
|  |         ): | ||||||
|  |             log.pdb( | ||||||
|  |                 _ctlc_ignore_header | ||||||
|  |                 + | ||||||
|  |                 f' by local task\n\n' | ||||||
|  |                 f'{repl_task}\n' | ||||||
|  |                 f' |_{repl}\n' | ||||||
|  |             ) | ||||||
|  |         elif req_task: | ||||||
|  |             log.debug( | ||||||
|  |                 _ctlc_ignore_header | ||||||
|  |                 + | ||||||
|  |                 f' by local request-task and either,\n' | ||||||
|  |                 f'- someone else is already REPL-in and has the `Lock`, or\n' | ||||||
|  |                 f'- some other local task already is replin?\n\n' | ||||||
|  |                 f'{req_task}\n' | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         # TODO can we remove this now? | ||||||
|  |         # -[ ] does this path ever get hit any more? | ||||||
|  |         else: | ||||||
|  |             msg: str = ( | ||||||
|  |                 'SIGINT shield handler still active BUT, \n\n' | ||||||
|  |             ) | ||||||
|  |             if repl_task is None: | ||||||
|  |                 msg += ( | ||||||
|  |                     '- No local task claims to be in debug?\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             if repl is None: | ||||||
|  |                 msg += ( | ||||||
|  |                     '- No local REPL is currently active?\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             if req_task is None: | ||||||
|  |                 msg += ( | ||||||
|  |                     '- No debug request task is active?\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             log.warning( | ||||||
|  |                 msg | ||||||
|  |                 + | ||||||
|  |                 'Reverting handler to `trio` default!\n' | ||||||
|  |             ) | ||||||
|  |             DebugStatus.unshield_sigint() | ||||||
|  | 
 | ||||||
|  |             # XXX ensure that the reverted-to-handler actually is | ||||||
|  |             # able to rx what should have been **this** KBI ;) | ||||||
|  |             do_cancel() | ||||||
|  | 
 | ||||||
|  |         # TODO: how to handle the case of an intermediary-child actor | ||||||
|  |         # that **is not** marked in debug mode? See oustanding issue: | ||||||
|  |         # https://github.com/goodboy/tractor/issues/320 | ||||||
|  |         # elif debug_mode(): | ||||||
|  | 
 | ||||||
|  |     # maybe redraw/print last REPL output to console since | ||||||
|  |     # we want to alert the user that more input is expect since | ||||||
|  |     # nothing has been done dur to ignoring sigint. | ||||||
|  |     if ( | ||||||
|  |         DebugStatus.repl  # only when current actor has a REPL engaged | ||||||
|  |     ): | ||||||
|  |         flush_status: str = ( | ||||||
|  |             'Flushing stdout to ensure new prompt line!\n' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # XXX: yah, mega hack, but how else do we catch this madness XD | ||||||
|  |         if ( | ||||||
|  |             repl.shname == 'xonsh' | ||||||
|  |         ): | ||||||
|  |             flush_status += ( | ||||||
|  |                 '-> ALSO re-flushing due to `xonsh`..\n' | ||||||
|  |             ) | ||||||
|  |             repl.stdout.write(repl.prompt) | ||||||
|  | 
 | ||||||
|  |         # log.warning( | ||||||
|  |         log.devx( | ||||||
|  |             flush_status | ||||||
|  |         ) | ||||||
|  |         repl.stdout.flush() | ||||||
|  | 
 | ||||||
|  |         # TODO: better console UX to match the current "mode": | ||||||
|  |         # -[ ] for example if in sticky mode where if there is output | ||||||
|  |         #   detected as written to the tty we redraw this part underneath | ||||||
|  |         #   and erase the past draw of this same bit above? | ||||||
|  |         # repl.sticky = True | ||||||
|  |         # repl._print_if_sticky() | ||||||
|  | 
 | ||||||
|  |         # also see these links for an approach from `ptk`: | ||||||
|  |         # https://github.com/goodboy/tractor/issues/130#issuecomment-663752040 | ||||||
|  |         # https://github.com/prompt-toolkit/python-prompt-toolkit/blob/c2c6af8a0308f9e5d7c0e28cb8a02963fe0ce07a/prompt_toolkit/patch_stdout.py | ||||||
|  |     else: | ||||||
|  |         log.devx( | ||||||
|  |         # log.warning( | ||||||
|  |             'Not flushing stdout since not needed?\n' | ||||||
|  |             f'|_{repl}\n' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     # XXX only for tracing this handler | ||||||
|  |     log.devx('exiting SIGINT') | ||||||
|  | @ -0,0 +1,220 @@ | ||||||
|  | # tractor: structured concurrent "actors". | ||||||
|  | # Copyright 2018-eternity Tyler Goodlet. | ||||||
|  | 
 | ||||||
|  | # This program is free software: you can redistribute it and/or | ||||||
|  | # modify it under the terms of the GNU Affero General Public License | ||||||
|  | # as published by the Free Software Foundation, either version 3 of | ||||||
|  | # the License, or (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | # This program is distributed in the hope that it will be useful, but | ||||||
|  | # WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU | ||||||
|  | # Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | # You should have received a copy of the GNU Affero General Public | ||||||
|  | # License along with this program.  If not, see | ||||||
|  | # <https://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | Debugger synchronization APIs to ensure orderly access and | ||||||
|  | non-TTY-clobbering graceful teardown. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | from contextlib import ( | ||||||
|  |     asynccontextmanager as acm, | ||||||
|  | ) | ||||||
|  | from functools import ( | ||||||
|  |     partial, | ||||||
|  | ) | ||||||
|  | from typing import ( | ||||||
|  |     AsyncGenerator, | ||||||
|  |     Callable, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | from tractor.log import get_logger | ||||||
|  | import trio | ||||||
|  | from trio.lowlevel import ( | ||||||
|  |     current_task, | ||||||
|  |     Task, | ||||||
|  | ) | ||||||
|  | from tractor._context import Context | ||||||
|  | from tractor._state import ( | ||||||
|  |     current_actor, | ||||||
|  |     debug_mode, | ||||||
|  |     is_root_process, | ||||||
|  | ) | ||||||
|  | from ._repl import ( | ||||||
|  |     TractorConfig as TractorConfig, | ||||||
|  | ) | ||||||
|  | from ._tty_lock import ( | ||||||
|  |     Lock, | ||||||
|  |     request_root_stdio_lock, | ||||||
|  |     any_connected_locker_child, | ||||||
|  | ) | ||||||
|  | from ._sigint import ( | ||||||
|  |     sigint_shield as sigint_shield, | ||||||
|  |     _ctlc_ignore_header as _ctlc_ignore_header | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | log = get_logger(__package__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | async def maybe_wait_for_debugger( | ||||||
|  |     poll_steps: int = 2, | ||||||
|  |     poll_delay: float = 0.1, | ||||||
|  |     child_in_debug: bool = False, | ||||||
|  | 
 | ||||||
|  |     header_msg: str = '', | ||||||
|  |     _ll: str = 'devx', | ||||||
|  | 
 | ||||||
|  | ) -> bool:  # was locked and we polled? | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |         not debug_mode() | ||||||
|  |         and | ||||||
|  |         not child_in_debug | ||||||
|  |     ): | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     logmeth: Callable = getattr(log, _ll) | ||||||
|  | 
 | ||||||
|  |     msg: str = header_msg | ||||||
|  |     if ( | ||||||
|  |         is_root_process() | ||||||
|  |     ): | ||||||
|  |         # If we error in the root but the debugger is | ||||||
|  |         # engaged we don't want to prematurely kill (and | ||||||
|  |         # thus clobber access to) the local tty since it | ||||||
|  |         # will make the pdb repl unusable. | ||||||
|  |         # Instead try to wait for pdb to be released before | ||||||
|  |         # tearing down. | ||||||
|  |         ctx_in_debug: Context|None = Lock.ctx_in_debug | ||||||
|  |         in_debug: tuple[str, str]|None = ( | ||||||
|  |             ctx_in_debug.chan.uid | ||||||
|  |             if ctx_in_debug | ||||||
|  |             else None | ||||||
|  |         ) | ||||||
|  |         if in_debug == current_actor().uid: | ||||||
|  |             log.debug( | ||||||
|  |                 msg | ||||||
|  |                 + | ||||||
|  |                 'Root already owns the TTY LOCK' | ||||||
|  |             ) | ||||||
|  |             return True | ||||||
|  | 
 | ||||||
|  |         elif in_debug: | ||||||
|  |             msg += ( | ||||||
|  |                 f'Debug `Lock` in use by subactor\n|\n|_{in_debug}\n' | ||||||
|  |             ) | ||||||
|  |             # TODO: could this make things more deterministic? | ||||||
|  |             # wait to see if a sub-actor task will be | ||||||
|  |             # scheduled and grab the tty lock on the next | ||||||
|  |             # tick? | ||||||
|  |             # XXX => but it doesn't seem to work.. | ||||||
|  |             # await trio.testing.wait_all_tasks_blocked(cushion=0) | ||||||
|  |         else: | ||||||
|  |             logmeth( | ||||||
|  |                 msg | ||||||
|  |                 + | ||||||
|  |                 'Root immediately acquired debug TTY LOCK' | ||||||
|  |             ) | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |         for istep in range(poll_steps): | ||||||
|  |             if ( | ||||||
|  |                 Lock.req_handler_finished is not None | ||||||
|  |                 and not Lock.req_handler_finished.is_set() | ||||||
|  |                 and in_debug is not None | ||||||
|  |             ): | ||||||
|  |                 # caller_frame_info: str = pformat_caller_frame() | ||||||
|  |                 logmeth( | ||||||
|  |                     msg | ||||||
|  |                     + | ||||||
|  |                     '\n^^ Root is waiting on tty lock release.. ^^\n' | ||||||
|  |                     # f'{caller_frame_info}\n' | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |                 if not any_connected_locker_child(): | ||||||
|  |                     Lock.get_locking_task_cs().cancel() | ||||||
|  | 
 | ||||||
|  |                 with trio.CancelScope(shield=True): | ||||||
|  |                     await Lock.req_handler_finished.wait() | ||||||
|  | 
 | ||||||
|  |                 log.devx( | ||||||
|  |                     f'Subactor released debug lock\n' | ||||||
|  |                     f'|_{in_debug}\n' | ||||||
|  |                 ) | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |             # is no subactor locking debugger currently? | ||||||
|  |             if ( | ||||||
|  |                 in_debug is None | ||||||
|  |                 and ( | ||||||
|  |                     Lock.req_handler_finished is None | ||||||
|  |                     or Lock.req_handler_finished.is_set() | ||||||
|  |                 ) | ||||||
|  |             ): | ||||||
|  |                 logmeth( | ||||||
|  |                     msg | ||||||
|  |                     + | ||||||
|  |                     'Root acquired tty lock!' | ||||||
|  |                 ) | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |             else: | ||||||
|  |                 logmeth( | ||||||
|  |                     'Root polling for debug:\n' | ||||||
|  |                     f'poll step: {istep}\n' | ||||||
|  |                     f'poll delya: {poll_delay}\n\n' | ||||||
|  |                     f'{Lock.repr()}\n' | ||||||
|  |                 ) | ||||||
|  |                 with trio.CancelScope(shield=True): | ||||||
|  |                     await trio.sleep(poll_delay) | ||||||
|  |                     continue | ||||||
|  | 
 | ||||||
|  |         return True | ||||||
|  | 
 | ||||||
|  |     # else: | ||||||
|  |     #     # TODO: non-root call for #320? | ||||||
|  |     #     this_uid: tuple[str, str] = current_actor().uid | ||||||
|  |     #     async with acquire_debug_lock( | ||||||
|  |     #         subactor_uid=this_uid, | ||||||
|  |     #     ): | ||||||
|  |     #         pass | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @acm | ||||||
|  | async def acquire_debug_lock( | ||||||
|  |     subactor_uid: tuple[str, str], | ||||||
|  | ) -> AsyncGenerator[ | ||||||
|  |     trio.CancelScope|None, | ||||||
|  |     tuple, | ||||||
|  | ]: | ||||||
|  |     ''' | ||||||
|  |     Request to acquire the TTY `Lock` in the root actor, release on | ||||||
|  |     exit. | ||||||
|  | 
 | ||||||
|  |     This helper is for actor's who don't actually need to acquired | ||||||
|  |     the debugger but want to wait until the lock is free in the | ||||||
|  |     process-tree root such that they don't clobber an ongoing pdb | ||||||
|  |     REPL session in some peer or child! | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     if not debug_mode(): | ||||||
|  |         yield None | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     task: Task = current_task() | ||||||
|  |     async with trio.open_nursery() as n: | ||||||
|  |         ctx: Context = await n.start( | ||||||
|  |             partial( | ||||||
|  |                 request_root_stdio_lock, | ||||||
|  |                 actor_uid=subactor_uid, | ||||||
|  |                 task_uid=(task.name, id(task)), | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         yield ctx | ||||||
|  |         ctx.cancel() | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -15,8 +15,10 @@ | ||||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| Pretty formatters for use throughout the code base. | Pretty formatters for use throughout our internals. | ||||||
| Mostly handy for logging and exception message content. | 
 | ||||||
|  | Handy for logging and exception message content but also for `repr()` | ||||||
|  | in REPL(s). | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| import sys | import sys | ||||||
|  | @ -224,8 +226,8 @@ def pformat_cs( | ||||||
|     field_prefix: str = ' |_', |     field_prefix: str = ' |_', | ||||||
| ) -> str: | ) -> str: | ||||||
|     ''' |     ''' | ||||||
|     Pretty format info about a `trio.CancelScope` including most |     Pretty format info about a `trio.CancelScope` including most of | ||||||
|     of its public state and `._cancel_status`. |     its public state and `._cancel_status`. | ||||||
| 
 | 
 | ||||||
|     The output can be modified to show a "var name" for the |     The output can be modified to show a "var name" for the | ||||||
|     instance as a field prefix, just a simple str before each |     instance as a field prefix, just a simple str before each | ||||||
|  | @ -249,14 +251,33 @@ def pformat_cs( | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # TODO: move this func to some kinda `.devx.pformat.py` eventually |  | ||||||
| # as we work out our multi-domain state-flow-syntax! |  | ||||||
| def nest_from_op( | def nest_from_op( | ||||||
|     input_op: str, |     input_op: str,  # TODO, Literal of all op-"symbols" from below? | ||||||
|  |     text: str, | ||||||
|  |     prefix_op: bool = True,  # unset is to suffix the first line | ||||||
|  |     # optionally suffix `text`, by def on a newline | ||||||
|  |     op_suffix='\n', | ||||||
|  | 
 | ||||||
|  |     nest_prefix: str = '|_', | ||||||
|  |     nest_indent: int|None = None, | ||||||
|  |     # XXX indent `next_prefix` "to-the-right-of" `input_op` | ||||||
|  |     # by this count of whitespaces (' '). | ||||||
|  |     rm_from_first_ln: str|None = None, | ||||||
|  | 
 | ||||||
|  | ) -> str: | ||||||
|  |     ''' | ||||||
|  |     Depth-increment the input (presumably hierarchy/supervision) | ||||||
|  |     input "tree string" below the provided `input_op` execution | ||||||
|  |     operator, so injecting a `"\n|_{input_op}\n"`and indenting the | ||||||
|  |     `tree_str` to nest content aligned with the ops last char. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     # `sclang` "structurred-concurrency-language": an ascii-encoded | ||||||
|  |     # symbolic alphabet to describe concurrent systems. | ||||||
|     # |     # | ||||||
|     # ?TODO? an idea for a syntax to the state of concurrent systems |     # ?TODO? aa more fomal idea for a syntax to the state of | ||||||
|     # as a "3-domain" (execution, scope, storage) model and using |     # concurrent systems as a "3-domain" (execution, scope, storage) | ||||||
|     # a minimal ascii/utf-8 operator-set. |     # model and using a minimal ascii/utf-8 operator-set. | ||||||
|     # |     # | ||||||
|     # try not to take any of this seriously yet XD |     # try not to take any of this seriously yet XD | ||||||
|     # |     # | ||||||
|  | @ -322,29 +343,185 @@ def nest_from_op( | ||||||
|     # |     # | ||||||
|     # =>{  recv-req to open |     # =>{  recv-req to open | ||||||
|     # <={  send-status that it closed |     # <={  send-status that it closed | ||||||
|  |     # | ||||||
|  |     if ( | ||||||
|  |         nest_prefix | ||||||
|  |         and | ||||||
|  |         nest_indent != 0 | ||||||
|  |     ): | ||||||
|  |         if nest_indent is not None: | ||||||
|  |             nest_prefix: str = textwrap.indent( | ||||||
|  |                 nest_prefix, | ||||||
|  |                 prefix=nest_indent*' ', | ||||||
|  |             ) | ||||||
|  |         nest_indent: int = len(nest_prefix) | ||||||
| 
 | 
 | ||||||
|     tree_str: str, |     # determine body-text indent either by, | ||||||
|  |     # - using wtv explicit indent value is provided, | ||||||
|  |     # OR | ||||||
|  |     # - auto-calcing the indent to embed `text` under | ||||||
|  |     #   the `nest_prefix` if provided, **IFF** `nest_indent=None`. | ||||||
|  |     tree_str_indent: int = 0 | ||||||
|  |     if nest_indent not in {0, None}: | ||||||
|  |         tree_str_indent = nest_indent | ||||||
|  |     elif ( | ||||||
|  |         nest_prefix | ||||||
|  |         and | ||||||
|  |         nest_indent != 0 | ||||||
|  |     ): | ||||||
|  |         tree_str_indent = len(nest_prefix) | ||||||
| 
 | 
 | ||||||
|     # NOTE: so move back-from-the-left of the `input_op` by |     indented_tree_str: str = text | ||||||
|     # this amount. |     if tree_str_indent: | ||||||
|     back_from_op: int = 0, |         indented_tree_str: str = textwrap.indent( | ||||||
|  |             text, | ||||||
|  |             prefix=' '*tree_str_indent, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     # inject any provided nesting-prefix chars | ||||||
|  |     # into the head of the first line. | ||||||
|  |     if nest_prefix: | ||||||
|  |         indented_tree_str: str = ( | ||||||
|  |             f'{nest_prefix}{indented_tree_str[tree_str_indent:]}' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |         not prefix_op | ||||||
|  |         or | ||||||
|  |         rm_from_first_ln | ||||||
|  |     ): | ||||||
|  |         tree_lns: list[str] = indented_tree_str.splitlines() | ||||||
|  |         first: str = tree_lns[0] | ||||||
|  |         if rm_from_first_ln: | ||||||
|  |             first = first.strip().replace( | ||||||
|  |                 rm_from_first_ln, | ||||||
|  |                 '', | ||||||
|  |             ) | ||||||
|  |         indented_tree_str: str = '\n'.join(tree_lns[1:]) | ||||||
|  | 
 | ||||||
|  |         if prefix_op: | ||||||
|  |             indented_tree_str = ( | ||||||
|  |                 f'{first}\n' | ||||||
|  |                 f'{indented_tree_str}' | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |     if prefix_op: | ||||||
|  |         return ( | ||||||
|  |             f'{input_op}{op_suffix}' | ||||||
|  |             f'{indented_tree_str}' | ||||||
|  |         ) | ||||||
|  |     else: | ||||||
|  |         return ( | ||||||
|  |             f'{first}{input_op}{op_suffix}' | ||||||
|  |             f'{indented_tree_str}' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # ------ modden.repr ------ | ||||||
|  | # XXX originally taken verbaatim from `modden.repr` | ||||||
|  | ''' | ||||||
|  | More "multi-line" representation then the stdlib's `pprint` equivs. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from inspect import ( | ||||||
|  |     FrameInfo, | ||||||
|  |     stack, | ||||||
|  | ) | ||||||
|  | import pprint | ||||||
|  | import reprlib | ||||||
|  | from typing import ( | ||||||
|  |     Callable, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def mk_repr( | ||||||
|  |     **repr_kws, | ||||||
|  | ) -> Callable[[str], str]: | ||||||
|  |     ''' | ||||||
|  |     Allocate and deliver a `repr.Repr` instance with provided input | ||||||
|  |     settings using the std-lib's `reprlib` mod, | ||||||
|  |      * https://docs.python.org/3/library/reprlib.html | ||||||
|  | 
 | ||||||
|  |     ------ Ex. ------ | ||||||
|  |     An up to 6-layer-nested `dict` as multi-line: | ||||||
|  |     - https://stackoverflow.com/a/79102479 | ||||||
|  |     - https://docs.python.org/3/library/reprlib.html#reprlib.Repr.maxlevel | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     def_kws: dict[str, int] = dict( | ||||||
|  |         indent=3,  # indent used for repr of recursive objects | ||||||
|  |         maxlevel=616,  # recursion levels | ||||||
|  |         maxdict=616,  # max items shown for `dict` | ||||||
|  |         maxlist=616,  # max items shown for `dict` | ||||||
|  |         maxstring=616,  # match editor line-len limit | ||||||
|  |         maxtuple=616,  # match editor line-len limit | ||||||
|  |         maxother=616,  # match editor line-len limit | ||||||
|  |     ) | ||||||
|  |     def_kws |= repr_kws | ||||||
|  |     reprr = reprlib.Repr(**def_kws) | ||||||
|  |     return reprr.repr | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def ppfmt( | ||||||
|  |     obj: object, | ||||||
|  |     do_print: bool = False, | ||||||
| ) -> str: | ) -> str: | ||||||
|     ''' |     ''' | ||||||
|     Depth-increment the input (presumably hierarchy/supervision) |     The `pprint.pformat()` version of `pprint.pp()`, namely | ||||||
|     input "tree string" below the provided `input_op` execution |     a default `sort_dicts=False`.. (which i think should be | ||||||
|     operator, so injecting a `"\n|_{input_op}\n"`and indenting the |     the normal default in the stdlib). | ||||||
|     `tree_str` to nest content aligned with the ops last char. | 
 | ||||||
|  |     ''' | ||||||
|  |     pprepr: Callable = mk_repr() | ||||||
|  |     repr_str: str = pprepr(obj) | ||||||
|  | 
 | ||||||
|  |     if do_print: | ||||||
|  |         return pprint.pp(repr_str) | ||||||
|  | 
 | ||||||
|  |     return repr_str | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | pformat = ppfmt | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def pfmt_frame_info(fi: FrameInfo) -> str: | ||||||
|  |     ''' | ||||||
|  |     Like a std `inspect.FrameInfo.__repr__()` but multi-line.. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     return ( |     return ( | ||||||
|         f'{input_op}\n' |         'FrameInfo(\n' | ||||||
|         + |         '  frame={!r},\n' | ||||||
|         textwrap.indent( |         '  filename={!r},\n' | ||||||
|             tree_str, |         '  lineno={!r},\n' | ||||||
|             prefix=( |         '  function={!r},\n' | ||||||
|                 len(input_op) |         '  code_context={!r},\n' | ||||||
|                 - |         '  index={!r},\n' | ||||||
|                 (back_from_op + 1) |         '  positions={!r})' | ||||||
|             ) * ' ', |         ).format( | ||||||
|  |             fi.frame, | ||||||
|  |             fi.filename, | ||||||
|  |             fi.lineno, | ||||||
|  |             fi.function, | ||||||
|  |             fi.code_context, | ||||||
|  |             fi.index, | ||||||
|  |             fi.positions | ||||||
|         ) |         ) | ||||||
|     ) | 
 | ||||||
|  | 
 | ||||||
|  | def pfmt_callstack(frames: int = 1) -> str: | ||||||
|  |     ''' | ||||||
|  |     Generate a string of nested `inspect.FrameInfo` objects returned | ||||||
|  |     from a `inspect.stack()` call such that only the `.frame` field | ||||||
|  |     for each  layer is pprinted. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     caller_frames: list[FrameInfo] =  stack()[1:1+frames] | ||||||
|  |     frames_str: str = '' | ||||||
|  |     for i, frame_info in enumerate(caller_frames): | ||||||
|  |         frames_str += textwrap.indent( | ||||||
|  |             f'{frame_info.frame!r}\n', | ||||||
|  |             prefix=' '*i, | ||||||
|  | 
 | ||||||
|  |         ) | ||||||
|  |     return frames_str | ||||||
|  |  | ||||||
|  | @ -101,11 +101,27 @@ class Channel: | ||||||
|         # ^XXX! ONLY set if a remote actor sends an `Error`-msg |         # ^XXX! ONLY set if a remote actor sends an `Error`-msg | ||||||
|         self._closed: bool = False |         self._closed: bool = False | ||||||
| 
 | 
 | ||||||
|         # flag set by ``Portal.cancel_actor()`` indicating remote |         # flag set by `Portal.cancel_actor()` indicating remote | ||||||
|         # (possibly peer) cancellation of the far end actor |         # (possibly peer) cancellation of the far end actor runtime. | ||||||
|         # runtime. |  | ||||||
|         self._cancel_called: bool = False |         self._cancel_called: bool = False | ||||||
| 
 | 
 | ||||||
|  |     @property | ||||||
|  |     def closed(self) -> bool: | ||||||
|  |         ''' | ||||||
|  |         Was `.aclose()` successfully called? | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         return self._closed | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def cancel_called(self) -> bool: | ||||||
|  |         ''' | ||||||
|  |         Set when `Portal.cancel_actor()` is called on a portal which | ||||||
|  |         wraps this IPC channel. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         return self._cancel_called | ||||||
|  | 
 | ||||||
|     @property |     @property | ||||||
|     def uid(self) -> tuple[str, str]: |     def uid(self) -> tuple[str, str]: | ||||||
|         ''' |         ''' | ||||||
|  | @ -169,13 +185,27 @@ class Channel: | ||||||
|             addr, |             addr, | ||||||
|             **kwargs, |             **kwargs, | ||||||
|         ) |         ) | ||||||
|         assert transport.raddr == addr |         # XXX, for UDS *no!* since we recv the peer-pid and build out | ||||||
|  |         # a new addr.. | ||||||
|  |         # assert transport.raddr == addr | ||||||
|         chan = Channel(transport=transport) |         chan = Channel(transport=transport) | ||||||
|         log.runtime( | 
 | ||||||
|             f'Connected channel IPC transport\n' |         # ?TODO, compact this into adapter level-methods? | ||||||
|             f'[>\n' |         # -[ ] would avoid extra repr-calcs if level not active? | ||||||
|             f' |_{chan}\n' |         #   |_ how would the `calc_if_level` look though? func? | ||||||
|         ) |         if log.at_least_level('runtime'): | ||||||
|  |             from tractor.devx import ( | ||||||
|  |                 pformat as _pformat, | ||||||
|  |             ) | ||||||
|  |             chan_repr: str = _pformat.nest_from_op( | ||||||
|  |                 input_op='[>', | ||||||
|  |                 text=chan.pformat(), | ||||||
|  |                 nest_indent=1, | ||||||
|  |             ) | ||||||
|  |             log.runtime( | ||||||
|  |                 f'Connected channel IPC transport\n' | ||||||
|  |                 f'{chan_repr}' | ||||||
|  |             ) | ||||||
|         return chan |         return chan | ||||||
| 
 | 
 | ||||||
|     @cm |     @cm | ||||||
|  | @ -196,9 +226,12 @@ class Channel: | ||||||
|             self._transport.codec = orig |             self._transport.codec = orig | ||||||
| 
 | 
 | ||||||
|     # TODO: do a .src/.dst: str for maddrs? |     # TODO: do a .src/.dst: str for maddrs? | ||||||
|     def pformat(self) -> str: |     def pformat( | ||||||
|  |         self, | ||||||
|  |         privates: bool = False, | ||||||
|  |     ) -> str: | ||||||
|         if not self._transport: |         if not self._transport: | ||||||
|             return '<Channel with inactive transport?>' |             return '<Channel( with inactive transport? )>' | ||||||
| 
 | 
 | ||||||
|         tpt: MsgTransport = self._transport |         tpt: MsgTransport = self._transport | ||||||
|         tpt_name: str = type(tpt).__name__ |         tpt_name: str = type(tpt).__name__ | ||||||
|  | @ -206,26 +239,35 @@ class Channel: | ||||||
|             'connected' if self.connected() |             'connected' if self.connected() | ||||||
|             else 'closed' |             else 'closed' | ||||||
|         ) |         ) | ||||||
|         return ( |         repr_str: str = ( | ||||||
|             f'<Channel(\n' |             f'<Channel(\n' | ||||||
|             f' |_status: {tpt_status!r}\n' |             f' |_status: {tpt_status!r}\n' | ||||||
|  |         ) + ( | ||||||
|             f'   _closed={self._closed}\n' |             f'   _closed={self._closed}\n' | ||||||
|             f'   _cancel_called={self._cancel_called}\n' |             f'   _cancel_called={self._cancel_called}\n' | ||||||
|             f'\n' |             if privates else '' | ||||||
|             f' |_peer: {self.aid}\n' |         ) + (  # peer-actor (processs) section | ||||||
|             f'\n' |             f' |_peer: {self.aid.reprol()!r}\n' | ||||||
|  |             if self.aid else ' |_peer: <unknown>\n' | ||||||
|  |         ) + ( | ||||||
|             f' |_msgstream: {tpt_name}\n' |             f' |_msgstream: {tpt_name}\n' | ||||||
|             f'   proto={tpt.laddr.proto_key!r}\n' |             f'   maddr: {tpt.maddr!r}\n' | ||||||
|             f'   layer={tpt.layer_key!r}\n' |             f'   proto: {tpt.laddr.proto_key!r}\n' | ||||||
|             f'   laddr={tpt.laddr}\n' |             f'   layer: {tpt.layer_key!r}\n' | ||||||
|             f'   raddr={tpt.raddr}\n' |             f'   codec: {tpt.codec_key!r}\n' | ||||||
|             f'   codec={tpt.codec_key!r}\n' |             f'   .laddr={tpt.laddr}\n' | ||||||
|             f'   stream={tpt.stream}\n' |             f'   .raddr={tpt.raddr}\n' | ||||||
|             f'   maddr={tpt.maddr!r}\n' |         ) + ( | ||||||
|             f'   drained={tpt.drained}\n' |             f'   ._transport.stream={tpt.stream}\n' | ||||||
|  |             f'   ._transport.drained={tpt.drained}\n' | ||||||
|  |             if privates else '' | ||||||
|  |         ) + ( | ||||||
|             f'   _send_lock={tpt._send_lock.statistics()}\n' |             f'   _send_lock={tpt._send_lock.statistics()}\n' | ||||||
|             f')>\n' |             if privates else '' | ||||||
|  |         ) + ( | ||||||
|  |             ')>\n' | ||||||
|         ) |         ) | ||||||
|  |         return repr_str | ||||||
| 
 | 
 | ||||||
|     # NOTE: making this return a value that can be passed to |     # NOTE: making this return a value that can be passed to | ||||||
|     # `eval()` is entirely **optional** FYI! |     # `eval()` is entirely **optional** FYI! | ||||||
|  | @ -247,6 +289,10 @@ class Channel: | ||||||
|     def raddr(self) -> Address|None: |     def raddr(self) -> Address|None: | ||||||
|         return self._transport.raddr if self._transport else None |         return self._transport.raddr if self._transport else None | ||||||
| 
 | 
 | ||||||
|  |     @property | ||||||
|  |     def maddr(self) -> str: | ||||||
|  |         return self._transport.maddr if self._transport else '<no-tpt>' | ||||||
|  | 
 | ||||||
|     # TODO: something like, |     # TODO: something like, | ||||||
|     # `pdbp.hideframe_on(errors=[MsgTypeError])` |     # `pdbp.hideframe_on(errors=[MsgTypeError])` | ||||||
|     # instead of the `try/except` hack we have rn.. |     # instead of the `try/except` hack we have rn.. | ||||||
|  | @ -257,7 +303,7 @@ class Channel: | ||||||
|         self, |         self, | ||||||
|         payload: Any, |         payload: Any, | ||||||
| 
 | 
 | ||||||
|         hide_tb: bool = True, |         hide_tb: bool = False, | ||||||
| 
 | 
 | ||||||
|     ) -> None: |     ) -> None: | ||||||
|         ''' |         ''' | ||||||
|  | @ -434,8 +480,8 @@ class Channel: | ||||||
|         await self.send(aid) |         await self.send(aid) | ||||||
|         peer_aid: Aid = await self.recv() |         peer_aid: Aid = await self.recv() | ||||||
|         log.runtime( |         log.runtime( | ||||||
|             f'Received hanshake with peer actor,\n' |             f'Received hanshake with peer\n' | ||||||
|             f'{peer_aid}\n' |             f'<= {peer_aid.reprol(sin_uuid=False)}\n' | ||||||
|         ) |         ) | ||||||
|         # NOTE, we always are referencing the remote peer! |         # NOTE, we always are referencing the remote peer! | ||||||
|         self.aid = peer_aid |         self.aid = peer_aid | ||||||
|  |  | ||||||
|  | @ -17,29 +17,59 @@ | ||||||
| Utils to tame mp non-SC madeness | Utils to tame mp non-SC madeness | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
|  | import platform | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| def disable_mantracker(): | def disable_mantracker(): | ||||||
|     ''' |     ''' | ||||||
|     Disable all ``multiprocessing``` "resource tracking" machinery since |     Disable all `multiprocessing` "resource tracking" machinery since | ||||||
|     it's an absolute multi-threaded mess of non-SC madness. |     it's an absolute multi-threaded mess of non-SC madness. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     from multiprocessing import resource_tracker as mantracker |     from multiprocessing.shared_memory import SharedMemory | ||||||
| 
 | 
 | ||||||
|     # Tell the "resource tracker" thing to fuck off. |  | ||||||
|     class ManTracker(mantracker.ResourceTracker): |  | ||||||
|         def register(self, name, rtype): |  | ||||||
|             pass |  | ||||||
| 
 | 
 | ||||||
|         def unregister(self, name, rtype): |     # 3.13+ only.. can pass `track=False` to disable | ||||||
|             pass |     # all the resource tracker bs. | ||||||
|  |     # https://docs.python.org/3/library/multiprocessing.shared_memory.html | ||||||
|  |     if (_py_313 := ( | ||||||
|  |             platform.python_version_tuple()[:-1] | ||||||
|  |             >= | ||||||
|  |             ('3', '13') | ||||||
|  |         ) | ||||||
|  |     ): | ||||||
|  |         from functools import partial | ||||||
|  |         return partial( | ||||||
|  |             SharedMemory, | ||||||
|  |             track=False, | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|         def ensure_running(self): |     # !TODO, once we drop 3.12- we can obvi remove all this! | ||||||
|             pass |     else: | ||||||
|  |         from multiprocessing import ( | ||||||
|  |             resource_tracker as mantracker, | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     # "know your land and know your prey" |         # Tell the "resource tracker" thing to fuck off. | ||||||
|     # https://www.dailymotion.com/video/x6ozzco |         class ManTracker(mantracker.ResourceTracker): | ||||||
|     mantracker._resource_tracker = ManTracker() |             def register(self, name, rtype): | ||||||
|     mantracker.register = mantracker._resource_tracker.register |                 pass | ||||||
|     mantracker.ensure_running = mantracker._resource_tracker.ensure_running | 
 | ||||||
|     mantracker.unregister = mantracker._resource_tracker.unregister |             def unregister(self, name, rtype): | ||||||
|     mantracker.getfd = mantracker._resource_tracker.getfd |                 pass | ||||||
|  | 
 | ||||||
|  |             def ensure_running(self): | ||||||
|  |                 pass | ||||||
|  | 
 | ||||||
|  |         # "know your land and know your prey" | ||||||
|  |         # https://www.dailymotion.com/video/x6ozzco | ||||||
|  |         mantracker._resource_tracker = ManTracker() | ||||||
|  |         mantracker.register = mantracker._resource_tracker.register | ||||||
|  |         mantracker.ensure_running = mantracker._resource_tracker.ensure_running | ||||||
|  |         mantracker.unregister = mantracker._resource_tracker.unregister | ||||||
|  |         mantracker.getfd = mantracker._resource_tracker.getfd | ||||||
|  | 
 | ||||||
|  |         # use std type verbatim | ||||||
|  |         shmT = SharedMemory | ||||||
|  | 
 | ||||||
|  |     return shmT | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -23,14 +23,15 @@ considered optional within the context of this runtime-library. | ||||||
| 
 | 
 | ||||||
| """ | """ | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  | from multiprocessing import shared_memory as shm | ||||||
|  | from multiprocessing.shared_memory import ( | ||||||
|  |     # SharedMemory, | ||||||
|  |     ShareableList, | ||||||
|  | ) | ||||||
|  | import platform | ||||||
| from sys import byteorder | from sys import byteorder | ||||||
| import time | import time | ||||||
| from typing import Optional | from typing import Optional | ||||||
| from multiprocessing import shared_memory as shm |  | ||||||
| from multiprocessing.shared_memory import ( |  | ||||||
|     SharedMemory, |  | ||||||
|     ShareableList, |  | ||||||
| ) |  | ||||||
| 
 | 
 | ||||||
| from msgspec import ( | from msgspec import ( | ||||||
|     Struct, |     Struct, | ||||||
|  | @ -61,7 +62,7 @@ except ImportError: | ||||||
| log = get_logger(__name__) | log = get_logger(__name__) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| disable_mantracker() | SharedMemory = disable_mantracker() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SharedInt: | class SharedInt: | ||||||
|  | @ -789,11 +790,23 @@ def open_shm_list( | ||||||
|         readonly=readonly, |         readonly=readonly, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  |     # TODO, factor into a @actor_fixture acm-API? | ||||||
|  |     # -[ ] also `@maybe_actor_fixture()` which inludes | ||||||
|  |     #     the .current_actor() convenience check? | ||||||
|  |     #   |_ orr can that just be in the sin-maybe-version? | ||||||
|  |     # | ||||||
|     # "close" attached shm on actor teardown |     # "close" attached shm on actor teardown | ||||||
|     try: |     try: | ||||||
|         actor = tractor.current_actor() |         actor = tractor.current_actor() | ||||||
|  | 
 | ||||||
|         actor.lifetime_stack.callback(shml.shm.close) |         actor.lifetime_stack.callback(shml.shm.close) | ||||||
|         actor.lifetime_stack.callback(shml.shm.unlink) | 
 | ||||||
|  |         # XXX on 3.13+ we don't need to call this? | ||||||
|  |         # -> bc we pass `track=False` for `SharedMemeory` orr? | ||||||
|  |         if ( | ||||||
|  |             platform.python_version_tuple()[:-1] < ('3', '13') | ||||||
|  |         ): | ||||||
|  |             actor.lifetime_stack.callback(shml.shm.unlink) | ||||||
|     except RuntimeError: |     except RuntimeError: | ||||||
|         log.warning('tractor runtime not active, skipping teardown steps') |         log.warning('tractor runtime not active, skipping teardown steps') | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -18,6 +18,7 @@ TCP implementation of tractor.ipc._transport.MsgTransport protocol | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  | import ipaddress | ||||||
| from typing import ( | from typing import ( | ||||||
|     ClassVar, |     ClassVar, | ||||||
| ) | ) | ||||||
|  | @ -50,13 +51,45 @@ class TCPAddress( | ||||||
|     _host: str |     _host: str | ||||||
|     _port: int |     _port: int | ||||||
| 
 | 
 | ||||||
|  |     def __post_init__(self): | ||||||
|  |         try: | ||||||
|  |             ipaddress.ip_address(self._host) | ||||||
|  |         except ValueError as valerr: | ||||||
|  |             raise ValueError( | ||||||
|  |                 'Invalid {type(self).__name__}._host = {self._host!r}\n' | ||||||
|  |             ) from valerr | ||||||
|  | 
 | ||||||
|     proto_key: ClassVar[str] = 'tcp' |     proto_key: ClassVar[str] = 'tcp' | ||||||
|     unwrapped_type: ClassVar[type] = tuple[str, int] |     unwrapped_type: ClassVar[type] = tuple[str, int] | ||||||
|     def_bindspace: ClassVar[str] = '127.0.0.1' |     def_bindspace: ClassVar[str] = '127.0.0.1' | ||||||
| 
 | 
 | ||||||
|  |     # ?TODO, actually validate ipv4/6 with stdlib's `ipaddress` | ||||||
|     @property |     @property | ||||||
|     def is_valid(self) -> bool: |     def is_valid(self) -> bool: | ||||||
|         return self._port != 0 |         ''' | ||||||
|  |         Predicate to ensure a valid socket-address pair. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         return ( | ||||||
|  |             self._port != 0 | ||||||
|  |             and | ||||||
|  |             (ipaddr := ipaddress.ip_address(self._host)) | ||||||
|  |             and not ( | ||||||
|  |                 ipaddr.is_reserved | ||||||
|  |                 or | ||||||
|  |                 ipaddr.is_unspecified | ||||||
|  |                 or | ||||||
|  |                 ipaddr.is_link_local | ||||||
|  |                 or | ||||||
|  |                 ipaddr.is_link_local | ||||||
|  |                 or | ||||||
|  |                 ipaddr.is_multicast | ||||||
|  |                 or | ||||||
|  |                 ipaddr.is_global | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         # ^XXX^ see various properties of invalid addrs here, | ||||||
|  |         # https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv4Address | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|     def bindspace(self) -> str: |     def bindspace(self) -> str: | ||||||
|  | @ -127,6 +160,10 @@ async def start_listener( | ||||||
|     Start a TCP socket listener on the given `TCPAddress`. |     Start a TCP socket listener on the given `TCPAddress`. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|  |     log.runtime( | ||||||
|  |         f'Trying socket bind\n' | ||||||
|  |         f'>[ {addr}\n' | ||||||
|  |     ) | ||||||
|     # ?TODO, maybe we should just change the lower-level call this is |     # ?TODO, maybe we should just change the lower-level call this is | ||||||
|     # using internall per-listener? |     # using internall per-listener? | ||||||
|     listeners: list[SocketListener] = await open_tcp_listeners( |     listeners: list[SocketListener] = await open_tcp_listeners( | ||||||
|  | @ -140,6 +177,11 @@ async def start_listener( | ||||||
|     assert len(listeners) == 1 |     assert len(listeners) == 1 | ||||||
|     listener = listeners[0] |     listener = listeners[0] | ||||||
|     host, port = listener.socket.getsockname()[:2] |     host, port = listener.socket.getsockname()[:2] | ||||||
|  |     bound_addr: TCPAddress = type(addr).from_addr((host, port)) | ||||||
|  |     log.info( | ||||||
|  |         f'Listening on TCP socket\n' | ||||||
|  |         f'[> {bound_addr}\n' | ||||||
|  |     ) | ||||||
|     return listener |     return listener | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -430,20 +430,25 @@ class MsgpackTransport(MsgTransport): | ||||||
|                 return await self.stream.send_all(size + bytes_data) |                 return await self.stream.send_all(size + bytes_data) | ||||||
|             except ( |             except ( | ||||||
|                 trio.BrokenResourceError, |                 trio.BrokenResourceError, | ||||||
|             ) as bre: |                 trio.ClosedResourceError, | ||||||
|                 trans_err = bre |             ) as _re: | ||||||
|  |                 trans_err = _re | ||||||
|                 tpt_name: str = f'{type(self).__name__!r}' |                 tpt_name: str = f'{type(self).__name__!r}' | ||||||
|  | 
 | ||||||
|                 match trans_err: |                 match trans_err: | ||||||
|  | 
 | ||||||
|  |                     # XXX, specifc to UDS transport and its, | ||||||
|  |                     # well, "speediness".. XD | ||||||
|  |                     # |_ likely todo with races related to how fast | ||||||
|  |                     #    the socket is setup/torn-down on linux | ||||||
|  |                     #    as it pertains to rando pings from the | ||||||
|  |                     #    `.discovery` subsys and protos. | ||||||
|                     case trio.BrokenResourceError() if ( |                     case trio.BrokenResourceError() if ( | ||||||
|                         '[Errno 32] Broken pipe' in trans_err.args[0] |                         '[Errno 32] Broken pipe' | ||||||
|                         # ^XXX, specifc to UDS transport and its, |                         in | ||||||
|                         # well, "speediness".. XD |                         trans_err.args[0] | ||||||
|                         # |_ likely todo with races related to how fast |  | ||||||
|                         #    the socket is setup/torn-down on linux |  | ||||||
|                         #    as it pertains to rando pings from the |  | ||||||
|                         #    `.discovery` subsys and protos. |  | ||||||
|                     ): |                     ): | ||||||
|                         raise TransportClosed.from_src_exc( |                         tpt_closed = TransportClosed.from_src_exc( | ||||||
|                             message=( |                             message=( | ||||||
|                                 f'{tpt_name} already closed by peer\n' |                                 f'{tpt_name} already closed by peer\n' | ||||||
|                             ), |                             ), | ||||||
|  | @ -451,14 +456,31 @@ class MsgpackTransport(MsgTransport): | ||||||
|                             src_exc=trans_err, |                             src_exc=trans_err, | ||||||
|                             raise_on_report=True, |                             raise_on_report=True, | ||||||
|                             loglevel='transport', |                             loglevel='transport', | ||||||
|                         ) from bre |                         ) | ||||||
|  |                         raise tpt_closed from trans_err | ||||||
|  | 
 | ||||||
|  |                     # case trio.ClosedResourceError() if ( | ||||||
|  |                     #     'this socket was already closed' | ||||||
|  |                     #     in | ||||||
|  |                     #     trans_err.args[0] | ||||||
|  |                     # ): | ||||||
|  |                     #     tpt_closed = TransportClosed.from_src_exc( | ||||||
|  |                     #         message=( | ||||||
|  |                     #             f'{tpt_name} already closed by peer\n' | ||||||
|  |                     #         ), | ||||||
|  |                     #         body=f'{self}\n', | ||||||
|  |                     #         src_exc=trans_err, | ||||||
|  |                     #         raise_on_report=True, | ||||||
|  |                     #         loglevel='transport', | ||||||
|  |                     #     ) | ||||||
|  |                     #     raise tpt_closed from trans_err | ||||||
| 
 | 
 | ||||||
|                     # unless the disconnect condition falls under "a |                     # unless the disconnect condition falls under "a | ||||||
|                     # normal operation breakage" we usualy console warn |                     # normal operation breakage" we usualy console warn | ||||||
|                     # about it. |                     # about it. | ||||||
|                     case _: |                     case _: | ||||||
|                         log.exception( |                         log.exception( | ||||||
|                             '{tpt_name} layer failed pre-send ??\n' |                             f'{tpt_name} layer failed pre-send ??\n' | ||||||
|                         ) |                         ) | ||||||
|                         raise trans_err |                         raise trans_err | ||||||
| 
 | 
 | ||||||
|  | @ -503,7 +525,7 @@ class MsgpackTransport(MsgTransport): | ||||||
|     def pformat(self) -> str: |     def pformat(self) -> str: | ||||||
|         return ( |         return ( | ||||||
|             f'<{type(self).__name__}(\n' |             f'<{type(self).__name__}(\n' | ||||||
|             f' |_peers: 2\n' |             f' |_peers: 1\n' | ||||||
|             f'   laddr: {self._laddr}\n' |             f'   laddr: {self._laddr}\n' | ||||||
|             f'   raddr: {self._raddr}\n' |             f'   raddr: {self._raddr}\n' | ||||||
|             # f'\n' |             # f'\n' | ||||||
|  |  | ||||||
|  | @ -18,6 +18,9 @@ Unix Domain Socket implementation of tractor.ipc._transport.MsgTransport protoco | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
|  | from contextlib import ( | ||||||
|  |     contextmanager as cm, | ||||||
|  | ) | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import os | import os | ||||||
| from socket import ( | from socket import ( | ||||||
|  | @ -29,6 +32,7 @@ from socket import ( | ||||||
| ) | ) | ||||||
| import struct | import struct | ||||||
| from typing import ( | from typing import ( | ||||||
|  |     Type, | ||||||
|     TYPE_CHECKING, |     TYPE_CHECKING, | ||||||
|     ClassVar, |     ClassVar, | ||||||
| ) | ) | ||||||
|  | @ -99,8 +103,6 @@ class UDSAddress( | ||||||
|             self.filedir |             self.filedir | ||||||
|             or |             or | ||||||
|             self.def_bindspace |             self.def_bindspace | ||||||
|             # or |  | ||||||
|             # get_rt_dir() |  | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     @property |     @property | ||||||
|  | @ -205,12 +207,35 @@ class UDSAddress( | ||||||
|             f']' |             f']' | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  | @cm | ||||||
|  | def _reraise_as_connerr( | ||||||
|  |     src_excs: tuple[Type[Exception]], | ||||||
|  |     addr: UDSAddress, | ||||||
|  | ): | ||||||
|  |     try: | ||||||
|  |         yield | ||||||
|  |     except src_excs as src_exc: | ||||||
|  |         raise ConnectionError( | ||||||
|  |             f'Bad UDS socket-filepath-as-address ??\n' | ||||||
|  |             f'{addr}\n' | ||||||
|  |             f' |_sockpath: {addr.sockpath}\n' | ||||||
|  |             f'\n' | ||||||
|  |             f'from src: {src_exc!r}\n' | ||||||
|  |         ) from src_exc | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| async def start_listener( | async def start_listener( | ||||||
|     addr: UDSAddress, |     addr: UDSAddress, | ||||||
|     **kwargs, |     **kwargs, | ||||||
| ) -> SocketListener: | ) -> SocketListener: | ||||||
|     # sock = addr._sock = socket.socket( |     ''' | ||||||
|  |     Start listening for inbound connections via | ||||||
|  |     a `trio.SocketListener` (task) which `socket.bind()`s on `addr`. | ||||||
|  | 
 | ||||||
|  |     Note, if the `UDSAddress.bindspace: Path` directory dne it is | ||||||
|  |     implicitly created. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|     sock = socket.socket( |     sock = socket.socket( | ||||||
|         socket.AF_UNIX, |         socket.AF_UNIX, | ||||||
|         socket.SOCK_STREAM |         socket.SOCK_STREAM | ||||||
|  | @ -221,17 +246,25 @@ async def start_listener( | ||||||
|         f'|_{addr}\n' |         f'|_{addr}\n' | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  |     # ?TODO? should we use the `actor.lifetime_stack` | ||||||
|  |     # to rm on shutdown? | ||||||
|     bindpath: Path = addr.sockpath |     bindpath: Path = addr.sockpath | ||||||
|     try: |     if not (bs := addr.bindspace).is_dir(): | ||||||
|  |         log.info( | ||||||
|  |             'Creating bindspace dir in file-sys\n' | ||||||
|  |             f'>{{\n' | ||||||
|  |             f'|_{bs!r}\n' | ||||||
|  |         ) | ||||||
|  |         bs.mkdir() | ||||||
|  | 
 | ||||||
|  |     with _reraise_as_connerr( | ||||||
|  |         src_excs=( | ||||||
|  |             FileNotFoundError, | ||||||
|  |             OSError, | ||||||
|  |         ), | ||||||
|  |         addr=addr | ||||||
|  |     ): | ||||||
|         await sock.bind(str(bindpath)) |         await sock.bind(str(bindpath)) | ||||||
|     except ( |  | ||||||
|         FileNotFoundError, |  | ||||||
|     ) as fdne: |  | ||||||
|         raise ConnectionError( |  | ||||||
|             f'Bad UDS socket-filepath-as-address ??\n' |  | ||||||
|             f'{addr}\n' |  | ||||||
|             f' |_sockpath: {addr.sockpath}\n' |  | ||||||
|         ) from fdne |  | ||||||
| 
 | 
 | ||||||
|     sock.listen(1) |     sock.listen(1) | ||||||
|     log.info( |     log.info( | ||||||
|  | @ -356,27 +389,30 @@ class MsgpackUDSStream(MsgpackTransport): | ||||||
|         # `.setsockopt()` call tells the OS provide it; the client |         # `.setsockopt()` call tells the OS provide it; the client | ||||||
|         # pid can then be read on server/listen() side via |         # pid can then be read on server/listen() side via | ||||||
|         # `get_peer_info()` above. |         # `get_peer_info()` above. | ||||||
|         try: | 
 | ||||||
|  |         with _reraise_as_connerr( | ||||||
|  |             src_excs=( | ||||||
|  |                 FileNotFoundError, | ||||||
|  |             ), | ||||||
|  |             addr=addr | ||||||
|  |         ): | ||||||
|             stream = await open_unix_socket_w_passcred( |             stream = await open_unix_socket_w_passcred( | ||||||
|                 str(sockpath), |                 str(sockpath), | ||||||
|                 **kwargs |                 **kwargs | ||||||
|             ) |             ) | ||||||
|         except ( |  | ||||||
|             FileNotFoundError, |  | ||||||
|         ) as fdne: |  | ||||||
|             raise ConnectionError( |  | ||||||
|                 f'Bad UDS socket-filepath-as-address ??\n' |  | ||||||
|                 f'{addr}\n' |  | ||||||
|                 f' |_sockpath: {sockpath}\n' |  | ||||||
|             ) from fdne |  | ||||||
| 
 | 
 | ||||||
|         stream = MsgpackUDSStream( |         tpt_stream = MsgpackUDSStream( | ||||||
|             stream, |             stream, | ||||||
|             prefix_size=prefix_size, |             prefix_size=prefix_size, | ||||||
|             codec=codec |             codec=codec | ||||||
|         ) |         ) | ||||||
|         stream._raddr = addr |         # XXX assign from new addrs after peer-PID extract! | ||||||
|         return stream |         ( | ||||||
|  |             tpt_stream._laddr, | ||||||
|  |             tpt_stream._raddr, | ||||||
|  |         ) = cls.get_stream_addrs(stream) | ||||||
|  | 
 | ||||||
|  |         return tpt_stream | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get_stream_addrs( |     def get_stream_addrs( | ||||||
|  |  | ||||||
|  | @ -81,10 +81,35 @@ BOLD_PALETTE = { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def at_least_level( | ||||||
|  |     log: Logger|LoggerAdapter, | ||||||
|  |     level: int|str, | ||||||
|  | ) -> bool: | ||||||
|  |     ''' | ||||||
|  |     Predicate to test if a given level is active. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     if isinstance(level, str): | ||||||
|  |         level: int = CUSTOM_LEVELS[level.upper()] | ||||||
|  | 
 | ||||||
|  |     if log.getEffectiveLevel() <= level: | ||||||
|  |         return True | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| # TODO: this isn't showing the correct '{filename}' | # TODO: this isn't showing the correct '{filename}' | ||||||
| # as it did before.. | # as it did before.. | ||||||
| class StackLevelAdapter(LoggerAdapter): | class StackLevelAdapter(LoggerAdapter): | ||||||
| 
 | 
 | ||||||
|  |     def at_least_level( | ||||||
|  |         self, | ||||||
|  |         level: str, | ||||||
|  |     ) -> bool: | ||||||
|  |         return at_least_level( | ||||||
|  |             log=self, | ||||||
|  |             level=level, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|     def transport( |     def transport( | ||||||
|         self, |         self, | ||||||
|         msg: str, |         msg: str, | ||||||
|  | @ -401,19 +426,3 @@ def get_loglevel() -> str: | ||||||
| 
 | 
 | ||||||
| # global module logger for tractor itself | # global module logger for tractor itself | ||||||
| log: StackLevelAdapter = get_logger('tractor') | log: StackLevelAdapter = get_logger('tractor') | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def at_least_level( |  | ||||||
|     log: Logger|LoggerAdapter, |  | ||||||
|     level: int|str, |  | ||||||
| ) -> bool: |  | ||||||
|     ''' |  | ||||||
|     Predicate to test if a given level is active. |  | ||||||
| 
 |  | ||||||
|     ''' |  | ||||||
|     if isinstance(level, str): |  | ||||||
|         level: int = CUSTOM_LEVELS[level.upper()] |  | ||||||
| 
 |  | ||||||
|     if log.getEffectiveLevel() <= level: |  | ||||||
|         return True |  | ||||||
|     return False |  | ||||||
|  |  | ||||||
|  | @ -210,12 +210,14 @@ class PldRx(Struct): | ||||||
|         match msg: |         match msg: | ||||||
|             case Return()|Error(): |             case Return()|Error(): | ||||||
|                 log.runtime( |                 log.runtime( | ||||||
|                     f'Rxed final outcome msg\n' |                     f'Rxed final-outcome msg\n' | ||||||
|  |                     f'\n' | ||||||
|                     f'{msg}\n' |                     f'{msg}\n' | ||||||
|                 ) |                 ) | ||||||
|             case Stop(): |             case Stop(): | ||||||
|                 log.runtime( |                 log.runtime( | ||||||
|                     f'Rxed stream stopped msg\n' |                     f'Rxed stream stopped msg\n' | ||||||
|  |                     f'\n' | ||||||
|                     f'{msg}\n' |                     f'{msg}\n' | ||||||
|                 ) |                 ) | ||||||
|                 if passthrough_non_pld_msgs: |                 if passthrough_non_pld_msgs: | ||||||
|  | @ -261,8 +263,9 @@ class PldRx(Struct): | ||||||
|         if ( |         if ( | ||||||
|             type(msg) is Return |             type(msg) is Return | ||||||
|         ): |         ): | ||||||
|             log.info( |             log.runtime( | ||||||
|                 f'Rxed final result msg\n' |                 f'Rxed final result msg\n' | ||||||
|  |                 f'\n' | ||||||
|                 f'{msg}\n' |                 f'{msg}\n' | ||||||
|             ) |             ) | ||||||
|         return self.decode_pld( |         return self.decode_pld( | ||||||
|  | @ -304,10 +307,13 @@ class PldRx(Struct): | ||||||
|                 try: |                 try: | ||||||
|                     pld: PayloadT = self._pld_dec.decode(pld) |                     pld: PayloadT = self._pld_dec.decode(pld) | ||||||
|                     log.runtime( |                     log.runtime( | ||||||
|                         'Decoded msg payload\n\n' |                         f'Decoded payload for\n' | ||||||
|  |                         # f'\n' | ||||||
|                         f'{msg}\n' |                         f'{msg}\n' | ||||||
|                         f'where payload decoded as\n' |                         # ^TODO?, ideally just render with `, | ||||||
|                         f'|_pld={pld!r}\n' |                         # pld={decode}` in the `msg.pformat()`?? | ||||||
|  |                         f'where, ' | ||||||
|  |                         f'{type(msg).__name__}.pld={pld!r}\n' | ||||||
|                     ) |                     ) | ||||||
|                     return pld |                     return pld | ||||||
|                 except TypeError as typerr: |                 except TypeError as typerr: | ||||||
|  | @ -494,7 +500,8 @@ def limit_plds( | ||||||
| 
 | 
 | ||||||
|     finally: |     finally: | ||||||
|         log.runtime( |         log.runtime( | ||||||
|             'Reverted to previous payload-decoder\n\n' |             f'Reverted to previous payload-decoder\n' | ||||||
|  |             f'\n' | ||||||
|             f'{orig_pldec}\n' |             f'{orig_pldec}\n' | ||||||
|         ) |         ) | ||||||
|         # sanity on orig settings |         # sanity on orig settings | ||||||
|  | @ -608,7 +615,7 @@ async def drain_to_final_msg( | ||||||
|             # |             # | ||||||
|             # -[ ] make sure pause points work here for REPLing |             # -[ ] make sure pause points work here for REPLing | ||||||
|             #   the runtime itself; i.e. ensure there's no hangs! |             #   the runtime itself; i.e. ensure there's no hangs! | ||||||
|             # |_from tractor.devx._debug import pause |             # |_from tractor.devx.debug import pause | ||||||
|             #   await pause() |             #   await pause() | ||||||
| 
 | 
 | ||||||
|         # NOTE: we get here if the far end was |         # NOTE: we get here if the far end was | ||||||
|  | @ -629,7 +636,8 @@ async def drain_to_final_msg( | ||||||
|                     (local_cs := rent_n.cancel_scope).cancel_called |                     (local_cs := rent_n.cancel_scope).cancel_called | ||||||
|                 ): |                 ): | ||||||
|                     log.cancel( |                     log.cancel( | ||||||
|                         'RPC-ctx cancelled by local-parent scope during drain!\n\n' |                         f'RPC-ctx cancelled by local-parent scope during drain!\n' | ||||||
|  |                         f'\n' | ||||||
|                         f'c}}>\n' |                         f'c}}>\n' | ||||||
|                         f' |_{rent_n}\n' |                         f' |_{rent_n}\n' | ||||||
|                         f'   |_.cancel_scope = {local_cs}\n' |                         f'   |_.cancel_scope = {local_cs}\n' | ||||||
|  | @ -663,7 +671,8 @@ async def drain_to_final_msg( | ||||||
|             # final result arrived! |             # final result arrived! | ||||||
|             case Return(): |             case Return(): | ||||||
|                 log.runtime( |                 log.runtime( | ||||||
|                     'Context delivered final draining msg:\n' |                     f'Context delivered final draining msg\n' | ||||||
|  |                     f'\n' | ||||||
|                     f'{pretty_struct.pformat(msg)}' |                     f'{pretty_struct.pformat(msg)}' | ||||||
|                 ) |                 ) | ||||||
|                 ctx._result: Any = pld |                 ctx._result: Any = pld | ||||||
|  | @ -697,12 +706,14 @@ async def drain_to_final_msg( | ||||||
|                 ): |                 ): | ||||||
|                     log.cancel( |                     log.cancel( | ||||||
|                         'Cancelling `MsgStream` drain since ' |                         'Cancelling `MsgStream` drain since ' | ||||||
|                         f'{reason}\n\n' |                         f'{reason}\n' | ||||||
|  |                         f'\n' | ||||||
|                         f'<= {ctx.chan.uid}\n' |                         f'<= {ctx.chan.uid}\n' | ||||||
|                         f'  |_{ctx._nsf}()\n\n' |                         f'  |_{ctx._nsf}()\n' | ||||||
|  |                         f'\n' | ||||||
|                         f'=> {ctx._task}\n' |                         f'=> {ctx._task}\n' | ||||||
|                         f'  |_{ctx._stream}\n\n' |                         f'  |_{ctx._stream}\n' | ||||||
| 
 |                         f'\n' | ||||||
|                         f'{pretty_struct.pformat(msg)}\n' |                         f'{pretty_struct.pformat(msg)}\n' | ||||||
|                     ) |                     ) | ||||||
|                     break |                     break | ||||||
|  | @ -739,7 +750,8 @@ async def drain_to_final_msg( | ||||||
|             case Stop(): |             case Stop(): | ||||||
|                 pre_result_drained.append(msg) |                 pre_result_drained.append(msg) | ||||||
|                 log.runtime(  # normal/expected shutdown transaction |                 log.runtime(  # normal/expected shutdown transaction | ||||||
|                     'Remote stream terminated due to "stop" msg:\n\n' |                     f'Remote stream terminated due to "stop" msg\n' | ||||||
|  |                     f'\n' | ||||||
|                     f'{pretty_struct.pformat(msg)}\n' |                     f'{pretty_struct.pformat(msg)}\n' | ||||||
|                 ) |                 ) | ||||||
|                 continue |                 continue | ||||||
|  | @ -814,7 +826,8 @@ async def drain_to_final_msg( | ||||||
| 
 | 
 | ||||||
|     else: |     else: | ||||||
|         log.cancel( |         log.cancel( | ||||||
|             'Skipping `MsgStream` drain since final outcome is set\n\n' |             f'Skipping `MsgStream` drain since final outcome is set\n' | ||||||
|  |             f'\n' | ||||||
|             f'{ctx.outcome}\n' |             f'{ctx.outcome}\n' | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -20,6 +20,7 @@ Prettified version of `msgspec.Struct` for easier console grokin. | ||||||
| ''' | ''' | ||||||
| from __future__ import annotations | from __future__ import annotations | ||||||
| from collections import UserList | from collections import UserList | ||||||
|  | import textwrap | ||||||
| from typing import ( | from typing import ( | ||||||
|     Any, |     Any, | ||||||
|     Iterator, |     Iterator, | ||||||
|  | @ -105,27 +106,11 @@ def iter_fields(struct: Struct) -> Iterator[ | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def pformat( | def iter_struct_ppfmt_lines( | ||||||
|     struct: Struct, |     struct: Struct, | ||||||
|     field_indent: int = 2, |     field_indent: int = 0, | ||||||
|     indent: int = 0, | ) -> Iterator[tuple[str, str]]: | ||||||
| 
 | 
 | ||||||
| ) -> str: |  | ||||||
|     ''' |  | ||||||
|     Recursion-safe `pprint.pformat()` style formatting of |  | ||||||
|     a `msgspec.Struct` for sane reading by a human using a REPL. |  | ||||||
| 
 |  | ||||||
|     ''' |  | ||||||
|     # global whitespace indent |  | ||||||
|     ws: str = ' '*indent |  | ||||||
| 
 |  | ||||||
|     # field whitespace indent |  | ||||||
|     field_ws: str = ' '*(field_indent + indent) |  | ||||||
| 
 |  | ||||||
|     # qtn: str = ws + struct.__class__.__qualname__ |  | ||||||
|     qtn: str = struct.__class__.__qualname__ |  | ||||||
| 
 |  | ||||||
|     obj_str: str = ''  # accumulator |  | ||||||
|     fi: structs.FieldInfo |     fi: structs.FieldInfo | ||||||
|     k: str |     k: str | ||||||
|     v: Any |     v: Any | ||||||
|  | @ -135,15 +120,18 @@ def pformat( | ||||||
|         # ..]` over .__name__ == `Literal` but still get only the |         # ..]` over .__name__ == `Literal` but still get only the | ||||||
|         # latter for simple types like `str | int | None` etc..? |         # latter for simple types like `str | int | None` etc..? | ||||||
|         ft: type = fi.type |         ft: type = fi.type | ||||||
|         typ_name: str = getattr(ft, '__name__', str(ft)) |         typ_name: str = getattr( | ||||||
|  |             ft, | ||||||
|  |             '__name__', | ||||||
|  |             str(ft) | ||||||
|  |         ).replace(' ', '') | ||||||
| 
 | 
 | ||||||
|         # recurse to get sub-struct's `.pformat()` output Bo |         # recurse to get sub-struct's `.pformat()` output Bo | ||||||
|         if isinstance(v, Struct): |         if isinstance(v, Struct): | ||||||
|             val_str: str =  v.pformat( |             yield from iter_struct_ppfmt_lines( | ||||||
|                 indent=field_indent + indent, |                 struct=v, | ||||||
|                 field_indent=indent + field_indent, |                 field_indent=field_indent+field_indent, | ||||||
|             ) |             ) | ||||||
| 
 |  | ||||||
|         else: |         else: | ||||||
|             val_str: str = repr(v) |             val_str: str = repr(v) | ||||||
| 
 | 
 | ||||||
|  | @ -161,8 +149,39 @@ def pformat( | ||||||
|                 # raise |                 # raise | ||||||
|                 # return _Struct.__repr__(struct) |                 # return _Struct.__repr__(struct) | ||||||
| 
 | 
 | ||||||
|         # TODO: LOLOL use `textwrap.indent()` instead dawwwwwg! |         yield ( | ||||||
|         obj_str += (field_ws + f'{k}: {typ_name} = {val_str},\n') |             ' '*field_indent,  # indented ws prefix | ||||||
|  |             f'{k}: {typ_name} = {val_str},',  # field's repr line content | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def pformat( | ||||||
|  |     struct: Struct, | ||||||
|  |     field_indent: int = 2, | ||||||
|  |     indent: int = 0, | ||||||
|  | ) -> str: | ||||||
|  |     ''' | ||||||
|  |     Recursion-safe `pprint.pformat()` style formatting of | ||||||
|  |     a `msgspec.Struct` for sane reading by a human using a REPL. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     obj_str: str = ''  # accumulator | ||||||
|  |     for prefix, field_repr, in iter_struct_ppfmt_lines( | ||||||
|  |         struct, | ||||||
|  |         field_indent=field_indent, | ||||||
|  |     ): | ||||||
|  |         obj_str += f'{prefix}{field_repr}\n' | ||||||
|  | 
 | ||||||
|  |     # global whitespace indent | ||||||
|  |     ws: str = ' '*indent | ||||||
|  |     if indent: | ||||||
|  |         obj_str: str = textwrap.indent( | ||||||
|  |             text=obj_str, | ||||||
|  |             prefix=ws, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     # qtn: str = ws + struct.__class__.__qualname__ | ||||||
|  |     qtn: str = struct.__class__.__qualname__ | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|         f'{qtn}(\n' |         f'{qtn}(\n' | ||||||
|  |  | ||||||
|  | @ -154,6 +154,39 @@ class Aid( | ||||||
|     #     should also include at least `.pid` (equiv to port for tcp) |     #     should also include at least `.pid` (equiv to port for tcp) | ||||||
|     #     and/or host-part always? |     #     and/or host-part always? | ||||||
| 
 | 
 | ||||||
|  |     @property | ||||||
|  |     def uid(self) -> tuple[str, str]: | ||||||
|  |         ''' | ||||||
|  |         Legacy actor "unique-id" pair format. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|  |         return ( | ||||||
|  |             self.name, | ||||||
|  |             self.uuid, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def reprol( | ||||||
|  |         self, | ||||||
|  |         sin_uuid: bool = True, | ||||||
|  |     ) -> str: | ||||||
|  |         if not sin_uuid: | ||||||
|  |             return ( | ||||||
|  |                 f'{self.name}[{self.uuid[:6]}]@{self.pid!r}' | ||||||
|  |             ) | ||||||
|  |         return ( | ||||||
|  |             f'{self.name}@{self.pid!r}' | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     # mk hashable via `.uuid` | ||||||
|  |     def __hash__(self) -> int: | ||||||
|  |         return hash(self.uuid) | ||||||
|  | 
 | ||||||
|  |     def __eq__(self, other: Aid) -> bool: | ||||||
|  |         return self.uuid == other.uuid | ||||||
|  | 
 | ||||||
|  |     # use pretty fmt since often repr-ed for console/log | ||||||
|  |     __repr__ = pretty_struct.Struct.__repr__ | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| class SpawnSpec( | class SpawnSpec( | ||||||
|     pretty_struct.Struct, |     pretty_struct.Struct, | ||||||
|  | @ -170,6 +203,7 @@ class SpawnSpec( | ||||||
|     # a hard `Struct` def for all of these fields! |     # a hard `Struct` def for all of these fields! | ||||||
|     _parent_main_data: dict |     _parent_main_data: dict | ||||||
|     _runtime_vars: dict[str, Any] |     _runtime_vars: dict[str, Any] | ||||||
|  |     # ^NOTE see `._state._runtime_vars: dict` | ||||||
| 
 | 
 | ||||||
|     # module import capability |     # module import capability | ||||||
|     enable_modules: dict[str, str] |     enable_modules: dict[str, str] | ||||||
|  |  | ||||||
|  | @ -38,7 +38,6 @@ from typing import ( | ||||||
| import tractor | import tractor | ||||||
| from tractor._exceptions import ( | from tractor._exceptions import ( | ||||||
|     InternalError, |     InternalError, | ||||||
|     is_multi_cancelled, |  | ||||||
|     TrioTaskExited, |     TrioTaskExited, | ||||||
|     TrioCancelled, |     TrioCancelled, | ||||||
|     AsyncioTaskExited, |     AsyncioTaskExited, | ||||||
|  | @ -49,7 +48,7 @@ from tractor._state import ( | ||||||
|     _runtime_vars, |     _runtime_vars, | ||||||
| ) | ) | ||||||
| from tractor._context import Unresolved | from tractor._context import Unresolved | ||||||
| from tractor.devx import _debug | from tractor.devx import debug | ||||||
| from tractor.log import ( | from tractor.log import ( | ||||||
|     get_logger, |     get_logger, | ||||||
|     StackLevelAdapter, |     StackLevelAdapter, | ||||||
|  | @ -59,6 +58,9 @@ from tractor.log import ( | ||||||
| # from tractor.msg import ( | # from tractor.msg import ( | ||||||
| #     pretty_struct, | #     pretty_struct, | ||||||
| # ) | # ) | ||||||
|  | from tractor.trionics import ( | ||||||
|  |     is_multi_cancelled, | ||||||
|  | ) | ||||||
| from tractor.trionics._broadcast import ( | from tractor.trionics._broadcast import ( | ||||||
|     broadcast_receiver, |     broadcast_receiver, | ||||||
|     BroadcastReceiver, |     BroadcastReceiver, | ||||||
|  | @ -128,6 +130,7 @@ class LinkedTaskChannel( | ||||||
|     _trio_task: trio.Task |     _trio_task: trio.Task | ||||||
|     _aio_task_complete: trio.Event |     _aio_task_complete: trio.Event | ||||||
| 
 | 
 | ||||||
|  |     _closed_by_aio_task: bool = False | ||||||
|     _suppress_graceful_exits: bool = True |     _suppress_graceful_exits: bool = True | ||||||
| 
 | 
 | ||||||
|     _trio_err: BaseException|None = None |     _trio_err: BaseException|None = None | ||||||
|  | @ -206,10 +209,15 @@ class LinkedTaskChannel( | ||||||
|     async def aclose(self) -> None: |     async def aclose(self) -> None: | ||||||
|         await self._from_aio.aclose() |         await self._from_aio.aclose() | ||||||
| 
 | 
 | ||||||
|     def started( |     # ?TODO? async version of this? | ||||||
|  |     def started_nowait( | ||||||
|         self, |         self, | ||||||
|         val: Any = None, |         val: Any = None, | ||||||
|     ) -> None: |     ) -> None: | ||||||
|  |         ''' | ||||||
|  |         Synchronize aio-side with its trio-parent. | ||||||
|  | 
 | ||||||
|  |         ''' | ||||||
|         self._aio_started_val = val |         self._aio_started_val = val | ||||||
|         return self._to_trio.send_nowait(val) |         return self._to_trio.send_nowait(val) | ||||||
| 
 | 
 | ||||||
|  | @ -240,6 +248,7 @@ class LinkedTaskChannel( | ||||||
|             # cycle on the trio side? |             # cycle on the trio side? | ||||||
|             # await trio.lowlevel.checkpoint() |             # await trio.lowlevel.checkpoint() | ||||||
|             return await self._from_aio.receive() |             return await self._from_aio.receive() | ||||||
|  | 
 | ||||||
|         except BaseException as err: |         except BaseException as err: | ||||||
|             async with translate_aio_errors( |             async with translate_aio_errors( | ||||||
|                 chan=self, |                 chan=self, | ||||||
|  | @ -317,7 +326,7 @@ def _run_asyncio_task( | ||||||
|     qsize: int = 1, |     qsize: int = 1, | ||||||
|     provide_channels: bool = False, |     provide_channels: bool = False, | ||||||
|     suppress_graceful_exits: bool = True, |     suppress_graceful_exits: bool = True, | ||||||
|     hide_tb: bool = False, |     hide_tb: bool = True, | ||||||
|     **kwargs, |     **kwargs, | ||||||
| 
 | 
 | ||||||
| ) -> LinkedTaskChannel: | ) -> LinkedTaskChannel: | ||||||
|  | @ -345,18 +354,6 @@ def _run_asyncio_task( | ||||||
|         # value otherwise it would just return ;P |         # value otherwise it would just return ;P | ||||||
|         assert qsize > 1 |         assert qsize > 1 | ||||||
| 
 | 
 | ||||||
|     if provide_channels: |  | ||||||
|         assert 'to_trio' in args |  | ||||||
| 
 |  | ||||||
|     # allow target func to accept/stream results manually by name |  | ||||||
|     if 'to_trio' in args: |  | ||||||
|         kwargs['to_trio'] = to_trio |  | ||||||
| 
 |  | ||||||
|     if 'from_trio' in args: |  | ||||||
|         kwargs['from_trio'] = from_trio |  | ||||||
| 
 |  | ||||||
|     coro = func(**kwargs) |  | ||||||
| 
 |  | ||||||
|     trio_task: trio.Task = trio.lowlevel.current_task() |     trio_task: trio.Task = trio.lowlevel.current_task() | ||||||
|     trio_cs = trio.CancelScope() |     trio_cs = trio.CancelScope() | ||||||
|     aio_task_complete = trio.Event() |     aio_task_complete = trio.Event() | ||||||
|  | @ -371,6 +368,25 @@ def _run_asyncio_task( | ||||||
|         _suppress_graceful_exits=suppress_graceful_exits, |         _suppress_graceful_exits=suppress_graceful_exits, | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|  |     # allow target func to accept/stream results manually by name | ||||||
|  |     if 'to_trio' in args: | ||||||
|  |         kwargs['to_trio'] = to_trio | ||||||
|  | 
 | ||||||
|  |     if 'from_trio' in args: | ||||||
|  |         kwargs['from_trio'] = from_trio | ||||||
|  | 
 | ||||||
|  |     if 'chan' in args: | ||||||
|  |         kwargs['chan'] = chan | ||||||
|  | 
 | ||||||
|  |     if provide_channels: | ||||||
|  |         assert ( | ||||||
|  |             'to_trio' in args | ||||||
|  |             or | ||||||
|  |             'chan' in args | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     coro = func(**kwargs) | ||||||
|  | 
 | ||||||
|     async def wait_on_coro_final_result( |     async def wait_on_coro_final_result( | ||||||
|         to_trio: trio.MemorySendChannel, |         to_trio: trio.MemorySendChannel, | ||||||
|         coro: Awaitable, |         coro: Awaitable, | ||||||
|  | @ -443,9 +459,23 @@ def _run_asyncio_task( | ||||||
|                         f'Task exited with final result: {result!r}\n' |                         f'Task exited with final result: {result!r}\n' | ||||||
|                     ) |                     ) | ||||||
| 
 | 
 | ||||||
|                 # only close the sender side which will relay |                 # XXX ALWAYS close the child-`asyncio`-task-side's | ||||||
|                 # a `trio.EndOfChannel` to the trio (consumer) side. |                 # `to_trio` handle which will in turn relay | ||||||
|  |                 # a `trio.EndOfChannel` to the `trio`-parent. | ||||||
|  |                 # Consequently the parent `trio` task MUST ALWAYS | ||||||
|  |                 # check for any `chan._aio_err` to be raised when it | ||||||
|  |                 # receives an EoC. | ||||||
|  |                 # | ||||||
|  |                 # NOTE, there are 2 EoC cases, | ||||||
|  |                 # - normal/graceful EoC due to the aio-side actually | ||||||
|  |                 #   terminating its "streaming", but the task did not | ||||||
|  |                 #   error and is not yet complete. | ||||||
|  |                 # | ||||||
|  |                 # - the aio-task terminated and we specially mark the | ||||||
|  |                 #   closure as due to the `asyncio.Task`'s exit. | ||||||
|  |                 # | ||||||
|                 to_trio.close() |                 to_trio.close() | ||||||
|  |                 chan._closed_by_aio_task = True | ||||||
| 
 | 
 | ||||||
|             aio_task_complete.set() |             aio_task_complete.set() | ||||||
|             log.runtime( |             log.runtime( | ||||||
|  | @ -479,12 +509,12 @@ def _run_asyncio_task( | ||||||
|     if ( |     if ( | ||||||
|         debug_mode() |         debug_mode() | ||||||
|         and |         and | ||||||
|         (greenback := _debug.maybe_import_greenback( |         (greenback := debug.maybe_import_greenback( | ||||||
|             force_reload=True, |             force_reload=True, | ||||||
|             raise_not_found=False, |             raise_not_found=False, | ||||||
|         )) |         )) | ||||||
|     ): |     ): | ||||||
|         log.info( |         log.devx( | ||||||
|             f'Bestowing `greenback` portal for `asyncio`-task\n' |             f'Bestowing `greenback` portal for `asyncio`-task\n' | ||||||
|             f'{task}\n' |             f'{task}\n' | ||||||
|         ) |         ) | ||||||
|  | @ -643,8 +673,9 @@ def _run_asyncio_task( | ||||||
|                 not trio_cs.cancel_called |                 not trio_cs.cancel_called | ||||||
|             ): |             ): | ||||||
|                 log.cancel( |                 log.cancel( | ||||||
|                     f'Cancelling `trio` side due to aio-side src exc\n' |                     f'Cancelling trio-side due to aio-side src exc\n' | ||||||
|                     f'{curr_aio_err}\n' |                     f'\n' | ||||||
|  |                     f'{curr_aio_err!r}\n' | ||||||
|                     f'\n' |                     f'\n' | ||||||
|                     f'(c>\n' |                     f'(c>\n' | ||||||
|                     f'  |_{trio_task}\n' |                     f'  |_{trio_task}\n' | ||||||
|  | @ -756,6 +787,7 @@ async def translate_aio_errors( | ||||||
|     aio_done_before_trio: bool = aio_task.done() |     aio_done_before_trio: bool = aio_task.done() | ||||||
|     assert aio_task |     assert aio_task | ||||||
|     trio_err: BaseException|None = None |     trio_err: BaseException|None = None | ||||||
|  |     eoc: trio.EndOfChannel|None = None | ||||||
|     try: |     try: | ||||||
|         yield  # back to one of the cross-loop apis |         yield  # back to one of the cross-loop apis | ||||||
|     except trio.Cancelled as taskc: |     except trio.Cancelled as taskc: | ||||||
|  | @ -787,12 +819,48 @@ async def translate_aio_errors( | ||||||
|         # ) |         # ) | ||||||
|         # raise |         # raise | ||||||
| 
 | 
 | ||||||
|     # XXX always passthrough EoC since this translator is often |     # XXX EoC is a special SIGNAL from the aio-side here! | ||||||
|     # called from `LinkedTaskChannel.receive()` which we want |     # There are 2 cases to handle: | ||||||
|     # passthrough and further we have no special meaning for it in |     # 1. the "EoC passthrough" case. | ||||||
|     # terms of relaying errors or signals from the aio side! |     #   - the aio-task actually closed the channel "gracefully" and | ||||||
|     except trio.EndOfChannel as eoc: |     #     the trio-task should unwind any ongoing channel | ||||||
|         trio_err = chan._trio_err = eoc |     #     iteration/receiving, | ||||||
|  |     #  |_this exc-translator wraps calls to `LinkedTaskChannel.receive()` | ||||||
|  |     #    in which case we want to relay the actual "end-of-chan" for | ||||||
|  |     #    iteration purposes. | ||||||
|  |     # | ||||||
|  |     # 2. relaying the "asyncio.Task termination" case. | ||||||
|  |     #   - if the aio-task terminates, maybe with an error, AND the | ||||||
|  |     #    `open_channel_from()` API was used, it will always signal | ||||||
|  |     #    that termination. | ||||||
|  |     #  |_`wait_on_coro_final_result()` always calls | ||||||
|  |     #    `to_trio.close()` when `provide_channels=True` so we need to | ||||||
|  |     #    always check if there is an aio-side exc which needs to be | ||||||
|  |     #    relayed to the parent trio side! | ||||||
|  |     #  |_in this case the special `chan._closed_by_aio_task` is | ||||||
|  |     #    ALWAYS set. | ||||||
|  |     # | ||||||
|  |     except trio.EndOfChannel as _eoc: | ||||||
|  |         eoc = _eoc | ||||||
|  |         if ( | ||||||
|  |             chan._closed_by_aio_task | ||||||
|  |             and | ||||||
|  |             aio_err | ||||||
|  |         ): | ||||||
|  |             log.cancel( | ||||||
|  |                 f'The asyncio-child task terminated due to error\n' | ||||||
|  |                 f'{aio_err!r}\n' | ||||||
|  |             ) | ||||||
|  |             chan._trio_to_raise = aio_err | ||||||
|  |             trio_err = chan._trio_err = eoc | ||||||
|  |             # | ||||||
|  |             # ?TODO?, raise something like a, | ||||||
|  |             # chan._trio_to_raise = AsyncioErrored() | ||||||
|  |             # BUT, with the tb rewritten to reflect the underlying | ||||||
|  |             # call stack? | ||||||
|  |         else: | ||||||
|  |             trio_err = chan._trio_err = eoc | ||||||
|  | 
 | ||||||
|         raise eoc |         raise eoc | ||||||
| 
 | 
 | ||||||
|     # NOTE ALSO SEE the matching note in the `cancel_trio()` asyncio |     # NOTE ALSO SEE the matching note in the `cancel_trio()` asyncio | ||||||
|  | @ -841,7 +909,7 @@ async def translate_aio_errors( | ||||||
|     except BaseException as _trio_err: |     except BaseException as _trio_err: | ||||||
|         trio_err = chan._trio_err = _trio_err |         trio_err = chan._trio_err = _trio_err | ||||||
|         # await tractor.pause(shield=True)  # workx! |         # await tractor.pause(shield=True)  # workx! | ||||||
|         entered: bool = await _debug._maybe_enter_pm( |         entered: bool = await debug._maybe_enter_pm( | ||||||
|             trio_err, |             trio_err, | ||||||
|             api_frame=inspect.currentframe(), |             api_frame=inspect.currentframe(), | ||||||
|         ) |         ) | ||||||
|  | @ -1045,7 +1113,7 @@ async def translate_aio_errors( | ||||||
|         # |         # | ||||||
|         if wait_on_aio_task: |         if wait_on_aio_task: | ||||||
|             await chan._aio_task_complete.wait() |             await chan._aio_task_complete.wait() | ||||||
|             log.info( |             log.debug( | ||||||
|                 'asyncio-task is done and unblocked trio-side!\n' |                 'asyncio-task is done and unblocked trio-side!\n' | ||||||
|             ) |             ) | ||||||
| 
 | 
 | ||||||
|  | @ -1062,11 +1130,17 @@ async def translate_aio_errors( | ||||||
|         trio_to_raise: ( |         trio_to_raise: ( | ||||||
|             AsyncioCancelled| |             AsyncioCancelled| | ||||||
|             AsyncioTaskExited| |             AsyncioTaskExited| | ||||||
|  |             Exception|  # relayed from aio-task | ||||||
|             None |             None | ||||||
|         ) = chan._trio_to_raise |         ) = chan._trio_to_raise | ||||||
| 
 | 
 | ||||||
|  |         raise_from: Exception = ( | ||||||
|  |             trio_err if (aio_err is trio_to_raise) | ||||||
|  |             else aio_err | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|         if not suppress_graceful_exits: |         if not suppress_graceful_exits: | ||||||
|             raise trio_to_raise from (aio_err or trio_err) |             raise trio_to_raise from raise_from | ||||||
| 
 | 
 | ||||||
|         if trio_to_raise: |         if trio_to_raise: | ||||||
|             match ( |             match ( | ||||||
|  | @ -1099,7 +1173,7 @@ async def translate_aio_errors( | ||||||
|                         ) |                         ) | ||||||
|                         return |                         return | ||||||
|                 case _: |                 case _: | ||||||
|                     raise trio_to_raise from (aio_err or trio_err) |                     raise trio_to_raise from raise_from | ||||||
| 
 | 
 | ||||||
|         # Check if the asyncio-side is the cause of the trio-side |         # Check if the asyncio-side is the cause of the trio-side | ||||||
|         # error. |         # error. | ||||||
|  | @ -1165,7 +1239,6 @@ async def run_task( | ||||||
| 
 | 
 | ||||||
| @acm | @acm | ||||||
| async def open_channel_from( | async def open_channel_from( | ||||||
| 
 |  | ||||||
|     target: Callable[..., Any], |     target: Callable[..., Any], | ||||||
|     suppress_graceful_exits: bool = True, |     suppress_graceful_exits: bool = True, | ||||||
|     **target_kwargs, |     **target_kwargs, | ||||||
|  | @ -1199,7 +1272,6 @@ async def open_channel_from( | ||||||
|                     # deliver stream handle upward |                     # deliver stream handle upward | ||||||
|                     yield first, chan |                     yield first, chan | ||||||
|             except trio.Cancelled as taskc: |             except trio.Cancelled as taskc: | ||||||
|                 # await tractor.pause(shield=True)  # ya it worx ;) |  | ||||||
|                 if cs.cancel_called: |                 if cs.cancel_called: | ||||||
|                     if isinstance(chan._trio_to_raise, AsyncioCancelled): |                     if isinstance(chan._trio_to_raise, AsyncioCancelled): | ||||||
|                         log.cancel( |                         log.cancel( | ||||||
|  | @ -1406,7 +1478,7 @@ def run_as_asyncio_guest( | ||||||
|             ) |             ) | ||||||
|             # XXX make it obvi we know this isn't supported yet! |             # XXX make it obvi we know this isn't supported yet! | ||||||
|             assert 0 |             assert 0 | ||||||
|             # await _debug.maybe_init_greenback( |             # await debug.maybe_init_greenback( | ||||||
|             #     force_reload=True, |             #     force_reload=True, | ||||||
|             # ) |             # ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,7 +30,11 @@ from ._broadcast import ( | ||||||
| ) | ) | ||||||
| from ._beg import ( | from ._beg import ( | ||||||
|     collapse_eg as collapse_eg, |     collapse_eg as collapse_eg, | ||||||
|     maybe_collapse_eg as maybe_collapse_eg, |     get_collapsed_eg as get_collapsed_eg, | ||||||
|  |     is_multi_cancelled as is_multi_cancelled, | ||||||
|  | ) | ||||||
|  | from ._taskc import ( | ||||||
|  |     maybe_raise_from_masking_exc as maybe_raise_from_masking_exc, | ||||||
| ) | ) | ||||||
| from ._tn import ( | from ._tn import ( | ||||||
|     maybe_open_nursery as maybe_open_nursery, |     maybe_open_nursery as maybe_open_nursery, | ||||||
|  |  | ||||||
|  | @ -15,31 +15,94 @@ | ||||||
| # along with this program.  If not, see <https://www.gnu.org/licenses/>. | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| `BaseExceptionGroup` related utils and helpers pertaining to | `BaseExceptionGroup` utils and helpers pertaining to | ||||||
| first-class-`trio` from a historical perspective B) | first-class-`trio` from a "historical" perspective, like "loose | ||||||
|  | exception group" task-nurseries. | ||||||
| 
 | 
 | ||||||
| ''' | ''' | ||||||
| from contextlib import ( | from contextlib import ( | ||||||
|     asynccontextmanager as acm, |     asynccontextmanager as acm, | ||||||
| ) | ) | ||||||
|  | from typing import ( | ||||||
|  |     Literal, | ||||||
|  |     Type, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | import trio | ||||||
|  | # from trio._core._concat_tb import ( | ||||||
|  | #     concat_tb, | ||||||
|  | # ) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def maybe_collapse_eg( | # XXX NOTE | ||||||
|     beg: BaseExceptionGroup, | # taken verbatim from `trio._core._run` except, | ||||||
|  | # - remove the NONSTRICT_EXCEPTIONGROUP_NOTE deprecation-note | ||||||
|  | #   guard-check; we know we want an explicit collapse. | ||||||
|  | # - mask out tb rewriting in collapse case, i don't think it really | ||||||
|  | #   matters? | ||||||
|  | # | ||||||
|  | def collapse_exception_group( | ||||||
|  |     excgroup: BaseExceptionGroup[BaseException], | ||||||
| ) -> BaseException: | ) -> BaseException: | ||||||
|  |     """Recursively collapse any single-exception groups into that single contained | ||||||
|  |     exception. | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  |     exceptions = list(excgroup.exceptions) | ||||||
|  |     modified = False | ||||||
|  |     for i, exc in enumerate(exceptions): | ||||||
|  |         if isinstance(exc, BaseExceptionGroup): | ||||||
|  |             new_exc = collapse_exception_group(exc) | ||||||
|  |             if new_exc is not exc: | ||||||
|  |                 modified = True | ||||||
|  |                 exceptions[i] = new_exc | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |         len(exceptions) == 1 | ||||||
|  |         and isinstance(excgroup, BaseExceptionGroup) | ||||||
|  | 
 | ||||||
|  |         # XXX trio's loose-setting condition.. | ||||||
|  |         # and NONSTRICT_EXCEPTIONGROUP_NOTE in getattr(excgroup, "__notes__", ()) | ||||||
|  |     ): | ||||||
|  |         # exceptions[0].__traceback__ = concat_tb( | ||||||
|  |         #     excgroup.__traceback__, | ||||||
|  |         #     exceptions[0].__traceback__, | ||||||
|  |         # ) | ||||||
|  |         return exceptions[0] | ||||||
|  |     elif modified: | ||||||
|  |         return excgroup.derive(exceptions) | ||||||
|  |     else: | ||||||
|  |         return excgroup | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_collapsed_eg( | ||||||
|  |     beg: BaseExceptionGroup, | ||||||
|  | 
 | ||||||
|  | ) -> BaseException|None: | ||||||
|     ''' |     ''' | ||||||
|     If the input beg can collapse to a single non-eg sub-exception, |     If the input beg can collapse to a single sub-exception which is | ||||||
|     return it instead. |     itself **not** an eg, return it. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     if len(excs := beg.exceptions) == 1: |     maybe_exc = collapse_exception_group(beg) | ||||||
|         return excs[0] |     if maybe_exc is beg: | ||||||
|  |         return None | ||||||
| 
 | 
 | ||||||
|     return beg |     return maybe_exc | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @acm | @acm | ||||||
| async def collapse_eg(): | async def collapse_eg( | ||||||
|  |     hide_tb: bool = True, | ||||||
|  | 
 | ||||||
|  |     # XXX, for ex. will always show begs containing single taskc | ||||||
|  |     ignore: set[Type[BaseException]] = { | ||||||
|  |         # trio.Cancelled, | ||||||
|  |     }, | ||||||
|  |     add_notes: bool = True, | ||||||
|  | 
 | ||||||
|  |     bp: bool = False, | ||||||
|  | ): | ||||||
|     ''' |     ''' | ||||||
|     If `BaseExceptionGroup` raised in the body scope is |     If `BaseExceptionGroup` raised in the body scope is | ||||||
|     "collapse-able" (in the same way that |     "collapse-able" (in the same way that | ||||||
|  | @ -47,12 +110,114 @@ async def collapse_eg(): | ||||||
|     only raise the lone emedded non-eg in in place. |     only raise the lone emedded non-eg in in place. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|  |     __tracebackhide__: bool = hide_tb | ||||||
|     try: |     try: | ||||||
|         yield |         yield | ||||||
|     except* BaseException as beg: |     except BaseExceptionGroup as _beg: | ||||||
|         if ( |         beg = _beg | ||||||
|             exc := maybe_collapse_eg(beg) |  | ||||||
|         ) is not beg: |  | ||||||
|             raise exc |  | ||||||
| 
 | 
 | ||||||
|         raise beg |         if ( | ||||||
|  |             bp | ||||||
|  |             and | ||||||
|  |             len(beg.exceptions) > 1 | ||||||
|  |         ): | ||||||
|  |             import tractor | ||||||
|  |             if tractor.current_actor( | ||||||
|  |                 err_on_no_runtime=False, | ||||||
|  |             ): | ||||||
|  |                 await tractor.pause(shield=True) | ||||||
|  |             else: | ||||||
|  |                 breakpoint() | ||||||
|  | 
 | ||||||
|  |         if ( | ||||||
|  |             (exc := get_collapsed_eg(beg)) | ||||||
|  |             and | ||||||
|  |             type(exc) not in ignore | ||||||
|  |         ): | ||||||
|  | 
 | ||||||
|  |             # TODO? report number of nested groups it was collapsed | ||||||
|  |             # *from*? | ||||||
|  |             if add_notes: | ||||||
|  |                 from_group_note: str = ( | ||||||
|  |                     '( ^^^ this exc was collapsed from a group ^^^ )\n' | ||||||
|  |                 ) | ||||||
|  |                 if ( | ||||||
|  |                     from_group_note | ||||||
|  |                     not in | ||||||
|  |                     getattr(exc, "__notes__", ()) | ||||||
|  |                 ): | ||||||
|  |                     exc.add_note(from_group_note) | ||||||
|  | 
 | ||||||
|  |             # raise exc | ||||||
|  |             # ^^ this will leave the orig beg tb above with the | ||||||
|  |             # "during the handling of <beg> the following.." | ||||||
|  |             # So, instead do.. | ||||||
|  |             # | ||||||
|  |             if cause := exc.__cause__: | ||||||
|  |                 raise exc from cause | ||||||
|  |             else: | ||||||
|  |                 # suppress "during handling of <the beg>" | ||||||
|  |                 # output in tb/console. | ||||||
|  |                 raise exc from None | ||||||
|  | 
 | ||||||
|  |         # keep original | ||||||
|  |         raise # beg | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def is_multi_cancelled( | ||||||
|  |     beg: BaseException|BaseExceptionGroup, | ||||||
|  | 
 | ||||||
|  |     ignore_nested: set[BaseException] = set(), | ||||||
|  | 
 | ||||||
|  | ) -> Literal[False]|BaseExceptionGroup: | ||||||
|  |     ''' | ||||||
|  |     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 not in ignore_nested | ||||||
|  |         # XXX always count-in `trio`'s native signal | ||||||
|  |     ): | ||||||
|  |         ignore_nested.update({trio.Cancelled}) | ||||||
|  | 
 | ||||||
|  |     if isinstance(beg, BaseExceptionGroup): | ||||||
|  |         # https://docs.python.org/3/library/exceptions.html#BaseExceptionGroup.subgroup | ||||||
|  |         # |_ "The condition can be an exception type or tuple of | ||||||
|  |         #   exception types, in which case each exception is checked | ||||||
|  |         #   for a match using the same check that is used in an | ||||||
|  |         #   except clause. The condition can also be a callable | ||||||
|  |         #   (other than a type object) that accepts an exception as | ||||||
|  |         #   its single argument and returns true for the exceptions | ||||||
|  |         #   that should be in the subgroup." | ||||||
|  |         matched_exc: BaseExceptionGroup|None = beg.subgroup( | ||||||
|  |             tuple(ignore_nested), | ||||||
|  | 
 | ||||||
|  |             # ??TODO, complain about why not allowed to use | ||||||
|  |             # named arg style calling??? | ||||||
|  |             # XD .. wtf? | ||||||
|  |             # 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 | ||||||
|  |  | ||||||
|  | @ -38,6 +38,12 @@ from typing import ( | ||||||
| import trio | import trio | ||||||
| from tractor._state import current_actor | from tractor._state import current_actor | ||||||
| from tractor.log import get_logger | from tractor.log import get_logger | ||||||
|  | from ._tn import maybe_open_nursery | ||||||
|  | # from ._beg import collapse_eg | ||||||
|  | # from ._taskc import ( | ||||||
|  | #     maybe_raise_from_masking_exc, | ||||||
|  | # ) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| log = get_logger(__name__) | log = get_logger(__name__) | ||||||
|  | @ -75,6 +81,9 @@ async def _enter_and_wait( | ||||||
| async def gather_contexts( | async def gather_contexts( | ||||||
|     mngrs: Sequence[AsyncContextManager[T]], |     mngrs: Sequence[AsyncContextManager[T]], | ||||||
| 
 | 
 | ||||||
|  |     # caller can provide their own scope | ||||||
|  |     tn: trio.Nursery|None = None, | ||||||
|  | 
 | ||||||
| ) -> AsyncGenerator[ | ) -> AsyncGenerator[ | ||||||
|     tuple[ |     tuple[ | ||||||
|         T | None, |         T | None, | ||||||
|  | @ -83,17 +92,19 @@ async def gather_contexts( | ||||||
|     None, |     None, | ||||||
| ]: | ]: | ||||||
|     ''' |     ''' | ||||||
|     Concurrently enter a sequence of async context managers (acms), |     Concurrently enter a sequence of async context managers (`acm`s), | ||||||
|     each from a separate `trio` task and deliver the unwrapped |     each scheduled in a separate `trio.Task` and deliver their | ||||||
|     `yield`-ed values in the same order once all managers have entered. |     unwrapped `yield`-ed values in the same order once all `@acm`s | ||||||
|  |     in every task have entered. | ||||||
| 
 | 
 | ||||||
|     On exit, all acms are subsequently and concurrently exited. |     On exit, all `acm`s are subsequently and concurrently exited with | ||||||
|  |     **no order guarantees**. | ||||||
| 
 | 
 | ||||||
|     This function is somewhat similar to a batch of non-blocking |     This function is somewhat similar to a batch of non-blocking | ||||||
|     calls to `contextlib.AsyncExitStack.enter_async_context()` |     calls to `contextlib.AsyncExitStack.enter_async_context()` | ||||||
|     (inside a loop) *in combo with* a `asyncio.gather()` to get the |     (inside a loop) *in combo with* a `asyncio.gather()` to get the | ||||||
|     `.__aenter__()`-ed values, except the managers are both |     `.__aenter__()`-ed values, except the managers are both | ||||||
|     concurrently entered and exited and *cancellation just works*(R). |     concurrently entered and exited and *cancellation-just-works™*. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     seed: int = id(mngrs) |     seed: int = id(mngrs) | ||||||
|  | @ -113,37 +124,47 @@ async def gather_contexts( | ||||||
|     if not mngrs: |     if not mngrs: | ||||||
|         raise ValueError( |         raise ValueError( | ||||||
|             '`.trionics.gather_contexts()` input mngrs is empty?\n' |             '`.trionics.gather_contexts()` input mngrs is empty?\n' | ||||||
|  |             '\n' | ||||||
|             'Did try to use inline generator syntax?\n' |             'Did try to use inline generator syntax?\n' | ||||||
|             'Use a non-lazy iterator or sequence type intead!' |             'Check that list({mngrs}) works!\n' | ||||||
|  |             # 'or sequence-type intead!\n' | ||||||
|  |             # 'Use a non-lazy iterator or sequence-type intead!\n' | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     async with trio.open_nursery( |     try: | ||||||
|         strict_exception_groups=False, |         async with ( | ||||||
|         # ^XXX^ TODO? soo roll our own then ?? |             # | ||||||
|         # -> since we kinda want the "if only one `.exception` then |             # ?TODO, does including these (eg-collapsing, | ||||||
|         # just raise that" interface? |             # taskc-unmasking) improve tb noise-reduction/legibility? | ||||||
|     ) as tn: |             # | ||||||
|         for mngr in mngrs: |             # collapse_eg(), | ||||||
|             tn.start_soon( |             maybe_open_nursery( | ||||||
|                 _enter_and_wait, |                 nursery=tn, | ||||||
|                 mngr, |             ) as tn, | ||||||
|                 unwrapped, |             # maybe_raise_from_masking_exc(), | ||||||
|                 all_entered, |         ): | ||||||
|                 parent_exit, |             for mngr in mngrs: | ||||||
|                 seed, |                 tn.start_soon( | ||||||
|             ) |                     _enter_and_wait, | ||||||
|  |                     mngr, | ||||||
|  |                     unwrapped, | ||||||
|  |                     all_entered, | ||||||
|  |                     parent_exit, | ||||||
|  |                     seed, | ||||||
|  |                 ) | ||||||
| 
 | 
 | ||||||
|         # deliver control once all managers have started up |             # deliver control to caller once all ctx-managers have | ||||||
|         await all_entered.wait() |             # started (yielded back to us). | ||||||
| 
 |             await all_entered.wait() | ||||||
|         try: |  | ||||||
|             yield tuple(unwrapped.values()) |             yield tuple(unwrapped.values()) | ||||||
|         finally: |  | ||||||
|             # NOTE: this is ABSOLUTELY REQUIRED to avoid |  | ||||||
|             # the following wacky bug: |  | ||||||
|             # <tractorbugurlhere> |  | ||||||
|             parent_exit.set() |             parent_exit.set() | ||||||
| 
 | 
 | ||||||
|  |     finally: | ||||||
|  |         # XXX NOTE: this is ABSOLUTELY REQUIRED to avoid | ||||||
|  |         # the following wacky bug: | ||||||
|  |         # <tractorbugurlhere> | ||||||
|  |         parent_exit.set() | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| # Per actor task caching helpers. | # Per actor task caching helpers. | ||||||
| # Further potential examples of interest: | # Further potential examples of interest: | ||||||
|  | @ -155,7 +176,7 @@ class _Cache: | ||||||
|     a kept-alive-while-in-use async resource. |     a kept-alive-while-in-use async resource. | ||||||
| 
 | 
 | ||||||
|     ''' |     ''' | ||||||
|     service_n: Optional[trio.Nursery] = None |     service_tn: Optional[trio.Nursery] = None | ||||||
|     locks: dict[Hashable, trio.Lock] = {} |     locks: dict[Hashable, trio.Lock] = {} | ||||||
|     users: int = 0 |     users: int = 0 | ||||||
|     values: dict[Any,  Any] = {} |     values: dict[Any,  Any] = {} | ||||||
|  | @ -196,6 +217,9 @@ async def maybe_open_context( | ||||||
|     kwargs: dict = {}, |     kwargs: dict = {}, | ||||||
|     key: Hashable | Callable[..., Hashable] = None, |     key: Hashable | Callable[..., Hashable] = None, | ||||||
| 
 | 
 | ||||||
|  |     # caller can provide their own scope | ||||||
|  |     tn: trio.Nursery|None = None, | ||||||
|  | 
 | ||||||
| ) -> AsyncIterator[tuple[bool, T]]: | ) -> AsyncIterator[tuple[bool, T]]: | ||||||
|     ''' |     ''' | ||||||
|     Maybe open an async-context-manager (acm) if there is not already |     Maybe open an async-context-manager (acm) if there is not already | ||||||
|  | @ -228,40 +252,94 @@ async def maybe_open_context( | ||||||
|     # have it not be closed until all consumers have exited (which is |     # have it not be closed until all consumers have exited (which is | ||||||
|     # currently difficult to implement any other way besides using our |     # currently difficult to implement any other way besides using our | ||||||
|     # pre-allocated runtime instance..) |     # pre-allocated runtime instance..) | ||||||
|     service_n: trio.Nursery = current_actor()._service_n |     if tn: | ||||||
|  |         # TODO, assert tn is eventual parent of this task! | ||||||
|  |         task: trio.Task = trio.lowlevel.current_task() | ||||||
|  |         task_tn: trio.Nursery = task.parent_nursery | ||||||
|  |         if not tn._cancel_status.encloses( | ||||||
|  |             task_tn._cancel_status | ||||||
|  |         ): | ||||||
|  |             raise RuntimeError( | ||||||
|  |                 f'Mis-nesting of task under provided {tn} !?\n' | ||||||
|  |                 f'Current task is NOT a child(-ish)!!\n' | ||||||
|  |                 f'\n' | ||||||
|  |                 f'task: {task}\n' | ||||||
|  |                 f'task_tn: {task_tn}\n' | ||||||
|  |             ) | ||||||
|  |         service_tn = tn | ||||||
|  |     else: | ||||||
|  |         service_tn: trio.Nursery = current_actor()._service_tn | ||||||
| 
 | 
 | ||||||
|     # TODO: is there any way to allocate |     # TODO: is there any way to allocate | ||||||
|     # a 'stays-open-till-last-task-finshed nursery? |     # a 'stays-open-till-last-task-finshed nursery? | ||||||
|     # service_n: trio.Nursery |     # service_tn: trio.Nursery | ||||||
|     # async with maybe_open_nursery(_Cache.service_n) as service_n: |     # async with maybe_open_nursery(_Cache.service_tn) as service_tn: | ||||||
|     #     _Cache.service_n = service_n |     #     _Cache.service_tn = service_tn | ||||||
| 
 | 
 | ||||||
|  |     cache_miss_ke: KeyError|None = None | ||||||
|  |     maybe_taskc: trio.Cancelled|None = None | ||||||
|     try: |     try: | ||||||
|         # **critical section** that should prevent other tasks from |         # **critical section** that should prevent other tasks from | ||||||
|         # checking the _Cache until complete otherwise the scheduler |         # checking the _Cache until complete otherwise the scheduler | ||||||
|         # may switch and by accident we create more then one resource. |         # may switch and by accident we create more then one resource. | ||||||
|         yielded = _Cache.values[ctx_key] |         yielded = _Cache.values[ctx_key] | ||||||
| 
 | 
 | ||||||
|     except KeyError: |     except KeyError as _ke: | ||||||
|         log.debug(f'Allocating new {acm_func} for {ctx_key}') |         # XXX, stay mutexed up to cache-miss yield | ||||||
|         mngr = acm_func(**kwargs) |         try: | ||||||
|         resources = _Cache.resources |             cache_miss_ke = _ke | ||||||
|         assert not resources.get(ctx_key), f'Resource exists? {ctx_key}' |             log.debug( | ||||||
|         resources[ctx_key] = (service_n, trio.Event()) |                 f'Allocating new @acm-func entry\n' | ||||||
|  |                 f'ctx_key={ctx_key}\n' | ||||||
|  |                 f'acm_func={acm_func}\n' | ||||||
|  |             ) | ||||||
|  |             mngr = acm_func(**kwargs) | ||||||
|  |             resources = _Cache.resources | ||||||
|  |             assert not resources.get(ctx_key), f'Resource exists? {ctx_key}' | ||||||
|  |             resources[ctx_key] = (service_tn, trio.Event()) | ||||||
|  |             yielded: Any = await service_tn.start( | ||||||
|  |                 _Cache.run_ctx, | ||||||
|  |                 mngr, | ||||||
|  |                 ctx_key, | ||||||
|  |             ) | ||||||
|  |             _Cache.users += 1 | ||||||
|  |         finally: | ||||||
|  |             # XXX, since this runs from an `except` it's a checkpoint | ||||||
|  |             # whih can be `trio.Cancelled`-masked. | ||||||
|  |             # | ||||||
|  |             # NOTE, in that case the mutex is never released by the | ||||||
|  |             # (first and) caching task and **we can't** simply shield | ||||||
|  |             # bc that will inf-block on the `await | ||||||
|  |             # no_more_users.wait()`. | ||||||
|  |             # | ||||||
|  |             # SO just always unlock! | ||||||
|  |             lock.release() | ||||||
| 
 | 
 | ||||||
|         # sync up to the mngr's yielded value |         try: | ||||||
|         yielded = await service_n.start( |             yield ( | ||||||
|             _Cache.run_ctx, |                 False,  # cache_hit = "no" | ||||||
|             mngr, |                 yielded, | ||||||
|             ctx_key, |             ) | ||||||
|         ) |         except trio.Cancelled as taskc: | ||||||
|         _Cache.users += 1 |             maybe_taskc = taskc | ||||||
|         lock.release() |             log.cancel( | ||||||
|         yield False, yielded |                 f'Cancelled from cache-miss entry\n' | ||||||
|  |                 f'\n' | ||||||
|  |                 f'ctx_key: {ctx_key!r}\n' | ||||||
|  |                 f'mngr: {mngr!r}\n' | ||||||
|  |             ) | ||||||
|  |             # XXX, always unset ke from cancelled context | ||||||
|  |             # since we never consider it a masked exc case! | ||||||
|  |             # - bc this can be called directly ty `._rpc._invoke()`? | ||||||
|  |             # | ||||||
|  |             if maybe_taskc.__context__ is cache_miss_ke: | ||||||
|  |                 maybe_taskc.__context__ = None | ||||||
|  | 
 | ||||||
|  |             raise taskc | ||||||
| 
 | 
 | ||||||
|     else: |     else: | ||||||
|         _Cache.users += 1 |         _Cache.users += 1 | ||||||
|         log.runtime( |         log.debug( | ||||||
|             f'Re-using cached resource for user {_Cache.users}\n\n' |             f'Re-using cached resource for user {_Cache.users}\n\n' | ||||||
|             f'{ctx_key!r} -> {type(yielded)}\n' |             f'{ctx_key!r} -> {type(yielded)}\n' | ||||||
| 
 | 
 | ||||||
|  | @ -271,9 +349,19 @@ async def maybe_open_context( | ||||||
|             # f'{ctx_key!r} -> {yielded!r}\n' |             # f'{ctx_key!r} -> {yielded!r}\n' | ||||||
|         ) |         ) | ||||||
|         lock.release() |         lock.release() | ||||||
|         yield True, yielded |         yield ( | ||||||
|  |             True,  # cache_hit = "yes" | ||||||
|  |             yielded, | ||||||
|  |         ) | ||||||
| 
 | 
 | ||||||
|     finally: |     finally: | ||||||
|  |         if lock.locked(): | ||||||
|  |             stats: trio.LockStatistics = lock.statistics() | ||||||
|  |             log.error( | ||||||
|  |                 f'Lock left locked by last owner !?\n' | ||||||
|  |                 f'{stats}\n' | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|         _Cache.users -= 1 |         _Cache.users -= 1 | ||||||
| 
 | 
 | ||||||
|         if yielded is not None: |         if yielded is not None: | ||||||
|  |  | ||||||
|  | @ -0,0 +1,184 @@ | ||||||
|  | # tractor: structured concurrent "actors". | ||||||
|  | # Copyright 2018-eternity Tyler Goodlet. | ||||||
|  | 
 | ||||||
|  | # This program is free software: you can redistribute it and/or modify | ||||||
|  | # it under the terms of the GNU Affero General Public License as published by | ||||||
|  | # the Free Software Foundation, either version 3 of the License, or | ||||||
|  | # (at your option) any later version. | ||||||
|  | 
 | ||||||
|  | # This program is distributed in the hope that it will be useful, | ||||||
|  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | # GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  | # You should have received a copy of the GNU Affero General Public License | ||||||
|  | # along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | `trio.Task` cancellation helpers, extensions and "holsters". | ||||||
|  | 
 | ||||||
|  | ''' | ||||||
|  | from __future__ import annotations | ||||||
|  | from contextlib import ( | ||||||
|  |     asynccontextmanager as acm, | ||||||
|  | ) | ||||||
|  | from typing import TYPE_CHECKING | ||||||
|  | 
 | ||||||
|  | import trio | ||||||
|  | from tractor.log import get_logger | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if TYPE_CHECKING: | ||||||
|  |     from tractor.devx.debug import BoxedMaybeException | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def find_masked_excs( | ||||||
|  |     maybe_masker: BaseException, | ||||||
|  |     unmask_from: set[BaseException], | ||||||
|  | ) -> BaseException|None: | ||||||
|  |     '''' | ||||||
|  |     Deliver any `maybe_masker.__context__` provided | ||||||
|  |     it a declared masking exc-type entry in `unmask_from`. | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     if ( | ||||||
|  |         type(maybe_masker) in unmask_from | ||||||
|  |         and | ||||||
|  |         (exc_ctx := maybe_masker.__context__) | ||||||
|  | 
 | ||||||
|  |         # TODO? what about any cases where | ||||||
|  |         # they could be the same type but not same instance? | ||||||
|  |         # |_i.e. a cancel masking a cancel ?? | ||||||
|  |         # or ( | ||||||
|  |         #     exc_ctx is not maybe_masker | ||||||
|  |         # ) | ||||||
|  |     ): | ||||||
|  |         return exc_ctx | ||||||
|  | 
 | ||||||
|  |     return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # XXX, relevant discussion @ `trio`-core, | ||||||
|  | # https://github.com/python-trio/trio/issues/455 | ||||||
|  | # | ||||||
|  | @acm | ||||||
|  | async def maybe_raise_from_masking_exc( | ||||||
|  |     tn: trio.Nursery|None = None, | ||||||
|  |     unmask_from: ( | ||||||
|  |         BaseException| | ||||||
|  |         tuple[BaseException] | ||||||
|  |     ) = (trio.Cancelled,), | ||||||
|  | 
 | ||||||
|  |     raise_unmasked: bool = True, | ||||||
|  |     extra_note: str = ( | ||||||
|  |         'This can occurr when,\n' | ||||||
|  |         ' - a `trio.Nursery` scope embeds a `finally:`-block ' | ||||||
|  |         'which executes a checkpoint!' | ||||||
|  |         # | ||||||
|  |         # ^TODO? other cases? | ||||||
|  |     ), | ||||||
|  | 
 | ||||||
|  |     always_warn_on: tuple[BaseException] = ( | ||||||
|  |         trio.Cancelled, | ||||||
|  |     ), | ||||||
|  |     # ^XXX, special case(s) where we warn-log bc likely | ||||||
|  |     # there will be no operational diff since the exc | ||||||
|  |     # is always expected to be consumed. | ||||||
|  | ) -> BoxedMaybeException: | ||||||
|  |     ''' | ||||||
|  |     Maybe un-mask and re-raise exception(s) suppressed by a known | ||||||
|  |     error-used-as-signal type (cough namely `trio.Cancelled`). | ||||||
|  | 
 | ||||||
|  |     Though this unmasker targets cancelleds, it can be used more | ||||||
|  |     generally to capture and unwrap masked excs detected as | ||||||
|  |     `.__context__` values which were suppressed by any error type | ||||||
|  |     passed in `unmask_from`. | ||||||
|  | 
 | ||||||
|  |     ------------- | ||||||
|  |     STILL-TODO ?? | ||||||
|  |     ------------- | ||||||
|  |     -[ ] support for egs which have multiple masked entries in | ||||||
|  |         `maybe_eg.exceptions`, in which case we should unmask the | ||||||
|  |         individual sub-excs but maintain the eg-parent's form right? | ||||||
|  | 
 | ||||||
|  |     ''' | ||||||
|  |     from tractor.devx.debug import ( | ||||||
|  |         BoxedMaybeException, | ||||||
|  |         pause, | ||||||
|  |     ) | ||||||
|  |     boxed_maybe_exc = BoxedMaybeException( | ||||||
|  |         raise_on_exit=raise_unmasked, | ||||||
|  |     ) | ||||||
|  |     matching: list[BaseException]|None = None | ||||||
|  |     maybe_eg: ExceptionGroup|None | ||||||
|  | 
 | ||||||
|  |     if tn: | ||||||
|  |         try:  # handle egs | ||||||
|  |             yield boxed_maybe_exc | ||||||
|  |             return | ||||||
|  |         except* unmask_from as _maybe_eg: | ||||||
|  |             maybe_eg = _maybe_eg | ||||||
|  |             matches: ExceptionGroup | ||||||
|  |             matches, _ = maybe_eg.split( | ||||||
|  |                 unmask_from | ||||||
|  |             ) | ||||||
|  |             if not matches: | ||||||
|  |                 raise | ||||||
|  | 
 | ||||||
|  |             matching: list[BaseException] = matches.exceptions | ||||||
|  |     else: | ||||||
|  |         try:  # handle non-egs | ||||||
|  |             yield boxed_maybe_exc | ||||||
|  |             return | ||||||
|  |         except unmask_from as _maybe_exc: | ||||||
|  |             maybe_exc = _maybe_exc | ||||||
|  |             matching: list[BaseException] = [ | ||||||
|  |                 maybe_exc | ||||||
|  |             ] | ||||||
|  | 
 | ||||||
|  |         # XXX, only unmask-ed for debuggin! | ||||||
|  |         # TODO, remove eventually.. | ||||||
|  |         except BaseException as _berr: | ||||||
|  |             berr = _berr | ||||||
|  |             await pause(shield=True) | ||||||
|  |             raise berr | ||||||
|  | 
 | ||||||
|  |     if matching is None: | ||||||
|  |         raise | ||||||
|  | 
 | ||||||
|  |     masked: list[tuple[BaseException, BaseException]] = [] | ||||||
|  |     for exc_match in matching: | ||||||
|  | 
 | ||||||
|  |         if exc_ctx := find_masked_excs( | ||||||
|  |             maybe_masker=exc_match, | ||||||
|  |             unmask_from={unmask_from}, | ||||||
|  |         ): | ||||||
|  |             masked.append((exc_ctx, exc_match)) | ||||||
|  |             boxed_maybe_exc.value = exc_match | ||||||
|  |             note: str = ( | ||||||
|  |                 f'\n' | ||||||
|  |                 f'^^WARNING^^ the above {exc_ctx!r} was masked by a {unmask_from!r}\n' | ||||||
|  |             ) | ||||||
|  |             if extra_note: | ||||||
|  |                 note += ( | ||||||
|  |                     f'\n' | ||||||
|  |                     f'{extra_note}\n' | ||||||
|  |                 ) | ||||||
|  |             exc_ctx.add_note(note) | ||||||
|  | 
 | ||||||
|  |             if type(exc_match) in always_warn_on: | ||||||
|  |                 log.warning(note) | ||||||
|  | 
 | ||||||
|  |             # await tractor.pause(shield=True) | ||||||
|  |             if raise_unmasked: | ||||||
|  | 
 | ||||||
|  |                 if len(masked) < 2: | ||||||
|  |                     raise exc_ctx from exc_match | ||||||
|  |                 else: | ||||||
|  |                     # ?TODO, see above but, possibly unmasking sub-exc | ||||||
|  |                     # entries if there are > 1 | ||||||
|  |                     await pause(shield=True) | ||||||
|  |     else: | ||||||
|  |         raise | ||||||
							
								
								
									
										419
									
								
								uv.lock
								
								
								
								
							
							
						
						
									
										419
									
								
								uv.lock
								
								
								
								
							|  | @ -1,23 +1,23 @@ | ||||||
| version = 1 | version = 1 | ||||||
| revision = 1 | revision = 2 | ||||||
| requires-python = ">=3.11" | requires-python = ">=3.11" | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "attrs" | name = "attrs" | ||||||
| version = "24.3.0" | version = "24.3.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984, upload-time = "2024-12-16T06:59:29.899Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 }, |     { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397, upload-time = "2024-12-16T06:59:26.977Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "bidict" | name = "bidict" | ||||||
| version = "0.23.1" | version = "0.23.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 } | sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 }, |     { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -27,51 +27,51 @@ source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "pycparser" }, |     { name = "pycparser" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 }, |     { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 }, |     { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 }, |     { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 }, |     { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 }, |     { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 }, |     { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 }, |     { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 }, |     { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 }, |     { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 }, |     { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, | ||||||
|     { 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/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" }, | ||||||
|     { 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/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 }, |     { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 }, |     { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, |     { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, |     { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, |     { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, |     { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, |     { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, |     { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, |     { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, | ||||||
|     { 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/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, | ||||||
|     { 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/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, |     { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, |     { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, |     { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, |     { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, |     { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, |     { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, |     { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, |     { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, |     { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, | ||||||
|     { 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/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, |     { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "colorama" | name = "colorama" | ||||||
| version = "0.4.6" | version = "0.4.6" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, |     { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -81,9 +81,9 @@ source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "colorama", marker = "sys_platform == 'win32'" }, |     { 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 } | sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424 }, |     { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -95,98 +95,98 @@ dependencies = [ | ||||||
|     { name = "outcome" }, |     { name = "outcome" }, | ||||||
|     { name = "sniffio" }, |     { name = "sniffio" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/ab3a42c0f3ed56df9cd33de1539b3198d98c6ccbaf88a73d6be0b72d85e0/greenback-1.2.1.tar.gz", hash = "sha256:de3ca656885c03b96dab36079f3de74bb5ba061da9bfe3bb69dccc866ef95ea3", size = 42597 } | sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/ab3a42c0f3ed56df9cd33de1539b3198d98c6ccbaf88a73d6be0b72d85e0/greenback-1.2.1.tar.gz", hash = "sha256:de3ca656885c03b96dab36079f3de74bb5ba061da9bfe3bb69dccc866ef95ea3", size = 42597, upload-time = "2024-02-20T21:23:13.239Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/71/d0/b8dc79d5ecfffacad9c844b6ae76b9c6259935796d3c561deccbf8fa421d/greenback-1.2.1-py3-none-any.whl", hash = "sha256:98768edbbe4340091a9730cf64a683fcbaa3f2cb81e4ac41d7ed28d3b6f74b79", size = 28062 }, |     { url = "https://files.pythonhosted.org/packages/71/d0/b8dc79d5ecfffacad9c844b6ae76b9c6259935796d3c561deccbf8fa421d/greenback-1.2.1-py3-none-any.whl", hash = "sha256:98768edbbe4340091a9730cf64a683fcbaa3f2cb81e4ac41d7ed28d3b6f74b79", size = 28062, upload-time = "2024-02-20T21:23:12.031Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "greenlet" | name = "greenlet" | ||||||
| version = "3.1.1" | version = "3.1.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" } | ||||||
| wheels = [ | 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/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" }, | ||||||
|     { 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/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" }, | ||||||
|     { 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/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" }, | ||||||
|     { 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/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" }, | ||||||
|     { 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/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" }, | ||||||
|     { 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/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, upload-time = "2024-09-20T17:08:31.728Z" }, | ||||||
|     { 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/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" }, | ||||||
|     { 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/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" }, | ||||||
|     { 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/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" }, | ||||||
|     { 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/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" }, | ||||||
|     { 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/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" }, | ||||||
|     { 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/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" }, | ||||||
|     { 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/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035, upload-time = "2024-09-20T17:44:26.501Z" }, | ||||||
|     { 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/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload-time = "2024-09-20T17:08:42.048Z" }, | ||||||
|     { 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/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, upload-time = "2024-09-20T17:08:33.707Z" }, | ||||||
|     { 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/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload-time = "2024-09-20T17:44:15.989Z" }, | ||||||
|     { 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/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955, upload-time = "2024-09-20T17:09:25.539Z" }, | ||||||
|     { 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/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655, upload-time = "2024-09-20T17:21:22.427Z" }, | ||||||
|     { 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/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload-time = "2024-09-20T17:08:26.312Z" }, | ||||||
|     { 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/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload-time = "2024-09-20T17:36:48.983Z" }, | ||||||
|     { 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/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload-time = "2024-09-20T17:39:22.705Z" }, | ||||||
|     { 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/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736, upload-time = "2024-09-20T17:44:28.544Z" }, | ||||||
|     { 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/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347, upload-time = "2024-09-20T17:08:45.56Z" }, | ||||||
|     { 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/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, upload-time = "2024-09-20T17:08:36.85Z" }, | ||||||
|     { 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/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039, upload-time = "2024-09-20T17:44:18.287Z" }, | ||||||
|     { 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/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716, upload-time = "2024-09-20T17:09:27.112Z" }, | ||||||
|     { 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/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490, upload-time = "2024-09-20T17:17:09.501Z" }, | ||||||
|     { 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/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731, upload-time = "2024-09-20T17:36:50.376Z" }, | ||||||
|     { 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/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304, upload-time = "2024-09-20T17:39:24.55Z" }, | ||||||
|     { 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/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537, upload-time = "2024-09-20T17:44:31.102Z" }, | ||||||
|     { 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/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506, upload-time = "2024-09-20T17:08:47.852Z" }, | ||||||
|     { 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/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, upload-time = "2024-09-20T17:08:38.079Z" }, | ||||||
|     { 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/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731, upload-time = "2024-09-20T17:44:20.556Z" }, | ||||||
|     { 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 }, |     { 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, upload-time = "2024-09-20T17:09:28.753Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "idna" | name = "idna" | ||||||
| version = "3.10" | version = "3.10" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, |     { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "iniconfig" | name = "iniconfig" | ||||||
| version = "2.0.0" | version = "2.0.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, |     { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "msgspec" | name = "msgspec" | ||||||
| version = "0.19.0" | version = "0.19.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934 } | sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939 }, |     { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939, upload-time = "2024-12-27T17:39:32.347Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202 }, |     { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202, upload-time = "2024-12-27T17:39:33.633Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029 }, |     { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029, upload-time = "2024-12-27T17:39:35.023Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682 }, |     { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682, upload-time = "2024-12-27T17:39:36.384Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003 }, |     { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003, upload-time = "2024-12-27T17:39:39.097Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833 }, |     { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833, upload-time = "2024-12-27T17:39:41.203Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184 }, |     { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184, upload-time = "2024-12-27T17:39:43.702Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485 }, |     { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910 }, |     { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633 }, |     { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594 }, |     { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594, upload-time = "2024-12-27T17:39:51.204Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053 }, |     { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053, upload-time = "2024-12-27T17:39:52.866Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081 }, |     { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081, upload-time = "2024-12-27T17:39:55.142Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467 }, |     { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467, upload-time = "2024-12-27T17:39:56.531Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498 }, |     { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498, upload-time = "2024-12-27T17:40:00.427Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950 }, |     { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950, upload-time = "2024-12-27T17:40:04.219Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647 }, |     { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647, upload-time = "2024-12-27T17:40:05.606Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563 }, |     { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563, upload-time = "2024-12-27T17:40:10.516Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996 }, |     { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996, upload-time = "2024-12-27T17:40:12.244Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087 }, |     { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087, upload-time = "2024-12-27T17:40:14.881Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432 }, |     { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432, upload-time = "2024-12-27T17:40:16.256Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -196,18 +196,18 @@ source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "attrs" }, |     { name = "attrs" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 } | sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" } | ||||||
| wheels = [ | 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 }, |     { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "packaging" | name = "packaging" | ||||||
| version = "24.2" | version = "24.2" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, |     { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -219,9 +219,9 @@ dependencies = [ | ||||||
|     { name = "pygments" }, |     { name = "pygments" }, | ||||||
|     { name = "tabcompleter" }, |     { name = "tabcompleter" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/69/13/80da03638f62facbee76312ca9ee5941c017b080f2e4c6919fd4e87e16e3/pdbp-1.6.1.tar.gz", hash = "sha256:f4041642952a05df89664e166d5bd379607a0866ddd753c06874f65552bdf40b", size = 25322 } | sdist = { url = "https://files.pythonhosted.org/packages/69/13/80da03638f62facbee76312ca9ee5941c017b080f2e4c6919fd4e87e16e3/pdbp-1.6.1.tar.gz", hash = "sha256:f4041642952a05df89664e166d5bd379607a0866ddd753c06874f65552bdf40b", size = 25322, upload-time = "2024-11-07T15:36:43.062Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/29/93/d56fb9ba5569dc29d8263c72e46d21a2fd38741339ebf03f54cf7561828c/pdbp-1.6.1-py3-none-any.whl", hash = "sha256:f10bad2ee044c0e5c168cb0825abfdbdc01c50013e9755df5261b060bdd35c22", size = 21495 }, |     { url = "https://files.pythonhosted.org/packages/29/93/d56fb9ba5569dc29d8263c72e46d21a2fd38741339ebf03f54cf7561828c/pdbp-1.6.1-py3-none-any.whl", hash = "sha256:f10bad2ee044c0e5c168cb0825abfdbdc01c50013e9755df5261b060bdd35c22", size = 21495, upload-time = "2024-11-07T15:36:41.061Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -231,18 +231,18 @@ source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "ptyprocess" }, |     { name = "ptyprocess" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 } | sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 }, |     { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "pluggy" | name = "pluggy" | ||||||
| version = "1.5.0" | version = "1.5.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, |     { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -252,66 +252,66 @@ source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "wcwidth" }, |     { name = "wcwidth" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 } | sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087, upload-time = "2025-01-20T15:55:35.072Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 }, |     { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816, upload-time = "2025-01-20T15:55:29.98Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "psutil" | name = "psutil" | ||||||
| version = "7.0.0" | version = "7.0.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 } | sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051 }, |     { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535 }, |     { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004 }, |     { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986 }, |     { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544 }, |     { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 }, |     { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 }, |     { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "ptyprocess" | name = "ptyprocess" | ||||||
| version = "0.7.0" | version = "0.7.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 }, |     { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "pycparser" | name = "pycparser" | ||||||
| version = "2.22" | version = "2.22" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, |     { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "pygments" | name = "pygments" | ||||||
| version = "2.19.1" | version = "2.19.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, |     { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "pyperclip" | name = "pyperclip" | ||||||
| version = "1.9.0" | version = "1.9.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961, upload-time = "2024-06-18T20:38:48.401Z" } | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "pyreadline3" | name = "pyreadline3" | ||||||
| version = "3.5.4" | version = "3.5.4" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 }, |     { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -324,36 +324,36 @@ dependencies = [ | ||||||
|     { name = "packaging" }, |     { name = "packaging" }, | ||||||
|     { name = "pluggy" }, |     { name = "pluggy" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, |     { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "sniffio" | name = "sniffio" | ||||||
| version = "1.3.1" | version = "1.3.1" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, |     { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "sortedcontainers" | name = "sortedcontainers" | ||||||
| version = "2.4.0" | version = "2.4.0" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 }, |     { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "stackscope" | name = "stackscope" | ||||||
| version = "0.2.2" | version = "0.2.2" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/4a/fc/20dbb993353f31230138f3c63f3f0c881d1853e70d7a30cd68d2ba4cf1e2/stackscope-0.2.2.tar.gz", hash = "sha256:f508c93eb4861ada466dd3ff613ca203962ceb7587ad013759f15394e6a4e619", size = 90479, upload-time = "2024-02-27T22:02:15.831Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/f1/5f/0a674fcafa03528089badb46419413f342537b5b57d2fefc9900fb8ee4e4/stackscope-0.2.2-py3-none-any.whl", hash = "sha256:c199b0cda738d39c993ee04eb01961b06b7e9aeb43ebf9fd6226cdd72ea9faf6", size = 80807 }, |     { url = "https://files.pythonhosted.org/packages/f1/5f/0a674fcafa03528089badb46419413f342537b5b57d2fefc9900fb8ee4e4/stackscope-0.2.2-py3-none-any.whl", hash = "sha256:c199b0cda738d39c993ee04eb01961b06b7e9aeb43ebf9fd6226cdd72ea9faf6", size = 80807, upload-time = "2024-02-27T22:02:13.692Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -363,9 +363,9 @@ source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "pyreadline3", marker = "sys_platform == 'win32'" }, |     { 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 } | sdist = { url = "https://files.pythonhosted.org/packages/73/1a/ed3544579628c5709bae6fae2255e94c6982a9ff77d42d8ba59fd2f3b21a/tabcompleter-1.4.0.tar.gz", hash = "sha256:7562a9938e62f8e7c3be612c3ac4e14c5ec4307b58ba9031c148260e866e8814", size = 10431, upload-time = "2024-10-28T00:44:52.665Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/65/44/bb509c3d2c0b5a87e7a5af1d5917a402a32ff026f777a6d7cb6990746cbb/tabcompleter-1.4.0-py3-none-any.whl", hash = "sha256:d744aa735b49c0a6cc2fb8fcd40077fec47425e4388301010b14e6ce3311368b", size = 6725 }, |     { url = "https://files.pythonhosted.org/packages/65/44/bb509c3d2c0b5a87e7a5af1d5917a402a32ff026f777a6d7cb6990746cbb/tabcompleter-1.4.0-py3-none-any.whl", hash = "sha256:d744aa735b49c0a6cc2fb8fcd40077fec47425e4388301010b14e6ce3311368b", size = 6725, upload-time = "2024-10-28T00:44:51.267Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -392,6 +392,7 @@ dev = [ | ||||||
|     { name = "pyperclip" }, |     { name = "pyperclip" }, | ||||||
|     { name = "pytest" }, |     { name = "pytest" }, | ||||||
|     { name = "stackscope" }, |     { name = "stackscope" }, | ||||||
|  |     { name = "typing-extensions" }, | ||||||
|     { name = "xonsh" }, |     { name = "xonsh" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | @ -416,6 +417,7 @@ dev = [ | ||||||
|     { name = "pyperclip", specifier = ">=1.9.0" }, |     { name = "pyperclip", specifier = ">=1.9.0" }, | ||||||
|     { name = "pytest", specifier = ">=8.3.5" }, |     { name = "pytest", specifier = ">=8.3.5" }, | ||||||
|     { name = "stackscope", specifier = ">=0.2.2,<0.3" }, |     { name = "stackscope", specifier = ">=0.2.2,<0.3" }, | ||||||
|  |     { name = "typing-extensions", specifier = ">=4.14.1" }, | ||||||
|     { name = "xonsh", specifier = ">=0.19.2" }, |     { name = "xonsh", specifier = ">=0.19.2" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | @ -426,9 +428,9 @@ source = { registry = "https://pypi.org/simple" } | ||||||
| dependencies = [ | dependencies = [ | ||||||
|     { name = "trio" }, |     { name = "trio" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/f8/8e/fdd7bc467b40eedd0a5f2ed36b0d692c6e6f2473be00c8160e2e9f53adc1/tricycle-0.4.1.tar.gz", hash = "sha256:f56edb4b3e1bed3e2552b1b499b24a2dab47741e92e9b4d806acc5c35c9e6066", size = 41551 } | sdist = { url = "https://files.pythonhosted.org/packages/f8/8e/fdd7bc467b40eedd0a5f2ed36b0d692c6e6f2473be00c8160e2e9f53adc1/tricycle-0.4.1.tar.gz", hash = "sha256:f56edb4b3e1bed3e2552b1b499b24a2dab47741e92e9b4d806acc5c35c9e6066", size = 41551, upload-time = "2024-02-02T20:41:15.298Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/d7/c6/7cc05d60e21c683df99167db071ce5d848f5063c2a63971a8443466f603e/tricycle-0.4.1-py3-none-any.whl", hash = "sha256:67900995a73e7445e2c70250cdca04a778d9c3923dd960a97ad4569085e0fb3f", size = 35316 }, |     { url = "https://files.pythonhosted.org/packages/d7/c6/7cc05d60e21c683df99167db071ce5d848f5063c2a63971a8443466f603e/tricycle-0.4.1-py3-none-any.whl", hash = "sha256:67900995a73e7445e2c70250cdca04a778d9c3923dd960a97ad4569085e0fb3f", size = 35316, upload-time = "2024-02-02T20:41:14.108Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
|  | @ -443,82 +445,91 @@ dependencies = [ | ||||||
|     { name = "sniffio" }, |     { name = "sniffio" }, | ||||||
|     { name = "sortedcontainers" }, |     { name = "sortedcontainers" }, | ||||||
| ] | ] | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 } | sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952, upload-time = "2025-02-14T07:13:50.724Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 }, |     { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920, upload-time = "2025-02-14T07:13:48.696Z" }, | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "typing-extensions" | ||||||
|  | version = "4.14.1" | ||||||
|  | source = { registry = "https://pypi.org/simple" } | ||||||
|  | sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } | ||||||
|  | wheels = [ | ||||||
|  |     { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "wcwidth" | name = "wcwidth" | ||||||
| version = "0.2.13" | version = "0.2.13" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 }, |     { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "wrapt" | name = "wrapt" | ||||||
| version = "1.17.2" | version = "1.17.2" | ||||||
| source = { registry = "https://pypi.org/simple" } | 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 } | sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" } | ||||||
| wheels = [ | 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/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" }, | ||||||
|     { 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/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" }, | ||||||
|     { 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/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" }, | ||||||
|     { 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/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" }, | ||||||
|     { 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/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, upload-time = "2025-01-14T10:33:41.868Z" }, | ||||||
|     { 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/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, upload-time = "2025-01-14T10:33:43.598Z" }, | ||||||
|     { 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/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" }, | ||||||
|     { 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/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" }, | ||||||
|     { 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/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" }, | ||||||
|     { 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/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" }, | ||||||
|     { 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/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" }, | ||||||
|     { 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/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" }, | ||||||
|     { 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/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" }, | ||||||
|     { 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/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" }, | ||||||
|     { 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/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" }, | ||||||
|     { 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/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, upload-time = "2025-01-14T10:34:09.82Z" }, | ||||||
|     { 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/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, upload-time = "2025-01-14T10:34:11.258Z" }, | ||||||
|     { 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/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" }, | ||||||
|     { 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/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" }, | ||||||
|     { 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/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" }, | ||||||
|     { 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/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" }, | ||||||
|     { 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/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" }, | ||||||
|     { 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/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" }, | ||||||
|     { 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/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" }, | ||||||
|     { 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/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" }, | ||||||
|     { 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/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" }, | ||||||
|     { 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/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, upload-time = "2025-01-14T10:34:29.167Z" }, | ||||||
|     { 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/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, upload-time = "2025-01-14T10:34:31.702Z" }, | ||||||
|     { 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/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" }, | ||||||
|     { 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/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" }, | ||||||
|     { 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/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" }, | ||||||
|     { 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/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" }, | ||||||
|     { 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/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" }, | ||||||
|     { 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/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" }, | ||||||
|     { 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/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" }, | ||||||
|     { 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/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" }, | ||||||
|     { 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/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" }, | ||||||
|     { 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/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, upload-time = "2025-01-14T10:34:52.297Z" }, | ||||||
|     { 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/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, upload-time = "2025-01-14T10:34:53.489Z" }, | ||||||
|     { 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/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" }, | ||||||
|     { 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/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" }, | ||||||
|     { 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/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" }, | ||||||
|     { 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/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" }, | ||||||
|     { 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/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, |     { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" }, | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "xonsh" | name = "xonsh" | ||||||
| version = "0.19.2" | version = "0.19.2" | ||||||
| source = { registry = "https://pypi.org/simple" } | source = { registry = "https://pypi.org/simple" } | ||||||
| sdist = { url = "https://files.pythonhosted.org/packages/68/4e/56e95a5e607eb3b0da37396f87cde70588efc8ef819ab16f02d5b8378dc4/xonsh-0.19.2.tar.gz", hash = "sha256:cfdd0680d954a2c3aefd6caddcc7143a3d06aa417ed18365a08219bb71b960b0", size = 799960 } | sdist = { url = "https://files.pythonhosted.org/packages/68/4e/56e95a5e607eb3b0da37396f87cde70588efc8ef819ab16f02d5b8378dc4/xonsh-0.19.2.tar.gz", hash = "sha256:cfdd0680d954a2c3aefd6caddcc7143a3d06aa417ed18365a08219bb71b960b0", size = 799960, upload-time = "2025-02-11T17:10:43.563Z" } | ||||||
| wheels = [ | wheels = [ | ||||||
|     { url = "https://files.pythonhosted.org/packages/6c/13/281094759df87b23b3c02dc4a16603ab08ea54d7f6acfeb69f3341137c7a/xonsh-0.19.2-py310-none-any.whl", hash = "sha256:ec7f163fd3a4943782aa34069d4e72793328c916a5975949dbec8536cbfc089b", size = 642301 }, |     { url = "https://files.pythonhosted.org/packages/6c/13/281094759df87b23b3c02dc4a16603ab08ea54d7f6acfeb69f3341137c7a/xonsh-0.19.2-py310-none-any.whl", hash = "sha256:ec7f163fd3a4943782aa34069d4e72793328c916a5975949dbec8536cbfc089b", size = 642301, upload-time = "2025-02-11T17:10:39.244Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286 }, |     { url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286, upload-time = "2025-02-11T17:10:41.678Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386 }, |     { url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386, upload-time = "2025-02-11T17:10:43.688Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873 }, |     { url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873, upload-time = "2025-02-11T17:10:39.297Z" }, | ||||||
|     { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602 }, |     { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602, upload-time = "2025-02-11T17:10:37.004Z" }, | ||||||
| ] | ] | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue