Compare commits
	
		
			35 Commits 
		
	
	
		
			main
			...
			SDOF_pre_s
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								 | 
						1c73c0c0ee | |
| 
							
							
								 | 
						101cd94e89 | |
| 
							
							
								 | 
						3f33ba1cc0 | |
| 
							
							
								 | 
						70f5315506 | |
| 
							
							
								 | 
						496fac04bb | |
| 
							
							
								 | 
						02baeb6a8b | |
| 
							
							
								 | 
						d4ab802e14 | |
| 
							
							
								 | 
						fdeaeef9f7 | |
| 
							
							
								 | 
						41609d1433 | |
| 
							
							
								 | 
						c9068522ed | |
| 
							
							
								 | 
						f3285ea870 | |
| 
							
							
								 | 
						a8caff9077 | |
| 
							
							
								 | 
						31a7e3b3c5 | |
| 
							
							
								 | 
						6163d42424 | |
| 
							
							
								 | 
						4540309296 | |
| 
							
							
								 | 
						4c8fadac85 | |
| 
							
							
								 | 
						252a325bb6 | |
| 
							
							
								 | 
						4621c528ac | |
| 
							
							
								 | 
						ddb299488a | |
| 
							
							
								 | 
						9f3702b64c | |
| 
							
							
								 | 
						2361a5f47f | |
| 
							
							
								 | 
						f2595c45d0 | |
| 
							
							
								 | 
						8d72423bb4 | |
| 
							
							
								 | 
						e3232aed30 | |
| 
							
							
								 | 
						1a591208f5 | |
| 
							
							
								 | 
						1efe5fa893 | |
| 
							
							
								 | 
						9579fa35a5 | |
| 
							
							
								 | 
						e4f5aac290 | |
| 
							
							
								 | 
						f20168b7e8 | |
| 
							
							
								 | 
						993c745729 | |
| 
							
							
								 | 
						844525ed34 | |
| 
							
							
								 | 
						9abd306860 | |
| 
							
							
								 | 
						1d113e3ae5 | |
| 
							
							
								 | 
						c911d224ca | |
| 
							
							
								 | 
						89b16f27de | 
| 
						 | 
					@ -8,70 +8,46 @@ 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@v4
 | 
					        uses: actions/checkout@v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Install latest uv
 | 
					      - name: Setup python
 | 
				
			||||||
        uses: astral-sh/setup-uv@v6
 | 
					        uses: actions/setup-python@v2
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          python-version: '3.11'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Build sdist as tar.gz
 | 
					      - name: Build sdist
 | 
				
			||||||
        run: uv build --sdist --python=3.13
 | 
					        run: python setup.py sdist --formats=zip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Install sdist from .tar.gz
 | 
					      - name: Install sdist from .zips
 | 
				
			||||||
        run: python -m pip install dist/*.tar.gz
 | 
					        run: python -m pip install dist/*.zip
 | 
				
			||||||
 | 
					 | 
				
			||||||
  # ------ 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:
 | 
				
			||||||
| 
						 | 
					@ -83,45 +59,32 @@ jobs:
 | 
				
			||||||
      fail-fast: false
 | 
					      fail-fast: false
 | 
				
			||||||
      matrix:
 | 
					      matrix:
 | 
				
			||||||
        os: [ubuntu-latest]
 | 
					        os: [ubuntu-latest]
 | 
				
			||||||
        python-version: ['3.13']
 | 
					        python: ['3.11']
 | 
				
			||||||
        spawn_backend: [
 | 
					        spawn_backend: [
 | 
				
			||||||
          'trio',
 | 
					          'trio',
 | 
				
			||||||
          # 'mp_spawn',
 | 
					          'mp_spawn',
 | 
				
			||||||
          # 'mp_forkserver',
 | 
					          'mp_forkserver',
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - name: Checkout
 | 
				
			||||||
 | 
					        uses: actions/checkout@v2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: 'Install uv + py-${{ matrix.python-version }}'
 | 
					      - name: Setup python
 | 
				
			||||||
        uses: astral-sh/setup-uv@v6
 | 
					        uses: actions/setup-python@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          python-version: ${{ matrix.python-version }}
 | 
					          python-version: '${{ matrix.python }}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # GH way.. faster?
 | 
					      - name: Install dependencies
 | 
				
			||||||
      # - name: setup-python@v6
 | 
					        run: pip install -U . -r requirements-test.txt -r requirements-docs.txt --upgrade-strategy eager
 | 
				
			||||||
      #   uses: actions/setup-python@v6
 | 
					 | 
				
			||||||
      #   with:
 | 
					 | 
				
			||||||
      #     python-version: '${{ matrix.python-version }}'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # consider caching for speedups?
 | 
					      - name: List dependencies
 | 
				
			||||||
      # https://docs.astral.sh/uv/guides/integration/github/#caching
 | 
					        run: pip list
 | 
				
			||||||
 | 
					 | 
				
			||||||
      - 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: uv run pytest tests/ --spawn-backend=${{ matrix.spawn_backend }} -rsx
 | 
					        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,5 +1,8 @@
 | 
				
			||||||
|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
 | 
				
			||||||
| 
						 | 
					@ -63,13 +66,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -693,11 +689,9 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
..
 | 
					
 | 
				
			||||||
   NOTE, on generating badge links from the UI
 | 
					.. |gh_actions| image:: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fgoodboy%2Ftractor%2Fbadge&style=popout-square
 | 
				
			||||||
   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
 | 
					    :target: https://actions-badge.atrox.dev/goodboy/tractor/goto
 | 
				
			||||||
.. |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,7 +16,6 @@ from tractor import (
 | 
				
			||||||
    ContextCancelled,
 | 
					    ContextCancelled,
 | 
				
			||||||
    MsgStream,
 | 
					    MsgStream,
 | 
				
			||||||
    _testing,
 | 
					    _testing,
 | 
				
			||||||
    trionics,
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
import trio
 | 
					import trio
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
| 
						 | 
					@ -63,8 +62,9 @@ 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,
 | 
				
			||||||
        trionics.collapse_eg(),
 | 
					        trio.open_nursery(
 | 
				
			||||||
        trio.open_nursery() as tn,
 | 
					            strict_exception_groups=False,
 | 
				
			||||||
 | 
					        ) 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,11 +4,6 @@ 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:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,23 +13,19 @@ async def main() -> None:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async with tractor.open_nursery(
 | 
					    async with tractor.open_nursery(
 | 
				
			||||||
        debug_mode=True,
 | 
					        debug_mode=True,
 | 
				
			||||||
        loglevel='devx',
 | 
					    ) as an:
 | 
				
			||||||
        maybe_enable_greenback=True,
 | 
					        assert an
 | 
				
			||||||
        # ^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,9 +24,10 @@ 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
 | 
					
 | 
				
			||||||
    debugger first engages:
 | 
					    The process tree should look as approximately as follows when the debugger
 | 
				
			||||||
 | 
					    first engages:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    python examples/debugging/multi_nested_subactors_bp_forever.py
 | 
					    python examples/debugging/multi_nested_subactors_bp_forever.py
 | 
				
			||||||
    ├─ python -m tractor._child --uid ('spawner1', '7eab8462 ...)
 | 
					    ├─ python -m tractor._child --uid ('spawner1', '7eab8462 ...)
 | 
				
			||||||
| 
						 | 
					@ -36,11 +37,10 @@ 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='devx',
 | 
					        loglevel='warning'
 | 
				
			||||||
        enable_transports=['uds'],
 | 
					 | 
				
			||||||
    ) as n:
 | 
					    ) as n:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # spawn both actors
 | 
					        # spawn both actors
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,35 +0,0 @@
 | 
				
			||||||
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,7 +37,6 @@ 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,11 +33,8 @@ 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,8 +23,9 @@ async def main():
 | 
				
			||||||
            modules=[__name__]
 | 
					            modules=[__name__]
 | 
				
			||||||
        ) as portal_map,
 | 
					        ) as portal_map,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tractor.trionics.collapse_eg(),
 | 
					        trio.open_nursery(
 | 
				
			||||||
        trio.open_nursery() as tn,
 | 
					            strict_exception_groups=False,
 | 
				
			||||||
 | 
					        ) as tn,
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (name, portal) in portal_map.items():
 | 
					        for (name, portal) in portal_map.items():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,85 +0,0 @@
 | 
				
			||||||
from contextlib import (
 | 
					 | 
				
			||||||
    asynccontextmanager as acm,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from functools import partial
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import tractor
 | 
					 | 
				
			||||||
import trio
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
log = tractor.log.get_logger(
 | 
					 | 
				
			||||||
    name=__name__
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
_lock: trio.Lock|None = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@acm
 | 
					 | 
				
			||||||
async def acquire_singleton_lock(
 | 
					 | 
				
			||||||
) -> None:
 | 
					 | 
				
			||||||
    global _lock
 | 
					 | 
				
			||||||
    if _lock is None:
 | 
					 | 
				
			||||||
        log.info('Allocating LOCK')
 | 
					 | 
				
			||||||
        _lock = trio.Lock()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    log.info('TRYING TO LOCK ACQUIRE')
 | 
					 | 
				
			||||||
    async with _lock:
 | 
					 | 
				
			||||||
        log.info('ACQUIRED')
 | 
					 | 
				
			||||||
        yield _lock
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    log.info('RELEASED')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async def hold_lock_forever(
 | 
					 | 
				
			||||||
    task_status=trio.TASK_STATUS_IGNORED
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    async with (
 | 
					 | 
				
			||||||
        tractor.trionics.maybe_raise_from_masking_exc(),
 | 
					 | 
				
			||||||
        acquire_singleton_lock() as lock,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        task_status.started(lock)
 | 
					 | 
				
			||||||
        await trio.sleep_forever()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async def main(
 | 
					 | 
				
			||||||
    ignore_special_cases: bool,
 | 
					 | 
				
			||||||
    loglevel: str = 'info',
 | 
					 | 
				
			||||||
    debug_mode: bool = True,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    async with (
 | 
					 | 
				
			||||||
        trio.open_nursery() as tn,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # tractor.trionics.maybe_raise_from_masking_exc()
 | 
					 | 
				
			||||||
        # ^^^ XXX NOTE, interestingly putting the unmasker
 | 
					 | 
				
			||||||
        # here does not exhibit the same behaviour ??
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        if not ignore_special_cases:
 | 
					 | 
				
			||||||
            from tractor.trionics import _taskc
 | 
					 | 
				
			||||||
            _taskc._mask_cases.clear()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        _lock = await tn.start(
 | 
					 | 
				
			||||||
            hold_lock_forever,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        with trio.move_on_after(0.2):
 | 
					 | 
				
			||||||
            await tn.start(
 | 
					 | 
				
			||||||
                hold_lock_forever,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        tn.cancel_scope.cancel()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# XXX, manual test as script
 | 
					 | 
				
			||||||
if __name__ == '__main__':
 | 
					 | 
				
			||||||
    tractor.log.get_console_log(level='info')
 | 
					 | 
				
			||||||
    for case in [True, False]:
 | 
					 | 
				
			||||||
        log.info(
 | 
					 | 
				
			||||||
            f'\n'
 | 
					 | 
				
			||||||
            f'------ RUNNING SCRIPT TRIAL ------\n'
 | 
					 | 
				
			||||||
            f'ignore_special_cases: {case!r}\n'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        trio.run(partial(
 | 
					 | 
				
			||||||
            main,
 | 
					 | 
				
			||||||
            ignore_special_cases=case,
 | 
					 | 
				
			||||||
            loglevel='info',
 | 
					 | 
				
			||||||
        ))
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,195 +0,0 @@
 | 
				
			||||||
from contextlib import (
 | 
					 | 
				
			||||||
    contextmanager as cm,
 | 
					 | 
				
			||||||
    # TODO, any diff in async case(s)??
 | 
					 | 
				
			||||||
    # asynccontextmanager as acm,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from functools import partial
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import tractor
 | 
					 | 
				
			||||||
import trio
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
log = tractor.log.get_logger(
 | 
					 | 
				
			||||||
    name=__name__
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@cm
 | 
					 | 
				
			||||||
def teardown_on_exc(
 | 
					 | 
				
			||||||
    raise_from_handler: bool = False,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    You could also have a teardown handler which catches any exc and
 | 
					 | 
				
			||||||
    does some required teardown. In this case the problem is
 | 
					 | 
				
			||||||
    compounded UNLESS you ensure the handler's scope is OUTSIDE the
 | 
					 | 
				
			||||||
    `ux.aclose()`.. that is in the caller's enclosing scope.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        yield
 | 
					 | 
				
			||||||
    except BaseException as _berr:
 | 
					 | 
				
			||||||
        berr = _berr
 | 
					 | 
				
			||||||
        log.exception(
 | 
					 | 
				
			||||||
            f'Handling termination teardown in child due to,\n'
 | 
					 | 
				
			||||||
            f'{berr!r}\n'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        if raise_from_handler:
 | 
					 | 
				
			||||||
            # XXX teardown ops XXX
 | 
					 | 
				
			||||||
            # on termination these steps say need to be run to
 | 
					 | 
				
			||||||
            # ensure wider system consistency (like the state of
 | 
					 | 
				
			||||||
            # remote connections/services).
 | 
					 | 
				
			||||||
            #
 | 
					 | 
				
			||||||
            # HOWEVER, any bug in this teardown code is also
 | 
					 | 
				
			||||||
            # masked by the `tx.aclose()`!
 | 
					 | 
				
			||||||
            # this is also true if `_tn.cancel_scope` is
 | 
					 | 
				
			||||||
            # `.cancel_called` by the parent in a graceful
 | 
					 | 
				
			||||||
            # request case..
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # simulate a bug in teardown handler.
 | 
					 | 
				
			||||||
            raise RuntimeError(
 | 
					 | 
				
			||||||
                'woopsie teardown bug!'
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        raise  # no teardown bug.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async def finite_stream_to_rent(
 | 
					 | 
				
			||||||
    tx: trio.abc.SendChannel,
 | 
					 | 
				
			||||||
    child_errors_mid_stream: bool,
 | 
					 | 
				
			||||||
    raise_unmasked: bool,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    task_status: trio.TaskStatus[
 | 
					 | 
				
			||||||
        trio.CancelScope,
 | 
					 | 
				
			||||||
    ] = trio.TASK_STATUS_IGNORED,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    async with (
 | 
					 | 
				
			||||||
        # XXX without this unmasker the mid-streaming RTE is never
 | 
					 | 
				
			||||||
        # reported since it is masked by the `tx.aclose()`
 | 
					 | 
				
			||||||
        # call which in turn raises `Cancelled`!
 | 
					 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        # NOTE, this is WITHOUT doing any exception handling
 | 
					 | 
				
			||||||
        # inside the child  task!
 | 
					 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        # TODO, uncomment next LoC to see the supprsessed beg[RTE]!
 | 
					 | 
				
			||||||
        tractor.trionics.maybe_raise_from_masking_exc(
 | 
					 | 
				
			||||||
            raise_unmasked=raise_unmasked,
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        tx as tx,  # .aclose() is the guilty masker chkpt!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # XXX, this ONLY matters in the
 | 
					 | 
				
			||||||
        # `child_errors_mid_stream=False` case oddly!?
 | 
					 | 
				
			||||||
        # THAT IS, if no tn is opened in that case then the
 | 
					 | 
				
			||||||
        # test will not fail; it raises the RTE correctly?
 | 
					 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        # -> so it seems this new scope somehow affects the form of
 | 
					 | 
				
			||||||
        #    eventual in the parent EG?
 | 
					 | 
				
			||||||
        tractor.trionics.maybe_open_nursery(
 | 
					 | 
				
			||||||
            nursery=(
 | 
					 | 
				
			||||||
                None
 | 
					 | 
				
			||||||
                if not child_errors_mid_stream
 | 
					 | 
				
			||||||
                else True
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ) as _tn,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        # pass our scope back to parent for supervision\
 | 
					 | 
				
			||||||
        # control.
 | 
					 | 
				
			||||||
        cs: trio.CancelScope|None = (
 | 
					 | 
				
			||||||
            None
 | 
					 | 
				
			||||||
            if _tn is True
 | 
					 | 
				
			||||||
            else _tn.cancel_scope
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        task_status.started(cs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        with teardown_on_exc(
 | 
					 | 
				
			||||||
            raise_from_handler=not child_errors_mid_stream,
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            for i in range(100):
 | 
					 | 
				
			||||||
                log.debug(
 | 
					 | 
				
			||||||
                    f'Child tx {i!r}\n'
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                if (
 | 
					 | 
				
			||||||
                    child_errors_mid_stream
 | 
					 | 
				
			||||||
                    and
 | 
					 | 
				
			||||||
                    i == 66
 | 
					 | 
				
			||||||
                ):
 | 
					 | 
				
			||||||
                    # oh wait but WOOPS there's a bug
 | 
					 | 
				
			||||||
                    # in that teardown code!?
 | 
					 | 
				
			||||||
                    raise RuntimeError(
 | 
					 | 
				
			||||||
                        'woopsie, a mid-streaming bug!?'
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                await tx.send(i)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async def main(
 | 
					 | 
				
			||||||
    # TODO! toggle this for the 2 cases!
 | 
					 | 
				
			||||||
    # 1. child errors mid-stream while parent is also requesting
 | 
					 | 
				
			||||||
    #   (graceful) cancel of that child streamer.
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # 2. child contains a teardown handler which contains a
 | 
					 | 
				
			||||||
    #   bug and raises.
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    child_errors_mid_stream: bool,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    raise_unmasked: bool = False,
 | 
					 | 
				
			||||||
    loglevel: str = 'info',
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    tractor.log.get_console_log(level=loglevel)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # the `.aclose()` being checkpoints on these
 | 
					 | 
				
			||||||
    # is the source of the problem..
 | 
					 | 
				
			||||||
    tx, rx = trio.open_memory_channel(1)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async with (
 | 
					 | 
				
			||||||
        tractor.trionics.collapse_eg(),
 | 
					 | 
				
			||||||
        trio.open_nursery() as tn,
 | 
					 | 
				
			||||||
        rx as rx,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        _child_cs = await tn.start(
 | 
					 | 
				
			||||||
            partial(
 | 
					 | 
				
			||||||
                finite_stream_to_rent,
 | 
					 | 
				
			||||||
                child_errors_mid_stream=child_errors_mid_stream,
 | 
					 | 
				
			||||||
                raise_unmasked=raise_unmasked,
 | 
					 | 
				
			||||||
                tx=tx,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        async for msg in rx:
 | 
					 | 
				
			||||||
            log.debug(
 | 
					 | 
				
			||||||
                f'Rent rx {msg!r}\n'
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # simulate some external cancellation
 | 
					 | 
				
			||||||
            # request **JUST BEFORE** the child errors.
 | 
					 | 
				
			||||||
            if msg == 65:
 | 
					 | 
				
			||||||
                log.cancel(
 | 
					 | 
				
			||||||
                    f'Cancelling parent on,\n'
 | 
					 | 
				
			||||||
                    f'msg={msg}\n'
 | 
					 | 
				
			||||||
                    f'\n'
 | 
					 | 
				
			||||||
                    f'Simulates OOB cancel request!\n'
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                tn.cancel_scope.cancel()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# XXX, manual test as script
 | 
					 | 
				
			||||||
if __name__ == '__main__':
 | 
					 | 
				
			||||||
    tractor.log.get_console_log(level='info')
 | 
					 | 
				
			||||||
    for case in [True, False]:
 | 
					 | 
				
			||||||
        log.info(
 | 
					 | 
				
			||||||
            f'\n'
 | 
					 | 
				
			||||||
            f'------ RUNNING SCRIPT TRIAL ------\n'
 | 
					 | 
				
			||||||
            f'child_errors_midstream: {case!r}\n'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            trio.run(partial(
 | 
					 | 
				
			||||||
                main,
 | 
					 | 
				
			||||||
                child_errors_mid_stream=case,
 | 
					 | 
				
			||||||
                # raise_unmasked=True,
 | 
					 | 
				
			||||||
                loglevel='info',
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
        except Exception as _exc:
 | 
					 | 
				
			||||||
            exc = _exc
 | 
					 | 
				
			||||||
            log.exception(
 | 
					 | 
				
			||||||
                'Should have raised an RTE or Cancelled?\n'
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            breakpoint()
 | 
					 | 
				
			||||||
| 
						 | 
					@ -61,9 +61,6 @@ 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",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,22 +6,21 @@ from __future__ import annotations
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import subprocess
 | 
					import subprocess
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					import random
 | 
				
			||||||
import signal
 | 
					import signal
 | 
				
			||||||
import platform
 | 
					import platform
 | 
				
			||||||
import time
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
 | 
					import tractor
 | 
				
			||||||
from tractor._testing import (
 | 
					from tractor._testing import (
 | 
				
			||||||
    examples_dir as examples_dir,
 | 
					    examples_dir as examples_dir,
 | 
				
			||||||
    tractor_test as tractor_test,
 | 
					    tractor_test as tractor_test,
 | 
				
			||||||
    expect_ctxc as expect_ctxc,
 | 
					    expect_ctxc as expect_ctxc,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pytest_plugins: list[str] = [
 | 
					# TODO: include wtv plugin(s) we build in `._testing.pytest`?
 | 
				
			||||||
    'pytester',
 | 
					pytest_plugins = ['pytester']
 | 
				
			||||||
    'tractor._testing.pytest',
 | 
					 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives
 | 
					# Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives
 | 
				
			||||||
if platform.system() == 'Windows':
 | 
					if platform.system() == 'Windows':
 | 
				
			||||||
| 
						 | 
					@ -49,9 +48,6 @@ no_windows = pytest.mark.skipif(
 | 
				
			||||||
def pytest_addoption(
 | 
					def pytest_addoption(
 | 
				
			||||||
    parser: pytest.Parser,
 | 
					    parser: pytest.Parser,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    # ?TODO? should this be exposed from our `._testing.pytest`
 | 
					 | 
				
			||||||
    # plugin or should we make it more explicit with `--tl` for
 | 
					 | 
				
			||||||
    # tractor logging like we do in other client projects?
 | 
					 | 
				
			||||||
    parser.addoption(
 | 
					    parser.addoption(
 | 
				
			||||||
        "--ll",
 | 
					        "--ll",
 | 
				
			||||||
        action="store",
 | 
					        action="store",
 | 
				
			||||||
| 
						 | 
					@ -59,10 +55,54 @@ def pytest_addoption(
 | 
				
			||||||
        default='ERROR', help="logging level to set when testing"
 | 
					        default='ERROR', help="logging level to set when testing"
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parser.addoption(
 | 
				
			||||||
 | 
					        "--spawn-backend",
 | 
				
			||||||
 | 
					        action="store",
 | 
				
			||||||
 | 
					        dest='spawn_backend',
 | 
				
			||||||
 | 
					        default='trio',
 | 
				
			||||||
 | 
					        help="Processing spawning backend to use for test run",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    parser.addoption(
 | 
				
			||||||
 | 
					        "--tpdb",
 | 
				
			||||||
 | 
					        "--debug-mode",
 | 
				
			||||||
 | 
					        action="store_true",
 | 
				
			||||||
 | 
					        dest='tractor_debug_mode',
 | 
				
			||||||
 | 
					        # default=False,
 | 
				
			||||||
 | 
					        help=(
 | 
				
			||||||
 | 
					            'Enable a flag that can be used by tests to to set the '
 | 
				
			||||||
 | 
					            '`debug_mode: bool` for engaging the internal '
 | 
				
			||||||
 | 
					            'multi-proc debugger sys.'
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # provide which IPC transport protocols opting-in test suites
 | 
				
			||||||
 | 
					    # should accumulatively run against.
 | 
				
			||||||
 | 
					    parser.addoption(
 | 
				
			||||||
 | 
					        "--tpt-proto",
 | 
				
			||||||
 | 
					        nargs='+',  # accumulate-multiple-args
 | 
				
			||||||
 | 
					        action="store",
 | 
				
			||||||
 | 
					        dest='tpt_protos',
 | 
				
			||||||
 | 
					        default=['tcp'],
 | 
				
			||||||
 | 
					        help="Transport protocol to use under the `tractor.ipc.Channel`",
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pytest_configure(config):
 | 
				
			||||||
 | 
					    backend = config.option.spawn_backend
 | 
				
			||||||
 | 
					    tractor._spawn.try_set_start_method(backend)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.fixture(scope='session')
 | 
				
			||||||
 | 
					def debug_mode(request) -> bool:
 | 
				
			||||||
 | 
					    debug_mode: bool = request.config.option.tractor_debug_mode
 | 
				
			||||||
 | 
					    # if debug_mode:
 | 
				
			||||||
 | 
					    #     breakpoint()
 | 
				
			||||||
 | 
					    return debug_mode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.fixture(scope='session', autouse=True)
 | 
					@pytest.fixture(scope='session', autouse=True)
 | 
				
			||||||
def loglevel(request):
 | 
					def loglevel(request):
 | 
				
			||||||
    import tractor
 | 
					 | 
				
			||||||
    orig = tractor.log._default_loglevel
 | 
					    orig = tractor.log._default_loglevel
 | 
				
			||||||
    level = tractor.log._default_loglevel = request.config.option.loglevel
 | 
					    level = tractor.log._default_loglevel = request.config.option.loglevel
 | 
				
			||||||
    tractor.log.get_console_log(level)
 | 
					    tractor.log.get_console_log(level)
 | 
				
			||||||
| 
						 | 
					@ -70,6 +110,49 @@ def loglevel(request):
 | 
				
			||||||
    tractor.log._default_loglevel = orig
 | 
					    tractor.log._default_loglevel = orig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.fixture(scope='session')
 | 
				
			||||||
 | 
					def spawn_backend(request) -> str:
 | 
				
			||||||
 | 
					    return request.config.option.spawn_backend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.fixture(scope='session')
 | 
				
			||||||
 | 
					def tpt_protos(request) -> list[str]:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # allow quoting on CLI
 | 
				
			||||||
 | 
					    proto_keys: list[str] = [
 | 
				
			||||||
 | 
					        proto_key.replace('"', '').replace("'", "")
 | 
				
			||||||
 | 
					        for proto_key in request.config.option.tpt_protos
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # ?TODO, eventually support multiple protos per test-sesh?
 | 
				
			||||||
 | 
					    if len(proto_keys) > 1:
 | 
				
			||||||
 | 
					        pytest.fail(
 | 
				
			||||||
 | 
					            'We only support one `--tpt-proto <key>` atm!\n'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # XXX ensure we support the protocol by name via lookup!
 | 
				
			||||||
 | 
					    for proto_key in proto_keys:
 | 
				
			||||||
 | 
					        addr_type = tractor._addr._address_types[proto_key]
 | 
				
			||||||
 | 
					        assert addr_type.proto_key == proto_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    yield proto_keys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.fixture(
 | 
				
			||||||
 | 
					    scope='session',
 | 
				
			||||||
 | 
					    autouse=True,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					def tpt_proto(
 | 
				
			||||||
 | 
					    tpt_protos: list[str],
 | 
				
			||||||
 | 
					) -> str:
 | 
				
			||||||
 | 
					    proto_key: str = tpt_protos[0]
 | 
				
			||||||
 | 
					    from tractor import _state
 | 
				
			||||||
 | 
					    if _state._def_tpt_proto != proto_key:
 | 
				
			||||||
 | 
					        _state._def_tpt_proto = proto_key
 | 
				
			||||||
 | 
					    # breakpoint()
 | 
				
			||||||
 | 
					    yield proto_key
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_ci_env: bool = os.environ.get('CI', False)
 | 
					_ci_env: bool = os.environ.get('CI', False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,6 +165,80 @@ def ci_env() -> bool:
 | 
				
			||||||
    return _ci_env
 | 
					    return _ci_env
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: also move this to `._testing` for now?
 | 
				
			||||||
 | 
					# -[ ] possibly generalize and re-use for multi-tree spawning
 | 
				
			||||||
 | 
					#    along with the new stuff for multi-addrs?
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# choose random port at import time
 | 
				
			||||||
 | 
					_rando_port: str = random.randint(1000, 9999)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@pytest.fixture(scope='session')
 | 
				
			||||||
 | 
					def reg_addr(
 | 
				
			||||||
 | 
					    tpt_proto: str,
 | 
				
			||||||
 | 
					) -> tuple[str, int|str]:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # globally override the runtime to the per-test-session-dynamic
 | 
				
			||||||
 | 
					    # addr so that all tests never conflict with any other actor
 | 
				
			||||||
 | 
					    # tree using the default.
 | 
				
			||||||
 | 
					    from tractor import (
 | 
				
			||||||
 | 
					        _addr,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    addr_type = _addr._address_types[tpt_proto]
 | 
				
			||||||
 | 
					    def_reg_addr: tuple[str, int] = _addr._default_lo_addrs[tpt_proto]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testrun_reg_addr: tuple[str, int]
 | 
				
			||||||
 | 
					    match tpt_proto:
 | 
				
			||||||
 | 
					        case 'tcp':
 | 
				
			||||||
 | 
					            testrun_reg_addr = (
 | 
				
			||||||
 | 
					                addr_type.def_bindspace,
 | 
				
			||||||
 | 
					                _rando_port,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # NOTE, file-name uniqueness (no-collisions) will be based on
 | 
				
			||||||
 | 
					        # the runtime-directory and root (pytest-proc's) pid.
 | 
				
			||||||
 | 
					        case 'uds':
 | 
				
			||||||
 | 
					            testrun_reg_addr = addr_type.get_random().unwrap()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert def_reg_addr != testrun_reg_addr
 | 
				
			||||||
 | 
					    return testrun_reg_addr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def pytest_generate_tests(metafunc):
 | 
				
			||||||
 | 
					    spawn_backend: str = metafunc.config.option.spawn_backend
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if not spawn_backend:
 | 
				
			||||||
 | 
					        # XXX some weird windows bug with `pytest`?
 | 
				
			||||||
 | 
					        spawn_backend = 'trio'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO: maybe just use the literal `._spawn.SpawnMethodKey`?
 | 
				
			||||||
 | 
					    assert spawn_backend in (
 | 
				
			||||||
 | 
					        'mp_spawn',
 | 
				
			||||||
 | 
					        'mp_forkserver',
 | 
				
			||||||
 | 
					        'trio',
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # NOTE: used-to-be-used-to dyanmically parametrize tests for when
 | 
				
			||||||
 | 
					    # you just passed --spawn-backend=`mp` on the cli, but now we expect
 | 
				
			||||||
 | 
					    # that cli input to be manually specified, BUT, maybe we'll do
 | 
				
			||||||
 | 
					    # something like this again in the future?
 | 
				
			||||||
 | 
					    if 'start_method' in metafunc.fixturenames:
 | 
				
			||||||
 | 
					        metafunc.parametrize(
 | 
				
			||||||
 | 
					            "start_method",
 | 
				
			||||||
 | 
					            [spawn_backend],
 | 
				
			||||||
 | 
					            scope='module',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # TODO, parametrize any `tpt_proto: str` declaring tests!
 | 
				
			||||||
 | 
					    # proto_tpts: list[str] = metafunc.config.option.proto_tpts
 | 
				
			||||||
 | 
					    # if 'tpt_proto' in metafunc.fixturenames:
 | 
				
			||||||
 | 
					    #     metafunc.parametrize(
 | 
				
			||||||
 | 
					    #         'tpt_proto',
 | 
				
			||||||
 | 
					    #         proto_tpts,  # TODO, double check this list usage!
 | 
				
			||||||
 | 
					    #         scope='module',
 | 
				
			||||||
 | 
					    #     )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def sig_prog(
 | 
					def sig_prog(
 | 
				
			||||||
    proc: subprocess.Popen,
 | 
					    proc: subprocess.Popen,
 | 
				
			||||||
    sig: int,
 | 
					    sig: int,
 | 
				
			||||||
| 
						 | 
					@ -103,7 +260,7 @@ def sig_prog(
 | 
				
			||||||
def daemon(
 | 
					def daemon(
 | 
				
			||||||
    debug_mode: bool,
 | 
					    debug_mode: bool,
 | 
				
			||||||
    loglevel: str,
 | 
					    loglevel: str,
 | 
				
			||||||
    testdir: pytest.Pytester,
 | 
					    testdir,
 | 
				
			||||||
    reg_addr: tuple[str, int],
 | 
					    reg_addr: tuple[str, int],
 | 
				
			||||||
    tpt_proto: str,
 | 
					    tpt_proto: str,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,11 +2,9 @@
 | 
				
			||||||
`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
 | 
				
			||||||
| 
						 | 
					@ -18,7 +16,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,
 | 
				
			||||||
| 
						 | 
					@ -28,22 +26,14 @@ 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: str,
 | 
					    start_method,
 | 
				
			||||||
    testdir: pytest.Pytester,
 | 
					    testdir: pytest.Pytester,
 | 
				
			||||||
    reg_addr: tuple[str, int],
 | 
					    reg_addr: tuple[str, int],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> PexpectSpawner:
 | 
					) -> Callable[[str], None]:
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    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.
 | 
				
			||||||
| 
						 | 
					@ -69,7 +59,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(
 | 
				
			||||||
| 
						 | 
					@ -83,7 +73,7 @@ def spawn(
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # such that test-dep can pass input script name.
 | 
					    # such that test-dep can pass input script name.
 | 
				
			||||||
    return _spawn  # the `PexpectSpawner`, type alias.
 | 
					    return _spawn
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.fixture(
 | 
					@pytest.fixture(
 | 
				
			||||||
| 
						 | 
					@ -121,7 +111,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,23 +1,19 @@
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
That "native" debug mode better work!
 | 
					That "native" debug mode better work!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
All these tests can be understood (somewhat) by running the
 | 
					All these tests can be understood (somewhat) by running the equivalent
 | 
				
			||||||
equivalent `examples/debugging/` scripts manually.
 | 
					`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 (
 | 
				
			||||||
| 
						 | 
					@ -38,9 +34,6 @@ 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?
 | 
				
			||||||
| 
						 | 
					@ -317,6 +310,7 @@ 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',
 | 
				
			||||||
| 
						 | 
					@ -534,7 +528,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
 | 
				
			||||||
| 
						 | 
					@ -925,7 +919,6 @@ def test_post_mortem_api(
 | 
				
			||||||
            "<Task 'name_error'",
 | 
					            "<Task 'name_error'",
 | 
				
			||||||
            "NameError",
 | 
					            "NameError",
 | 
				
			||||||
            "('child'",
 | 
					            "('child'",
 | 
				
			||||||
            'getattr(doggypants)',  # exc-LoC
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    if ctlc:
 | 
					    if ctlc:
 | 
				
			||||||
| 
						 | 
					@ -942,8 +935,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:
 | 
				
			||||||
| 
						 | 
					@ -961,10 +954,6 @@ 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:
 | 
				
			||||||
| 
						 | 
					@ -1074,136 +1063,6 @@ 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,16 +13,9 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -31,19 +24,14 @@ 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: PexpectSpawner,
 | 
					    spawn,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Verify the `tractor.pause()/.post_mortem()` API works inside an
 | 
					    Verify the `tractor.pause()/.post_mortem()` API works inside an
 | 
				
			||||||
| 
						 | 
					@ -121,11 +109,9 @@ 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(
 | 
				
			||||||
| 
						 | 
					@ -140,7 +126,7 @@ def test_shield_pause(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_breakpoint_hook_restored(
 | 
					def test_breakpoint_hook_restored(
 | 
				
			||||||
    spawn: PexpectSpawner,
 | 
					    spawn,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Ensures our actor runtime sets a custom `breakpoint()` hook
 | 
					    Ensures our actor runtime sets a custom `breakpoint()` hook
 | 
				
			||||||
| 
						 | 
					@ -154,22 +140,16 @@ def test_breakpoint_hook_restored(
 | 
				
			||||||
    child = spawn('restore_builtin_breakpoint')
 | 
					    child = spawn('restore_builtin_breakpoint')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    child.expect(PROMPT)
 | 
					    child.expect(PROMPT)
 | 
				
			||||||
    try:
 | 
					    assert_before(
 | 
				
			||||||
        assert_before(
 | 
					        child,
 | 
				
			||||||
            child,
 | 
					        [
 | 
				
			||||||
            [
 | 
					            _pause_msg,
 | 
				
			||||||
                _pause_msg,
 | 
					            "<Task '__main__.main'",
 | 
				
			||||||
                "<Task '__main__.main'",
 | 
					            "('root'",
 | 
				
			||||||
                "('root'",
 | 
					            "first bp, tractor hook set",
 | 
				
			||||||
                "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,
 | 
				
			||||||
| 
						 | 
					@ -190,117 +170,3 @@ 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
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,114 +0,0 @@
 | 
				
			||||||
'''
 | 
					 | 
				
			||||||
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)
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,95 +0,0 @@
 | 
				
			||||||
'''
 | 
					 | 
				
			||||||
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)
 | 
					 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@ def test_basic_ipc_server(
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            assert server._no_more_peers.is_set()
 | 
					            assert server._no_more_peers.is_set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            eps: list[ipc._server.Endpoint] = await server.listen_on(
 | 
					            eps: list[ipc.IPCEndpoint] = await server.listen_on(
 | 
				
			||||||
                accept_addrs=[rando_addr],
 | 
					                accept_addrs=[rando_addr],
 | 
				
			||||||
                stream_handler_nursery=None,
 | 
					                stream_handler_nursery=None,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -313,8 +313,9 @@ 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?
 | 
				
			||||||
        tractor.trionics.collapse_eg(),
 | 
					        trio.open_nursery(
 | 
				
			||||||
        trio.open_nursery() as tn,
 | 
					            strict_exception_groups=False,
 | 
				
			||||||
 | 
					        ) 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,10 +236,7 @@ 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 (
 | 
					    with trio.move_on_after(1) as cancel_scope:
 | 
				
			||||||
        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',
 | 
				
			||||||
| 
						 | 
					@ -287,32 +284,20 @@ async def test_cancel_infinite_streamer(start_method):
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@tractor_test
 | 
					@tractor_test
 | 
				
			||||||
async def test_some_cancels_all(
 | 
					async def test_some_cancels_all(num_actors_and_errs, start_method, loglevel):
 | 
				
			||||||
    num_actors_and_errs: tuple,
 | 
					    """Verify a subset of failed subactors causes all others in
 | 
				
			||||||
    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 an:
 | 
					        async with tractor.open_nursery() as n:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # 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 an.start_actor(
 | 
					                dactor_portals.append(await n.start_actor(
 | 
				
			||||||
                    f'deamon_{i}',
 | 
					                    f'deamon_{i}',
 | 
				
			||||||
                    enable_modules=[__name__],
 | 
					                    enable_modules=[__name__],
 | 
				
			||||||
                ))
 | 
					                ))
 | 
				
			||||||
| 
						 | 
					@ -322,7 +307,7 @@ async def test_some_cancels_all(
 | 
				
			||||||
            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 an.run_in_actor(
 | 
					                    await n.run_in_actor(
 | 
				
			||||||
                        func,
 | 
					                        func,
 | 
				
			||||||
                        name=f'actor_{i}',
 | 
					                        name=f'actor_{i}',
 | 
				
			||||||
                        **kwargs
 | 
					                        **kwargs
 | 
				
			||||||
| 
						 | 
					@ -352,8 +337,7 @@ async def test_some_cancels_all(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # 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:
 | 
				
			||||||
| 
						 | 
					@ -364,8 +348,8 @@ async def test_some_cancels_all(
 | 
				
			||||||
        elif isinstance(err, tractor.RemoteActorError):
 | 
					        elif isinstance(err, tractor.RemoteActorError):
 | 
				
			||||||
            assert err.boxed_type == err_type
 | 
					            assert err.boxed_type == err_type
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert an.cancelled is True
 | 
					        assert n.cancelled is True
 | 
				
			||||||
        assert not an._children
 | 
					        assert not n._children
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        pytest.fail("Should have gotten a remote assertion error?")
 | 
					        pytest.fail("Should have gotten a remote assertion error?")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -535,15 +519,10 @@ 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 (
 | 
					            async with trio.open_nursery(
 | 
				
			||||||
 | 
					                strict_exception_groups=False,
 | 
				
			||||||
                # XXX ?TODO? why no work!?
 | 
					            ) as n:
 | 
				
			||||||
                # tractor.trionics.collapse_eg(),
 | 
					                await n.start(spawn_and_sleep_forever)
 | 
				
			||||||
                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)
 | 
				
			||||||
| 
						 | 
					@ -554,123 +533,38 @@ 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_sub_with_sync_blocking_task():
 | 
					async def spawn():
 | 
				
			||||||
    async with tractor.open_nursery() as an:
 | 
					    async with tractor.open_nursery() as tn:
 | 
				
			||||||
        print('starting sync blocking subactor..\n')
 | 
					        await tn.run_in_actor(
 | 
				
			||||||
        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: str,
 | 
					    loglevel,
 | 
				
			||||||
    start_method: str,
 | 
					    start_method,
 | 
				
			||||||
    spawn_backend: str,
 | 
					    spawn_backend,
 | 
				
			||||||
    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):
 | 
				
			||||||
        # XXX BIG TODO NOTE XXX
 | 
					            async with tractor.open_nursery() as tn:
 | 
				
			||||||
        #
 | 
					                await tn.run_in_actor(
 | 
				
			||||||
        # it seems there's a strange race that can happen
 | 
					                    spawn,
 | 
				
			||||||
        # where where the fail-after will trigger outer scope
 | 
					                    name='spawn',
 | 
				
			||||||
        # .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,10 +117,9 @@ async def open_actor_local_nursery(
 | 
				
			||||||
    ctx: tractor.Context,
 | 
					    ctx: tractor.Context,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    global _nursery
 | 
					    global _nursery
 | 
				
			||||||
    async with (
 | 
					    async with trio.open_nursery(
 | 
				
			||||||
        tractor.trionics.collapse_eg(),
 | 
					        strict_exception_groups=False,
 | 
				
			||||||
        trio.open_nursery() as tn
 | 
					    ) as tn:
 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        _nursery = tn
 | 
					        _nursery = tn
 | 
				
			||||||
        await ctx.started()
 | 
					        await ctx.started()
 | 
				
			||||||
        await trio.sleep(10)
 | 
					        await trio.sleep(10)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,24 +13,26 @@ 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(3):
 | 
					        with trio.fail_after(1):
 | 
				
			||||||
            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='cancel',
 | 
					                    loglevel='info',
 | 
				
			||||||
                    debug_mode=False,
 | 
					                    # debug_mode=True,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                ) as portals,
 | 
					                ) as portals,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                gather_contexts(mngrs=()),
 | 
					                gather_contexts(
 | 
				
			||||||
 | 
					                    # 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()
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                # should fail before this?
 | 
					                assert 0
 | 
				
			||||||
                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.trionics import is_multi_cancelled
 | 
					            from tractor._exceptions import is_multi_cancelled
 | 
				
			||||||
            assert is_multi_cancelled(beg)
 | 
					            assert is_multi_cancelled(beg)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        trio.run(main)
 | 
					        trio.run(main)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,6 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -194,10 +193,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 (
 | 
					                    async with trio.open_nursery(
 | 
				
			||||||
                        collapse_eg(),
 | 
					                        strict_exception_groups=False,
 | 
				
			||||||
                        trio.open_nursery() as trion,
 | 
					                    ) as trion:
 | 
				
			||||||
                    ):
 | 
					
 | 
				
			||||||
                        portals = {}
 | 
					                        portals = {}
 | 
				
			||||||
                        for i in range(3):
 | 
					                        for i in range(3):
 | 
				
			||||||
                            name = f'a{i}'
 | 
					                            name = f'a{i}'
 | 
				
			||||||
| 
						 | 
					@ -339,12 +338,11 @@ 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 (
 | 
					                            async with trio.open_nursery(
 | 
				
			||||||
                                collapse_eg(),
 | 
					                                strict_exception_groups=False,
 | 
				
			||||||
                                trio.open_nursery() as tn,
 | 
					                            ) as n:
 | 
				
			||||||
                            ):
 | 
					                                n.start_soon(streamer, agen1)
 | 
				
			||||||
                                tn.start_soon(streamer, agen1)
 | 
					                                n.start_soon(cancel, use_signal, .5)
 | 
				
			||||||
                                tn.start_soon(cancel, use_signal, .5)
 | 
					 | 
				
			||||||
                                try:
 | 
					                                try:
 | 
				
			||||||
                                    await streamer(agen2)
 | 
					                                    await streamer(agen2)
 | 
				
			||||||
                                finally:
 | 
					                                finally:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -95,7 +95,6 @@ def run_example_in_subproc(
 | 
				
			||||||
            and 'integration' not in p[0]
 | 
					            and 'integration' not in p[0]
 | 
				
			||||||
            and 'advanced_faults' not in p[0]
 | 
					            and 'advanced_faults' not in p[0]
 | 
				
			||||||
            and 'multihost' not in p[0]
 | 
					            and 'multihost' not in p[0]
 | 
				
			||||||
            and 'trio' not in p[0]
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    ids=lambda t: t[1],
 | 
					    ids=lambda t: t[1],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -234,8 +234,10 @@ async def trio_ctx(
 | 
				
			||||||
    with trio.fail_after(1 + delay):
 | 
					    with trio.fail_after(1 + delay):
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            async with (
 | 
					            async with (
 | 
				
			||||||
                tractor.trionics.collapse_eg(),
 | 
					                trio.open_nursery(
 | 
				
			||||||
                trio.open_nursery() as tn,
 | 
					                    # TODO, for new `trio` / py3.13
 | 
				
			||||||
 | 
					                    # 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),
 | 
				
			||||||
| 
						 | 
					@ -571,16 +573,14 @@ def test_basic_interloop_channel_stream(
 | 
				
			||||||
    fan_out: bool,
 | 
					    fan_out: bool,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    async def main():
 | 
					    async def main():
 | 
				
			||||||
        # TODO, figure out min timeout here!
 | 
					        async with tractor.open_nursery() as an:
 | 
				
			||||||
        with trio.fail_after(6):
 | 
					            portal = await an.run_in_actor(
 | 
				
			||||||
            async with tractor.open_nursery() as an:
 | 
					                stream_from_aio,
 | 
				
			||||||
                portal = await an.run_in_actor(
 | 
					                infect_asyncio=True,
 | 
				
			||||||
                    stream_from_aio,
 | 
					                fan_out=fan_out,
 | 
				
			||||||
                    infect_asyncio=True,
 | 
					            )
 | 
				
			||||||
                    fan_out=fan_out,
 | 
					            # should raise RAE diectly
 | 
				
			||||||
                )
 | 
					            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,108 +1088,6 @@ 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:
 | 
				
			||||||
| 
						 | 
					@ -1203,7 +1101,7 @@ def test_aio_side_raises_before_started(
 | 
				
			||||||
#    => 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():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,10 +24,14 @@ from tractor._testing import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# XXX TODO cases:
 | 
					# XXX TODO cases:
 | 
				
			||||||
 | 
					# - [ ] peer cancelled itself - so other peers should
 | 
				
			||||||
 | 
					#   get errors reflecting that the peer was itself the .canceller?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# - [x] WE cancelled the peer and thus should not see any raised
 | 
					# - [x] WE cancelled the peer and thus should not see any raised
 | 
				
			||||||
#   `ContextCancelled` as it should be reaped silently?
 | 
					#   `ContextCancelled` as it should be reaped silently?
 | 
				
			||||||
#   => pretty sure `test_context_stream_semantics::test_caller_cancels()`
 | 
					#   => pretty sure `test_context_stream_semantics::test_caller_cancels()`
 | 
				
			||||||
#      already covers this case?
 | 
					#      already covers this case?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# - [x] INTER-PEER: some arbitrary remote peer cancels via
 | 
					# - [x] INTER-PEER: some arbitrary remote peer cancels via
 | 
				
			||||||
#   Portal.cancel_actor().
 | 
					#   Portal.cancel_actor().
 | 
				
			||||||
#   => all other connected peers should get that cancel requesting peer's
 | 
					#   => all other connected peers should get that cancel requesting peer's
 | 
				
			||||||
| 
						 | 
					@ -40,6 +44,16 @@ from tractor._testing import (
 | 
				
			||||||
#   that also spawned a remote task task in that same peer-parent.
 | 
					#   that also spawned a remote task task in that same peer-parent.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# def test_self_cancel():
 | 
				
			||||||
 | 
					#     '''
 | 
				
			||||||
 | 
					#     2 cases:
 | 
				
			||||||
 | 
					#     - calls `Actor.cancel()` locally in some task
 | 
				
			||||||
 | 
					#     - calls LocalPortal.cancel_actor()` ?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#     '''
 | 
				
			||||||
 | 
					#     ...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@tractor.context
 | 
					@tractor.context
 | 
				
			||||||
async def open_stream_then_sleep_forever(
 | 
					async def open_stream_then_sleep_forever(
 | 
				
			||||||
    ctx: Context,
 | 
					    ctx: Context,
 | 
				
			||||||
| 
						 | 
					@ -396,6 +410,7 @@ 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(
 | 
				
			||||||
| 
						 | 
					@ -792,7 +807,7 @@ async def basic_echo_server(
 | 
				
			||||||
    ctx: Context,
 | 
					    ctx: Context,
 | 
				
			||||||
    peer_name: str = 'wittle_bruv',
 | 
					    peer_name: str = 'wittle_bruv',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    err_after_imsg: int|None = None,
 | 
					    err_after: int|None = None,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
| 
						 | 
					@ -821,9 +836,8 @@ async def basic_echo_server(
 | 
				
			||||||
            await ipc.send(resp)
 | 
					            await ipc.send(resp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (
 | 
					            if (
 | 
				
			||||||
                err_after_imsg
 | 
					                err_after
 | 
				
			||||||
                and
 | 
					                and i > err_after
 | 
				
			||||||
                i > err_after_imsg
 | 
					 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                raise RuntimeError(
 | 
					                raise RuntimeError(
 | 
				
			||||||
                    f'Simulated error in `{peer_name}`'
 | 
					                    f'Simulated error in `{peer_name}`'
 | 
				
			||||||
| 
						 | 
					@ -965,8 +979,7 @@ async def tell_little_bro(
 | 
				
			||||||
    actor_name: str,
 | 
					    actor_name: str,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    caller: str = '',
 | 
					    caller: str = '',
 | 
				
			||||||
    err_after: float|None = None,
 | 
					    err_after: int|None = None,
 | 
				
			||||||
    rng_seed: int = 50,
 | 
					 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    # contact target actor, do a stream dialog.
 | 
					    # contact target actor, do a stream dialog.
 | 
				
			||||||
    async with (
 | 
					    async with (
 | 
				
			||||||
| 
						 | 
					@ -977,18 +990,14 @@ async def tell_little_bro(
 | 
				
			||||||
            basic_echo_server,
 | 
					            basic_echo_server,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # XXX proxy any delayed err condition
 | 
					            # XXX proxy any delayed err condition
 | 
				
			||||||
            err_after_imsg=(
 | 
					            err_after=err_after,
 | 
				
			||||||
                err_after * rng_seed
 | 
					 | 
				
			||||||
                if err_after is not None
 | 
					 | 
				
			||||||
                else None
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ) as (sub_ctx, first),
 | 
					        ) as (sub_ctx, first),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sub_ctx.open_stream() as echo_ipc,
 | 
					        sub_ctx.open_stream() as echo_ipc,
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        actor: Actor = current_actor()
 | 
					        actor: Actor = current_actor()
 | 
				
			||||||
        uid: tuple = actor.uid
 | 
					        uid: tuple = actor.uid
 | 
				
			||||||
        for i in range(rng_seed):
 | 
					        for i in range(100):
 | 
				
			||||||
            msg: tuple = (
 | 
					            msg: tuple = (
 | 
				
			||||||
                uid,
 | 
					                uid,
 | 
				
			||||||
                i,
 | 
					                i,
 | 
				
			||||||
| 
						 | 
					@ -1013,13 +1022,13 @@ async def tell_little_bro(
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
    'raise_sub_spawn_error_after',
 | 
					    'raise_sub_spawn_error_after',
 | 
				
			||||||
    [None, 0.5],
 | 
					    [None, 50],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
def test_peer_spawns_and_cancels_service_subactor(
 | 
					def test_peer_spawns_and_cancels_service_subactor(
 | 
				
			||||||
    debug_mode: bool,
 | 
					    debug_mode: bool,
 | 
				
			||||||
    raise_client_error: str,
 | 
					    raise_client_error: str,
 | 
				
			||||||
    reg_addr: tuple[str, int],
 | 
					    reg_addr: tuple[str, int],
 | 
				
			||||||
    raise_sub_spawn_error_after: float|None,
 | 
					    raise_sub_spawn_error_after: int|None,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    # NOTE: this tests for the modden `mod wks open piker` bug
 | 
					    # NOTE: this tests for the modden `mod wks open piker` bug
 | 
				
			||||||
    # discovered as part of implementing workspace ctx
 | 
					    # discovered as part of implementing workspace ctx
 | 
				
			||||||
| 
						 | 
					@ -1033,7 +1042,6 @@ def test_peer_spawns_and_cancels_service_subactor(
 | 
				
			||||||
    #   and the server's spawned child should cancel and terminate!
 | 
					    #   and the server's spawned child should cancel and terminate!
 | 
				
			||||||
    peer_name: str = 'little_bro'
 | 
					    peer_name: str = 'little_bro'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def check_inner_rte(rae: RemoteActorError):
 | 
					    def check_inner_rte(rae: RemoteActorError):
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        Validate the little_bro's relayed inception!
 | 
					        Validate the little_bro's relayed inception!
 | 
				
			||||||
| 
						 | 
					@ -1127,7 +1135,8 @@ def test_peer_spawns_and_cancels_service_subactor(
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    try:
 | 
					                    try:
 | 
				
			||||||
                        res = await client_ctx.wait_for_result(hide_tb=False)
 | 
					                        res = await client_ctx.result(hide_tb=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        # in remote (relayed inception) error
 | 
					                        # in remote (relayed inception) error
 | 
				
			||||||
                        # case, we should error on the line above!
 | 
					                        # case, we should error on the line above!
 | 
				
			||||||
                        if raise_sub_spawn_error_after:
 | 
					                        if raise_sub_spawn_error_after:
 | 
				
			||||||
| 
						 | 
					@ -1138,23 +1147,6 @@ def test_peer_spawns_and_cancels_service_subactor(
 | 
				
			||||||
                        assert isinstance(res, ContextCancelled)
 | 
					                        assert isinstance(res, ContextCancelled)
 | 
				
			||||||
                        assert client_ctx.cancel_acked
 | 
					                        assert client_ctx.cancel_acked
 | 
				
			||||||
                        assert res.canceller == root.uid
 | 
					                        assert res.canceller == root.uid
 | 
				
			||||||
                        assert not raise_sub_spawn_error_after
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                        # cancelling the spawner sub should
 | 
					 | 
				
			||||||
                        # transitively cancel it's sub, the little
 | 
					 | 
				
			||||||
                        # bruv.
 | 
					 | 
				
			||||||
                        print('root cancelling server/client sub-actors')
 | 
					 | 
				
			||||||
                        await spawn_ctx.cancel()
 | 
					 | 
				
			||||||
                        async with tractor.find_actor(
 | 
					 | 
				
			||||||
                            name=peer_name,
 | 
					 | 
				
			||||||
                        ) as sub:
 | 
					 | 
				
			||||||
                            assert not sub
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    # XXX, only for tracing
 | 
					 | 
				
			||||||
                    # except BaseException as _berr:
 | 
					 | 
				
			||||||
                    #     berr = _berr
 | 
					 | 
				
			||||||
                    #     await tractor.pause(shield=True)
 | 
					 | 
				
			||||||
                    #     raise berr
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    except RemoteActorError as rae:
 | 
					                    except RemoteActorError as rae:
 | 
				
			||||||
                        _err = rae
 | 
					                        _err = rae
 | 
				
			||||||
| 
						 | 
					@ -1183,8 +1175,19 @@ def test_peer_spawns_and_cancels_service_subactor(
 | 
				
			||||||
                        raise
 | 
					                        raise
 | 
				
			||||||
                        # await tractor.pause()
 | 
					                        # await tractor.pause()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        assert not raise_sub_spawn_error_after
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        # cancelling the spawner sub should
 | 
				
			||||||
 | 
					                        # transitively cancel it's sub, the little
 | 
				
			||||||
 | 
					                        # bruv.
 | 
				
			||||||
 | 
					                        print('root cancelling server/client sub-actors')
 | 
				
			||||||
 | 
					                        await spawn_ctx.cancel()
 | 
				
			||||||
 | 
					                        async with tractor.find_actor(
 | 
				
			||||||
 | 
					                            name=peer_name,
 | 
				
			||||||
 | 
					                        ) as sub:
 | 
				
			||||||
 | 
					                            assert not sub
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    # await tractor.pause()
 | 
					 | 
				
			||||||
                    # await server.cancel_actor()
 | 
					                    # await server.cancel_actor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            except RemoteActorError as rae:
 | 
					            except RemoteActorError as rae:
 | 
				
			||||||
| 
						 | 
					@ -1197,7 +1200,7 @@ def test_peer_spawns_and_cancels_service_subactor(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # since we called `.cancel_actor()`, `.cancel_ack`
 | 
					            # since we called `.cancel_actor()`, `.cancel_ack`
 | 
				
			||||||
            # will not be set on the ctx bc `ctx.cancel()` was not
 | 
					            # will not be set on the ctx bc `ctx.cancel()` was not
 | 
				
			||||||
            # called directly for this confext.
 | 
					            # called directly fot this confext.
 | 
				
			||||||
            except ContextCancelled as ctxc:
 | 
					            except ContextCancelled as ctxc:
 | 
				
			||||||
                _ctxc = ctxc
 | 
					                _ctxc = ctxc
 | 
				
			||||||
                print(
 | 
					                print(
 | 
				
			||||||
| 
						 | 
					@ -1237,19 +1240,12 @@ def test_peer_spawns_and_cancels_service_subactor(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # assert spawn_ctx.cancelled_caught
 | 
					                # assert spawn_ctx.cancelled_caught
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def _main():
 | 
					 | 
				
			||||||
        with trio.fail_after(
 | 
					 | 
				
			||||||
            3 if not debug_mode
 | 
					 | 
				
			||||||
            else 999
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            await main()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if raise_sub_spawn_error_after:
 | 
					    if raise_sub_spawn_error_after:
 | 
				
			||||||
        with pytest.raises(RemoteActorError) as excinfo:
 | 
					        with pytest.raises(RemoteActorError) as excinfo:
 | 
				
			||||||
            trio.run(_main)
 | 
					            trio.run(main)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        rae: RemoteActorError = excinfo.value
 | 
					        rae: RemoteActorError = excinfo.value
 | 
				
			||||||
        check_inner_rte(rae)
 | 
					        check_inner_rte(rae)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        trio.run(_main)
 | 
					        trio.run(main)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -235,16 +235,10 @@ async def cancel_after(wait, reg_addr):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.fixture(scope='module')
 | 
					@pytest.fixture(scope='module')
 | 
				
			||||||
def time_quad_ex(
 | 
					def time_quad_ex(reg_addr, ci_env, spawn_backend):
 | 
				
			||||||
    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
 | 
				
			||||||
| 
						 | 
					@ -255,24 +249,12 @@ def time_quad_ex(
 | 
				
			||||||
    return results, diff
 | 
					    return results, diff
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_a_quadruple_example(
 | 
					def test_a_quadruple_example(time_quad_ex, ci_env, spawn_backend):
 | 
				
			||||||
    time_quad_ex: tuple,
 | 
					    """This also serves as a kind of "we'd like to be this fast test"."""
 | 
				
			||||||
    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 = (
 | 
					    this_fast = 6 if platform.system() in ('Windows', 'Darwin') else 3
 | 
				
			||||||
        6 if platform.system() in (
 | 
					 | 
				
			||||||
            'Windows',
 | 
					 | 
				
			||||||
            'Darwin',
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        else 3
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    assert diff < this_fast
 | 
					    assert diff < this_fast
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,239 +0,0 @@
 | 
				
			||||||
'''
 | 
					 | 
				
			||||||
Define the details of inter-actor "out-of-band" (OoB) cancel
 | 
					 | 
				
			||||||
semantics, that is how cancellation works when a cancel request comes
 | 
					 | 
				
			||||||
from the different concurrency (primitive's) "layer" then where the
 | 
					 | 
				
			||||||
eventual `trio.Task` actually raises a signal.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
'''
 | 
					 | 
				
			||||||
from functools import partial
 | 
					 | 
				
			||||||
# from contextlib import asynccontextmanager as acm
 | 
					 | 
				
			||||||
# import itertools
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import pytest
 | 
					 | 
				
			||||||
import trio
 | 
					 | 
				
			||||||
import tractor
 | 
					 | 
				
			||||||
from tractor import (  # typing
 | 
					 | 
				
			||||||
    ActorNursery,
 | 
					 | 
				
			||||||
    Portal,
 | 
					 | 
				
			||||||
    Context,
 | 
					 | 
				
			||||||
    # ContextCancelled,
 | 
					 | 
				
			||||||
    # RemoteActorError,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
# from tractor._testing import (
 | 
					 | 
				
			||||||
#     tractor_test,
 | 
					 | 
				
			||||||
#     expect_ctxc,
 | 
					 | 
				
			||||||
# )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# XXX TODO cases:
 | 
					 | 
				
			||||||
# - [ ] peer cancelled itself - so other peers should
 | 
					 | 
				
			||||||
#   get errors reflecting that the peer was itself the .canceller?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# def test_self_cancel():
 | 
					 | 
				
			||||||
#     '''
 | 
					 | 
				
			||||||
#     2 cases:
 | 
					 | 
				
			||||||
#     - calls `Actor.cancel()` locally in some task
 | 
					 | 
				
			||||||
#     - calls LocalPortal.cancel_actor()` ?
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# things to ensure!
 | 
					 | 
				
			||||||
# -[ ] the ctxc raised in a child should ideally show the tb of the
 | 
					 | 
				
			||||||
#     underlying `Cancelled` checkpoint, i.e.
 | 
					 | 
				
			||||||
#     `raise scope_error from ctxc`?
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# -[ ] a self-cancelled context, if not allowed to block on
 | 
					 | 
				
			||||||
#     `ctx.result()` at some point will hang since the `ctx._scope`
 | 
					 | 
				
			||||||
#     is never `.cancel_called`; cases for this include,
 | 
					 | 
				
			||||||
#     - an `open_ctx()` which never starteds before being OoB actor
 | 
					 | 
				
			||||||
#       cancelled.
 | 
					 | 
				
			||||||
#       |_ parent task will be blocked in `.open_context()` for the
 | 
					 | 
				
			||||||
#         `Started` msg, and when the OoB ctxc arrives `ctx._scope`
 | 
					 | 
				
			||||||
#         will never have been signalled..
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#     '''
 | 
					 | 
				
			||||||
#     ...
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# TODO, sanity test against the case in `/examples/trio/lockacquire_not_unmasked.py`
 | 
					 | 
				
			||||||
# but with the `Lock.acquire()` from a `@context` to ensure the
 | 
					 | 
				
			||||||
# implicit ignore-case-non-unmasking.
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
# @tractor.context
 | 
					 | 
				
			||||||
# async def acquire_actor_global_lock(
 | 
					 | 
				
			||||||
#     ctx: tractor.Context,
 | 
					 | 
				
			||||||
#     ignore_special_cases: bool,
 | 
					 | 
				
			||||||
# ):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#     async with maybe_unmask_excs(
 | 
					 | 
				
			||||||
#         ignore_special_cases=ignore_special_cases,
 | 
					 | 
				
			||||||
#     ):
 | 
					 | 
				
			||||||
#         await ctx.started('locked')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#     # block til cancelled
 | 
					 | 
				
			||||||
#     await trio.sleep_forever()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@tractor.context
 | 
					 | 
				
			||||||
async def sleep_forever(
 | 
					 | 
				
			||||||
    ctx: tractor.Context,
 | 
					 | 
				
			||||||
    # ignore_special_cases: bool,
 | 
					 | 
				
			||||||
    do_started: bool,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # async with maybe_unmask_excs(
 | 
					 | 
				
			||||||
    #     ignore_special_cases=ignore_special_cases,
 | 
					 | 
				
			||||||
    # ):
 | 
					 | 
				
			||||||
    #     await ctx.started('locked')
 | 
					 | 
				
			||||||
    if do_started:
 | 
					 | 
				
			||||||
        await ctx.started()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # block til cancelled
 | 
					 | 
				
			||||||
    print('sleepin on child-side..')
 | 
					 | 
				
			||||||
    await trio.sleep_forever()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					 | 
				
			||||||
    'cancel_ctx',
 | 
					 | 
				
			||||||
    [True, False],
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
def test_cancel_ctx_with_parent_side_entered_in_bg_task(
 | 
					 | 
				
			||||||
    debug_mode: bool,
 | 
					 | 
				
			||||||
    loglevel: str,
 | 
					 | 
				
			||||||
    cancel_ctx: bool,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    The most "basic" out-of-band-task self-cancellation case where
 | 
					 | 
				
			||||||
    `Portal.open_context()` is entered in a bg task and the
 | 
					 | 
				
			||||||
    parent-task (of the containing nursery) calls `Context.cancel()`
 | 
					 | 
				
			||||||
    without the child knowing; the `Context._scope` should be
 | 
					 | 
				
			||||||
    `.cancel_called` when the IPC ctx's child-side relays
 | 
					 | 
				
			||||||
    a `ContextCancelled` with a `.canceller` set to the parent
 | 
					 | 
				
			||||||
    actor('s task).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    async def main():
 | 
					 | 
				
			||||||
        with trio.fail_after(
 | 
					 | 
				
			||||||
            2 if not debug_mode else 999,
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            an: ActorNursery
 | 
					 | 
				
			||||||
            async with (
 | 
					 | 
				
			||||||
                tractor.open_nursery(
 | 
					 | 
				
			||||||
                    debug_mode=debug_mode,
 | 
					 | 
				
			||||||
                    loglevel='devx',
 | 
					 | 
				
			||||||
                    enable_stack_on_sig=True,
 | 
					 | 
				
			||||||
                ) as an,
 | 
					 | 
				
			||||||
                trio.open_nursery() as tn,
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                ptl: Portal = await an.start_actor(
 | 
					 | 
				
			||||||
                    'sub',
 | 
					 | 
				
			||||||
                    enable_modules=[__name__],
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                async def _open_ctx_async(
 | 
					 | 
				
			||||||
                    do_started: bool = True,
 | 
					 | 
				
			||||||
                    task_status=trio.TASK_STATUS_IGNORED,
 | 
					 | 
				
			||||||
                ):
 | 
					 | 
				
			||||||
                    # do we expect to never enter the
 | 
					 | 
				
			||||||
                    # `.open_context()` below.
 | 
					 | 
				
			||||||
                    if not do_started:
 | 
					 | 
				
			||||||
                        task_status.started()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    async with ptl.open_context(
 | 
					 | 
				
			||||||
                        sleep_forever,
 | 
					 | 
				
			||||||
                        do_started=do_started,
 | 
					 | 
				
			||||||
                    ) as (ctx, first):
 | 
					 | 
				
			||||||
                        task_status.started(ctx)
 | 
					 | 
				
			||||||
                        await trio.sleep_forever()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # XXX, this is the key OoB part!
 | 
					 | 
				
			||||||
                #
 | 
					 | 
				
			||||||
                # - start the `.open_context()` in a bg task which
 | 
					 | 
				
			||||||
                #   blocks inside the embedded scope-body,
 | 
					 | 
				
			||||||
                #
 | 
					 | 
				
			||||||
                # -  when we call `Context.cancel()` it **is
 | 
					 | 
				
			||||||
                #   not** from the same task which eventually runs
 | 
					 | 
				
			||||||
                #   `.__aexit__()`,
 | 
					 | 
				
			||||||
                #
 | 
					 | 
				
			||||||
                # - since the bg "opener" task will be in
 | 
					 | 
				
			||||||
                #   a `trio.sleep_forever()`, it must be interrupted
 | 
					 | 
				
			||||||
                #   by the `ContextCancelled` delivered from the
 | 
					 | 
				
			||||||
                #   child-side; `Context._scope: CancelScope` MUST
 | 
					 | 
				
			||||||
                #   be `.cancel_called`!
 | 
					 | 
				
			||||||
                #
 | 
					 | 
				
			||||||
                print('ASYNC opening IPC context in subtask..')
 | 
					 | 
				
			||||||
                maybe_ctx: Context|None = await tn.start(partial(
 | 
					 | 
				
			||||||
                    _open_ctx_async,
 | 
					 | 
				
			||||||
                ))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (
 | 
					 | 
				
			||||||
                    maybe_ctx
 | 
					 | 
				
			||||||
                    and
 | 
					 | 
				
			||||||
                    cancel_ctx
 | 
					 | 
				
			||||||
                ):
 | 
					 | 
				
			||||||
                    print('cancelling first IPC ctx!')
 | 
					 | 
				
			||||||
                    await maybe_ctx.cancel()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # XXX, note that despite `maybe_context.cancel()`
 | 
					 | 
				
			||||||
                # being called above, it's the parent (bg) task
 | 
					 | 
				
			||||||
                # which was originally never interrupted in
 | 
					 | 
				
			||||||
                # the `ctx._scope` body due to missing case logic in
 | 
					 | 
				
			||||||
                # `ctx._maybe_cancel_and_set_remote_error()`.
 | 
					 | 
				
			||||||
                #
 | 
					 | 
				
			||||||
                # It didn't matter that the subactor process was
 | 
					 | 
				
			||||||
                # already terminated and reaped, nothing was
 | 
					 | 
				
			||||||
                # cancelling the ctx-parent task's scope!
 | 
					 | 
				
			||||||
                #
 | 
					 | 
				
			||||||
                print('cancelling subactor!')
 | 
					 | 
				
			||||||
                await ptl.cancel_actor()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if maybe_ctx:
 | 
					 | 
				
			||||||
                    try:
 | 
					 | 
				
			||||||
                        await maybe_ctx.wait_for_result()
 | 
					 | 
				
			||||||
                    except tractor.ContextCancelled as ctxc:
 | 
					 | 
				
			||||||
                        assert not cancel_ctx
 | 
					 | 
				
			||||||
                        assert (
 | 
					 | 
				
			||||||
                            ctxc.canceller
 | 
					 | 
				
			||||||
                            ==
 | 
					 | 
				
			||||||
                            tractor.current_actor().aid.uid
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        # don't re-raise since it'll trigger
 | 
					 | 
				
			||||||
                        # an EG from the above tn.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if cancel_ctx:
 | 
					 | 
				
			||||||
        # graceful self-cancel
 | 
					 | 
				
			||||||
        trio.run(main)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        # ctx parent task should see OoB ctxc due to
 | 
					 | 
				
			||||||
        # `ptl.cancel_actor()`.
 | 
					 | 
				
			||||||
        with pytest.raises(tractor.ContextCancelled) as excinfo:
 | 
					 | 
				
			||||||
            trio.run(main)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        assert 'root' in excinfo.value.canceller[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# def test_parent_actor_cancels_subactor_with_gt1_ctxs_open_to_it(
 | 
					 | 
				
			||||||
#     debug_mode: bool,
 | 
					 | 
				
			||||||
#     loglevel: str,
 | 
					 | 
				
			||||||
# ):
 | 
					 | 
				
			||||||
#     '''
 | 
					 | 
				
			||||||
#     Demos OoB cancellation from the perspective of a ctx opened with
 | 
					 | 
				
			||||||
#     a child subactor where the parent cancels the child at the "actor
 | 
					 | 
				
			||||||
#     layer" using `Portal.cancel_actor()` and thus the
 | 
					 | 
				
			||||||
#     `ContextCancelled.canceller` received by the ctx's parent-side
 | 
					 | 
				
			||||||
#     task will appear to be a "self cancellation" even though that
 | 
					 | 
				
			||||||
#     specific task itself was not cancelled and thus
 | 
					 | 
				
			||||||
#     `Context.cancel_called ==False`.
 | 
					 | 
				
			||||||
#     '''
 | 
					 | 
				
			||||||
                # TODO, do we have an existing implied ctx
 | 
					 | 
				
			||||||
                # cancel test like this?
 | 
					 | 
				
			||||||
                # with trio.move_on_after(0.5):# as cs:
 | 
					 | 
				
			||||||
                #     await _open_ctx_async(
 | 
					 | 
				
			||||||
                #         do_started=False,
 | 
					 | 
				
			||||||
                #     )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # in-line ctx scope should definitely raise
 | 
					 | 
				
			||||||
                # a ctxc with `.canceller = 'root'`
 | 
					 | 
				
			||||||
                # async with ptl.open_context(
 | 
					 | 
				
			||||||
                #     sleep_forever,
 | 
					 | 
				
			||||||
                #     do_started=True,
 | 
					 | 
				
			||||||
                # ) as pair:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,237 +0,0 @@
 | 
				
			||||||
'''
 | 
					 | 
				
			||||||
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,6 +1,5 @@
 | 
				
			||||||
'''
 | 
					'''
 | 
				
			||||||
Suites for our `.trionics.maybe_open_context()` multi-task
 | 
					Async context manager cache api testing: ``trionics.maybe_open_context():``
 | 
				
			||||||
shared-cached `@acm` API.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
'''
 | 
					'''
 | 
				
			||||||
from contextlib import asynccontextmanager as acm
 | 
					from contextlib import asynccontextmanager as acm
 | 
				
			||||||
| 
						 | 
					@ -10,15 +9,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -62,7 +52,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 maybe_open_context(
 | 
					            async with tractor.trionics.maybe_open_context(
 | 
				
			||||||
                maybe_increment_counter,
 | 
					                maybe_increment_counter,
 | 
				
			||||||
                kwargs=kwargs,
 | 
					                kwargs=kwargs,
 | 
				
			||||||
                key=key,
 | 
					                key=key,
 | 
				
			||||||
| 
						 | 
					@ -82,13 +72,11 @@ 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 tn,
 | 
					                trio.open_nursery() as n,
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                for i in range(10):
 | 
					                for i in range(10):
 | 
				
			||||||
                    tn.start_soon(
 | 
					                    n.start_soon(enter_cached_mngr, f'task_{i}')
 | 
				
			||||||
                        enter_cached_mngr,
 | 
					 | 
				
			||||||
                        f'task_{i}',
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    await trio.sleep(0.001)
 | 
					                    await trio.sleep(0.001)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    trio.run(main)
 | 
					    trio.run(main)
 | 
				
			||||||
| 
						 | 
					@ -110,34 +98,23 @@ async def streamer(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@acm
 | 
					@acm
 | 
				
			||||||
async def open_stream() -> Awaitable[
 | 
					async def open_stream() -> Awaitable[tractor.MsgStream]:
 | 
				
			||||||
    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__],
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            try:
 | 
					            async with (
 | 
				
			||||||
                async with (
 | 
					                portal.open_context(streamer) as (ctx, first),
 | 
				
			||||||
                    portal.open_context(streamer) as (ctx, first),
 | 
					                ctx.open_stream() as stream,
 | 
				
			||||||
                    ctx.open_stream() as stream,
 | 
					            ):
 | 
				
			||||||
                ):
 | 
					                yield stream
 | 
				
			||||||
                    print('Entered open_stream() caller')
 | 
					 | 
				
			||||||
                    yield an, stream
 | 
					 | 
				
			||||||
                    print('Exited open_stream() caller')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            finally:
 | 
					            print('Cancelling streamer')
 | 
				
			||||||
                print(
 | 
					            await portal.cancel_actor()
 | 
				
			||||||
                    'Cancelling streamer with,\n'
 | 
					            print('Cancelled streamer')
 | 
				
			||||||
                    '=> `Portal.cancel_actor()`'
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                await portal.cancel_actor()
 | 
					 | 
				
			||||||
                print('Cancelled streamer')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    except Exception as err:
 | 
					    except Exception as err:
 | 
				
			||||||
        print(
 | 
					        print(
 | 
				
			||||||
| 
						 | 
					@ -150,15 +127,11 @@ async def open_stream() -> Awaitable[
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@acm
 | 
					@acm
 | 
				
			||||||
async def maybe_open_stream(taskname: str):
 | 
					async def maybe_open_stream(taskname: str):
 | 
				
			||||||
    async with maybe_open_context(
 | 
					    async with tractor.trionics.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 (
 | 
					    ) as (cache_hit, stream):
 | 
				
			||||||
        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')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -166,43 +139,10 @@ 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 an, bstream
 | 
					                yield 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
 | 
				
			||||||
            try:
 | 
					            yield stream
 | 
				
			||||||
                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(
 | 
				
			||||||
| 
						 | 
					@ -219,24 +159,16 @@ 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 (
 | 
					                maybe_open_stream(taskname) as stream,
 | 
				
			||||||
                    an,
 | 
					 | 
				
			||||||
                    stream,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                if '0' in taskname:
 | 
					                if '0' in taskname:
 | 
				
			||||||
                    assert isinstance(stream, tractor.MsgStream)
 | 
					                    assert isinstance(stream, tractor.MsgStream)
 | 
				
			||||||
| 
						 | 
					@ -248,159 +180,34 @@ 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: list[int] = []
 | 
					                seq = []
 | 
				
			||||||
                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(num_tasks):
 | 
					                    for i in range(10):
 | 
				
			||||||
                        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,7 +147,8 @@ 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 if not debug_mode else 999):
 | 
					        # with trio.fail_after(2):
 | 
				
			||||||
 | 
					        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()
 | 
				
			||||||
| 
						 | 
					@ -216,25 +217,32 @@ 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',
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        patt: str = 'trio-side'
 | 
					        assert len(errs := rest_eg.exceptions) == 1
 | 
				
			||||||
        expect_exc = TypeError
 | 
					        typerr = errs[0]
 | 
				
			||||||
 | 
					        assert (
 | 
				
			||||||
 | 
					            type(typerr) is TypeError
 | 
				
			||||||
 | 
					            and
 | 
				
			||||||
 | 
					            'trio-side' in typerr.args
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # when aio errors BEFORE (last) trio task is scheduled, we should
 | 
					    # 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:
 | 
				
			||||||
        patt: str = 'asyncio-side'
 | 
					        assert len(rtes := rte_eg.exceptions) == 1
 | 
				
			||||||
        expect_exc = RuntimeError
 | 
					        assert 'asyncio-side' in rtes[0].args[0]
 | 
				
			||||||
 | 
					 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,26 +83,3 @@ def test_implicit_root_via_first_nursery(
 | 
				
			||||||
            assert tractor.current_actor().aid.name == 'root'
 | 
					            assert tractor.current_actor().aid.name == 'root'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    trio.run(main)
 | 
					    trio.run(main)
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def test_runtime_vars_unset(
 | 
					 | 
				
			||||||
    reg_addr: tuple,
 | 
					 | 
				
			||||||
    debug_mode: bool
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    Ensure any `._state._runtime_vars` are restored to default values
 | 
					 | 
				
			||||||
    after the root actor-runtime exits!
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    assert not tractor._state._runtime_vars['_debug_mode']
 | 
					 | 
				
			||||||
    async def main():
 | 
					 | 
				
			||||||
        assert not tractor._state._runtime_vars['_debug_mode']
 | 
					 | 
				
			||||||
        async with tractor.open_nursery(
 | 
					 | 
				
			||||||
            debug_mode=True,
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            assert tractor._state._runtime_vars['_debug_mode']
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # after runtime closure, should be reverted!
 | 
					 | 
				
			||||||
        assert not tractor._state._runtime_vars['_debug_mode']
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    trio.run(main)
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,18 +6,10 @@ want to see changed.
 | 
				
			||||||
from contextlib import (
 | 
					from contextlib import (
 | 
				
			||||||
    asynccontextmanager as acm,
 | 
					    asynccontextmanager as acm,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from types import ModuleType
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from functools import partial
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					import pytest
 | 
				
			||||||
from _pytest import pathlib
 | 
					 | 
				
			||||||
from tractor.trionics import collapse_eg
 | 
					 | 
				
			||||||
import trio
 | 
					import trio
 | 
				
			||||||
from trio import TaskStatus
 | 
					from trio import TaskStatus
 | 
				
			||||||
from tractor._testing import (
 | 
					 | 
				
			||||||
    examples_dir,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					@pytest.mark.parametrize(
 | 
				
			||||||
| 
						 | 
					@ -72,8 +64,9 @@ def test_stashed_child_nursery(use_start_soon):
 | 
				
			||||||
    async def main():
 | 
					    async def main():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        async with (
 | 
					        async with (
 | 
				
			||||||
            collapse_eg(),
 | 
					            trio.open_nursery(
 | 
				
			||||||
            trio.open_nursery() as pn,
 | 
					                strict_exception_groups=False,
 | 
				
			||||||
 | 
					            ) as pn,
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            cn = await pn.start(mk_child_nursery)
 | 
					            cn = await pn.start(mk_child_nursery)
 | 
				
			||||||
            assert cn
 | 
					            assert cn
 | 
				
			||||||
| 
						 | 
					@ -113,21 +106,66 @@ def test_acm_embedded_nursery_propagates_enter_err(
 | 
				
			||||||
    debug_mode: bool,
 | 
					    debug_mode: bool,
 | 
				
			||||||
):
 | 
					):
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Demo how a masking `trio.Cancelled` could be handled by unmasking
 | 
					    Demo how a masking `trio.Cancelled` could be handled by unmasking from the
 | 
				
			||||||
    from the `.__context__` field when a user (by accident) re-raises
 | 
					    `.__context__` field when a user (by accident) re-raises from a `finally:`.
 | 
				
			||||||
    from a `finally:`.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    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,
 | 
				
			||||||
            tractor.trionics.maybe_raise_from_masking_exc(
 | 
					            maybe_raise_from_masking_exc(
 | 
				
			||||||
 | 
					                tn=tn,
 | 
				
			||||||
                unmask_from=(
 | 
					                unmask_from=(
 | 
				
			||||||
                    (trio.Cancelled,) if unmask_from_canc
 | 
					                    trio.Cancelled
 | 
				
			||||||
                    else ()
 | 
					                    if unmask_from_canc
 | 
				
			||||||
 | 
					                    else None
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
| 
						 | 
					@ -142,7 +180,8 @@ def test_acm_embedded_nursery_propagates_enter_err(
 | 
				
			||||||
        with tractor.devx.maybe_open_crash_handler(
 | 
					        with tractor.devx.maybe_open_crash_handler(
 | 
				
			||||||
            pdb=debug_mode,
 | 
					            pdb=debug_mode,
 | 
				
			||||||
        ) as bxerr:
 | 
					        ) as bxerr:
 | 
				
			||||||
            assert not bxerr.value
 | 
					            if bxerr:
 | 
				
			||||||
 | 
					                assert not bxerr.value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            async with (
 | 
					            async with (
 | 
				
			||||||
                wraps_tn_that_always_cancels() as tn,
 | 
					                wraps_tn_that_always_cancels() as tn,
 | 
				
			||||||
| 
						 | 
					@ -150,12 +189,11 @@ def test_acm_embedded_nursery_propagates_enter_err(
 | 
				
			||||||
                assert not tn.cancel_scope.cancel_called
 | 
					                assert not tn.cancel_scope.cancel_called
 | 
				
			||||||
                assert 0
 | 
					                assert 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if debug_mode:
 | 
					        assert (
 | 
				
			||||||
            assert (
 | 
					            (err := bxerr.value)
 | 
				
			||||||
                (err := bxerr.value)
 | 
					            and
 | 
				
			||||||
                and
 | 
					            type(err) is AssertionError
 | 
				
			||||||
                type(err) is AssertionError
 | 
					        )
 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    with pytest.raises(ExceptionGroup) as excinfo:
 | 
					    with pytest.raises(ExceptionGroup) as excinfo:
 | 
				
			||||||
        trio.run(_main)
 | 
					        trio.run(_main)
 | 
				
			||||||
| 
						 | 
					@ -164,139 +202,3 @@ 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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					 | 
				
			||||||
    'raise_unmasked', [
 | 
					 | 
				
			||||||
        True,
 | 
					 | 
				
			||||||
        pytest.param(
 | 
					 | 
				
			||||||
            False,
 | 
					 | 
				
			||||||
            marks=pytest.mark.xfail(
 | 
					 | 
				
			||||||
                reason="see examples/trio/send_chan_aclose_masks.py"
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					 | 
				
			||||||
    'child_errors_mid_stream',
 | 
					 | 
				
			||||||
    [True, False],
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
def test_unmask_aclose_as_checkpoint_on_aexit(
 | 
					 | 
				
			||||||
    raise_unmasked: bool,
 | 
					 | 
				
			||||||
    child_errors_mid_stream: bool,
 | 
					 | 
				
			||||||
    debug_mode: bool,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    Verify that our unmasker util works over the common case where
 | 
					 | 
				
			||||||
    a mem-chan's `.aclose()` is included in an `@acm` stack
 | 
					 | 
				
			||||||
    and it being currently a checkpoint, can `trio.Cancelled`-mask an embedded
 | 
					 | 
				
			||||||
    exception from user code resulting in a silent failure which
 | 
					 | 
				
			||||||
    appears like graceful cancellation.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    This test suite is mostly implemented as an example script so it
 | 
					 | 
				
			||||||
    could more easily be shared with `trio`-core peeps as `tractor`-less
 | 
					 | 
				
			||||||
    minimum reproducing example.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    mod: ModuleType = pathlib.import_path(
 | 
					 | 
				
			||||||
        examples_dir()
 | 
					 | 
				
			||||||
        / 'trio'
 | 
					 | 
				
			||||||
        / 'send_chan_aclose_masks_beg.py',
 | 
					 | 
				
			||||||
        root=examples_dir(),
 | 
					 | 
				
			||||||
        consider_namespace_packages=False,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    with pytest.raises(RuntimeError):
 | 
					 | 
				
			||||||
        trio.run(partial(
 | 
					 | 
				
			||||||
            mod.main,
 | 
					 | 
				
			||||||
            raise_unmasked=raise_unmasked,
 | 
					 | 
				
			||||||
            child_errors_mid_stream=child_errors_mid_stream,
 | 
					 | 
				
			||||||
        ))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.mark.parametrize(
 | 
					 | 
				
			||||||
    'ignore_special_cases', [
 | 
					 | 
				
			||||||
        True,
 | 
					 | 
				
			||||||
        pytest.param(
 | 
					 | 
				
			||||||
            False,
 | 
					 | 
				
			||||||
            marks=pytest.mark.xfail(
 | 
					 | 
				
			||||||
                reason="see examples/trio/lockacquire_not_umasked.py"
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
def test_cancelled_lockacquire_in_ipctx_not_unmasked(
 | 
					 | 
				
			||||||
    ignore_special_cases: bool,
 | 
					 | 
				
			||||||
    loglevel: str,
 | 
					 | 
				
			||||||
    debug_mode: bool,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    mod: ModuleType = pathlib.import_path(
 | 
					 | 
				
			||||||
        examples_dir()
 | 
					 | 
				
			||||||
        / 'trio'
 | 
					 | 
				
			||||||
        / 'lockacquire_not_unmasked.py',
 | 
					 | 
				
			||||||
        root=examples_dir(),
 | 
					 | 
				
			||||||
        consider_namespace_packages=False,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    async def _main():
 | 
					 | 
				
			||||||
        with trio.fail_after(2):
 | 
					 | 
				
			||||||
            await mod.main(
 | 
					 | 
				
			||||||
                ignore_special_cases=ignore_special_cases,
 | 
					 | 
				
			||||||
                loglevel=loglevel,
 | 
					 | 
				
			||||||
                debug_mode=debug_mode,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    trio.run(_main)
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,17 +55,10 @@ 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 (
 | 
					    async with tractor.open_nursery(
 | 
				
			||||||
        # tractor.trionics.collapse_eg(),
 | 
					        **runtime_kwargs,
 | 
				
			||||||
        tractor.open_nursery(
 | 
					    ) as an:
 | 
				
			||||||
            **runtime_kwargs,
 | 
					        async with trio.open_nursery() as n:
 | 
				
			||||||
        ) 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:
 | 
				
			||||||
| 
						 | 
					@ -76,8 +69,9 @@ async def open_actor_cluster(
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for name in names:
 | 
					            for name in names:
 | 
				
			||||||
                tn.start_soon(_start, name)
 | 
					                n.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,9 +101,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -154,7 +151,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 "child" side a context is
 | 
					    actor from a `Portal`. On the "callee" 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
 | 
				
			||||||
| 
						 | 
					@ -222,8 +219,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 parent side since
 | 
					    # NOTE: (for now) only set (a portal) on the caller side since
 | 
				
			||||||
    # the child doesn't generally need a ref to one and should
 | 
					    # the callee 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
 | 
				
			||||||
| 
						 | 
					@ -252,12 +249,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 "child" task, so
 | 
					    # delivered from the far end "callee" 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 "parent"  task errors this value is always set
 | 
					    # if the local "caller"  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
 | 
				
			||||||
| 
						 | 
					@ -293,9 +290,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 "child"
 | 
					    #   the crash handler REPL in such cases where the "callee"
 | 
				
			||||||
    #   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
 | 
				
			||||||
| 
						 | 
					@ -307,8 +304,8 @@ class Context:
 | 
				
			||||||
    _stream_opened: bool = False
 | 
					    _stream_opened: bool = False
 | 
				
			||||||
    _stream: MsgStream|None = None
 | 
					    _stream: MsgStream|None = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # the parent-task's calling-fn's frame-info, the frame above
 | 
					    # caller of `Portal.open_context()` for
 | 
				
			||||||
    # `Portal.open_context()`, for introspection/logging.
 | 
					    # logging purposes mostly
 | 
				
			||||||
    _caller_info: CallerInfo|None = None
 | 
					    _caller_info: CallerInfo|None = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # overrun handling machinery
 | 
					    # overrun handling machinery
 | 
				
			||||||
| 
						 | 
					@ -442,25 +439,25 @@ class Context:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        Records whether cancellation has been requested for this context
 | 
					        Records whether cancellation has been requested for this context
 | 
				
			||||||
        by a call to  `.cancel()` either due to,
 | 
					        by a call to  `.cancel()` either due to,
 | 
				
			||||||
        - an explicit call by some local task,
 | 
					        - either an explicit call by some local task,
 | 
				
			||||||
        - or an implicit call due to an error caught inside
 | 
					        - or an implicit call due to an error caught inside
 | 
				
			||||||
          the `Portal.open_context()` block.
 | 
					          the ``Portal.open_context()`` block.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        return self._cancel_called
 | 
					        return self._cancel_called
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # XXX, to debug who frickin sets it..
 | 
					    @cancel_called.setter
 | 
				
			||||||
    # @cancel_called.setter
 | 
					    def cancel_called(self, val: bool) -> None:
 | 
				
			||||||
    # def cancel_called(self, val: bool) -> None:
 | 
					        '''
 | 
				
			||||||
    #     '''
 | 
					        Set the self-cancelled request `bool` value.
 | 
				
			||||||
    #     Set the self-cancelled request `bool` value.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #     '''
 | 
					        '''
 | 
				
			||||||
    #     if val:
 | 
					        # to debug who frickin sets it..
 | 
				
			||||||
    #         from .devx import pause_from_sync
 | 
					        # if val:
 | 
				
			||||||
    #         pause_from_sync()
 | 
					        #     from .devx import pause_from_sync
 | 
				
			||||||
 | 
					        #     pause_from_sync()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #     self._cancel_called = val
 | 
					        self._cancel_called = val
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def canceller(self) -> tuple[str, str]|None:
 | 
					    def canceller(self) -> tuple[str, str]|None:
 | 
				
			||||||
| 
						 | 
					@ -529,11 +526,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 == "parent"` ctx wherein the
 | 
					        `True` for a `.side == "caller"` 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 parent side calls `.cancel()`, the far side cancels
 | 
					        - a caller 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
 | 
				
			||||||
| 
						 | 
					@ -542,20 +539,20 @@ class Context:
 | 
				
			||||||
          => `._scope.cancelled_caught == True` by normal `trio`
 | 
					          => `._scope.cancelled_caught == True` by normal `trio`
 | 
				
			||||||
          cs semantics.
 | 
					          cs semantics.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        - a parent side is delivered a `._remote_error:
 | 
					        - a caller 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 "child" side case it can also be
 | 
					        Only as an FYI, in the "callee" 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 child side calls `.cancel()`, `._scope.cancel()`
 | 
					        - when a callee 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 parent side.
 | 
					          sent to the caller 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
 | 
				
			||||||
| 
						 | 
					@ -635,71 +632,6 @@ class Context:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        await self.chan.send(Stop(cid=self.cid))
 | 
					        await self.chan.send(Stop(cid=self.cid))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def parent_task(self) -> trio.Task:
 | 
					 | 
				
			||||||
        '''
 | 
					 | 
				
			||||||
        This IPC context's "owning task" which is a `trio.Task`
 | 
					 | 
				
			||||||
        on one of the "sides" of the IPC.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Note that the "parent_" prefix here refers to the local
 | 
					 | 
				
			||||||
        `trio` task tree using the same interface as
 | 
					 | 
				
			||||||
        `trio.Nursery.parent_task` whereas for IPC contexts,
 | 
					 | 
				
			||||||
        a different cross-actor task hierarchy exists:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        - a "parent"-side which originally entered
 | 
					 | 
				
			||||||
          `Portal.open_context()`,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        - the "child"-side which was spawned and scheduled to invoke
 | 
					 | 
				
			||||||
          a function decorated with `@tractor.context`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        This task is thus a handle to mem-domain-distinct/per-process
 | 
					 | 
				
			||||||
        `Nursery.parent_task` depending on in which of the above
 | 
					 | 
				
			||||||
        "sides" this context exists.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        '''
 | 
					 | 
				
			||||||
        return self._task
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _is_blocked_on_rx_chan(self) -> bool:
 | 
					 | 
				
			||||||
        '''
 | 
					 | 
				
			||||||
        Predicate to indicate whether the owner `._task: trio.Task` is
 | 
					 | 
				
			||||||
        currently blocked (by `.receive()`-ing) on its underlying RPC
 | 
					 | 
				
			||||||
        feeder `._rx_chan`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        This knowledge is highly useful when handling so called
 | 
					 | 
				
			||||||
        "out-of-band" (OoB) cancellation conditions where a peer
 | 
					 | 
				
			||||||
        actor's task transmitted some remote error/cancel-msg and we
 | 
					 | 
				
			||||||
        must know whether to signal-via-cancel currently executing
 | 
					 | 
				
			||||||
        "user-code" (user defined code embedded in `ctx._scope`) or
 | 
					 | 
				
			||||||
        simply to forward the IPC-msg-as-error **without calling**
 | 
					 | 
				
			||||||
        `._scope.cancel()`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        In the latter case it is presumed that if the owner task is
 | 
					 | 
				
			||||||
        blocking for the next IPC msg, it will eventually receive,
 | 
					 | 
				
			||||||
        process and raise the equivalent local error **without**
 | 
					 | 
				
			||||||
        requiring `._scope.cancel()` to be explicitly called by the
 | 
					 | 
				
			||||||
        *delivering OoB RPC-task* (via `_deliver_msg()`).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        '''
 | 
					 | 
				
			||||||
        # NOTE, see the mem-chan meth-impls for *why* this
 | 
					 | 
				
			||||||
        # logic works,
 | 
					 | 
				
			||||||
        # `trio._channel.MemoryReceiveChannel.receive[_nowait]()`
 | 
					 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        # XXX realize that this is NOT an
 | 
					 | 
				
			||||||
        # official/will-be-loudly-deprecated API:
 | 
					 | 
				
			||||||
        # - https://trio.readthedocs.io/en/stable/reference-lowlevel.html#trio.lowlevel.Task.custom_sleep_data
 | 
					 | 
				
			||||||
        #  |_https://trio.readthedocs.io/en/stable/reference-lowlevel.html#trio.lowlevel.wait_task_rescheduled
 | 
					 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        # orig repo intro in the mem-chan change over patch:
 | 
					 | 
				
			||||||
        # - https://github.com/python-trio/trio/pull/586#issuecomment-414039117
 | 
					 | 
				
			||||||
        #  |_https://github.com/python-trio/trio/pull/616
 | 
					 | 
				
			||||||
        #  |_https://github.com/njsmith/trio/commit/98c38cef6f62e731bf8c7190e8756976bface8f0
 | 
					 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
            self._task.custom_sleep_data
 | 
					 | 
				
			||||||
            is
 | 
					 | 
				
			||||||
            self._rx_chan
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def _maybe_cancel_and_set_remote_error(
 | 
					    def _maybe_cancel_and_set_remote_error(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        error: BaseException,
 | 
					        error: BaseException,
 | 
				
			||||||
| 
						 | 
					@ -731,7 +663,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 parent has previously unwrapped
 | 
					          - It is expected that the caller 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*.
 | 
				
			||||||
| 
						 | 
					@ -741,7 +673,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 parent handling (eg.
 | 
					            the remote error and engage any caller 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
 | 
				
			||||||
| 
						 | 
					@ -808,8 +740,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -852,27 +782,13 @@ class Context:
 | 
				
			||||||
        if self._canceller is None:
 | 
					        if self._canceller is None:
 | 
				
			||||||
            log.error('Ctx has no canceller set!?')
 | 
					            log.error('Ctx has no canceller set!?')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cs: trio.CancelScope = self._scope
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # ?TODO? see comment @ .start_remote_task()`
 | 
					 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        # if not cs:
 | 
					 | 
				
			||||||
        #     from .devx import mk_pdb
 | 
					 | 
				
			||||||
        #     mk_pdb().set_trace()
 | 
					 | 
				
			||||||
        #     raise RuntimeError(
 | 
					 | 
				
			||||||
        #         f'IPC ctx was not be opened prior to remote error delivery !?\n'
 | 
					 | 
				
			||||||
        #         f'{self}\n'
 | 
					 | 
				
			||||||
        #         f'\n'
 | 
					 | 
				
			||||||
        #         f'`Portal.open_context()` must be entered (somewhere) beforehand!\n'
 | 
					 | 
				
			||||||
        #     )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Cancel the local `._scope`, catch that
 | 
					        # Cancel the local `._scope`, catch that
 | 
				
			||||||
        # `._scope.cancelled_caught` and re-raise any remote error
 | 
					        # `._scope.cancelled_caught` and re-raise any remote error
 | 
				
			||||||
        # once exiting (or manually calling `.wait_for_result()`) the
 | 
					        # once exiting (or manually calling `.wait_for_result()`) the
 | 
				
			||||||
        # `.open_context()`  block.
 | 
					        # `.open_context()`  block.
 | 
				
			||||||
 | 
					        cs: trio.CancelScope = self._scope
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            cs
 | 
					            cs
 | 
				
			||||||
            and not cs.cancel_called
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # XXX this is an expected cancel request response
 | 
					            # XXX this is an expected cancel request response
 | 
				
			||||||
            # message and we **don't need to raise it** in the
 | 
					            # message and we **don't need to raise it** in the
 | 
				
			||||||
| 
						 | 
					@ -881,7 +797,8 @@ class Context:
 | 
				
			||||||
            # if `._cancel_called` then `.cancel_acked and .cancel_called`
 | 
					            # if `._cancel_called` then `.cancel_acked and .cancel_called`
 | 
				
			||||||
            # always should be set.
 | 
					            # always should be set.
 | 
				
			||||||
            and not self._is_self_cancelled()
 | 
					            and not self._is_self_cancelled()
 | 
				
			||||||
            # and not cs.cancelled_caught
 | 
					            and not cs.cancel_called
 | 
				
			||||||
 | 
					            and not cs.cancelled_caught
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            if (
 | 
					            if (
 | 
				
			||||||
                msgerr
 | 
					                msgerr
 | 
				
			||||||
| 
						 | 
					@ -892,7 +809,7 @@ class Context:
 | 
				
			||||||
                not self._cancel_on_msgerr
 | 
					                not self._cancel_on_msgerr
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                message: str = (
 | 
					                message: str = (
 | 
				
			||||||
                    f'NOT Cancelling `Context._scope` since,\n'
 | 
					                    'NOT Cancelling `Context._scope` since,\n'
 | 
				
			||||||
                    f'Context._cancel_on_msgerr = {self._cancel_on_msgerr}\n\n'
 | 
					                    f'Context._cancel_on_msgerr = {self._cancel_on_msgerr}\n\n'
 | 
				
			||||||
                    f'AND we got a msg-type-error!\n'
 | 
					                    f'AND we got a msg-type-error!\n'
 | 
				
			||||||
                    f'{error}\n'
 | 
					                    f'{error}\n'
 | 
				
			||||||
| 
						 | 
					@ -902,43 +819,13 @@ class Context:
 | 
				
			||||||
                # `trio.Cancelled` subtype here ;)
 | 
					                # `trio.Cancelled` subtype here ;)
 | 
				
			||||||
                # https://github.com/goodboy/tractor/issues/368
 | 
					                # https://github.com/goodboy/tractor/issues/368
 | 
				
			||||||
                message: str = 'Cancelling `Context._scope` !\n\n'
 | 
					                message: str = 'Cancelling `Context._scope` !\n\n'
 | 
				
			||||||
                cs.cancel()
 | 
					                # from .devx import pause_from_sync
 | 
				
			||||||
 | 
					                # pause_from_sync()
 | 
				
			||||||
        # TODO, explicit condition for OoB (self-)cancellation?
 | 
					                self._scope.cancel()
 | 
				
			||||||
        # - we called `Portal.cancel_actor()` from this actor
 | 
					        else:
 | 
				
			||||||
        #   and the peer ctx task delivered ctxc due to it.
 | 
					            message: str = 'NOT cancelling `Context._scope` !\n\n'
 | 
				
			||||||
        # - currently `self._is_self_cancelled()` will be true
 | 
					 | 
				
			||||||
        #   since the ctxc.canceller check will match us even though it
 | 
					 | 
				
			||||||
        #   wasn't from this ctx specifically!
 | 
					 | 
				
			||||||
        elif (
 | 
					 | 
				
			||||||
            cs
 | 
					 | 
				
			||||||
            and self._is_self_cancelled()
 | 
					 | 
				
			||||||
            and not cs.cancel_called
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            message: str = (
 | 
					 | 
				
			||||||
                'Cancelling `ctx._scope` due to OoB self-cancel ?!\n'
 | 
					 | 
				
			||||||
                '\n'
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            # from .devx import mk_pdb
 | 
					            # from .devx import mk_pdb
 | 
				
			||||||
            # mk_pdb().set_trace()
 | 
					            # mk_pdb().set_trace()
 | 
				
			||||||
            # TODO XXX, required to fix timeout failure in
 | 
					 | 
				
			||||||
            # `test_cancelled_lockacquire_in_ipctx_not_unmaskeed`
 | 
					 | 
				
			||||||
            #
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # XXX NOTE XXX, this is SUPER SUBTLE!
 | 
					 | 
				
			||||||
            # we only want to cancel our embedded `._scope`
 | 
					 | 
				
			||||||
            # if the ctx's current/using task is NOT blocked
 | 
					 | 
				
			||||||
            # on `._rx_chan.receive()` and on some other
 | 
					 | 
				
			||||||
            # `trio`-checkpoint since in the former case
 | 
					 | 
				
			||||||
            # any `._remote_error` will be relayed through
 | 
					 | 
				
			||||||
            # the rx-chan and appropriately raised by the owning
 | 
					 | 
				
			||||||
            # `._task` directly. IF the owner task is however
 | 
					 | 
				
			||||||
            # blocking elsewhere we need to interrupt it **now**.
 | 
					 | 
				
			||||||
            if not self._is_blocked_on_rx_chan():
 | 
					 | 
				
			||||||
                cs.cancel()
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            # rx_stats = self._rx_chan.statistics()
 | 
					 | 
				
			||||||
            message: str = 'NOT cancelling `Context._scope` !\n\n'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        fmt_str: str = 'No `self._scope: CancelScope` was set/used ?\n'
 | 
					        fmt_str: str = 'No `self._scope: CancelScope` was set/used ?\n'
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
| 
						 | 
					@ -962,7 +849,6 @@ class Context:
 | 
				
			||||||
                +
 | 
					                +
 | 
				
			||||||
                cs_fmt
 | 
					                cs_fmt
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					 | 
				
			||||||
        log.cancel(
 | 
					        log.cancel(
 | 
				
			||||||
            message
 | 
					            message
 | 
				
			||||||
            +
 | 
					            +
 | 
				
			||||||
| 
						 | 
					@ -995,11 +881,6 @@ 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 (
 | 
				
			||||||
| 
						 | 
					@ -1013,7 +894,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 fn!
 | 
					        # TODO: use `.dev._frame_stack` scanning to find caller!
 | 
				
			||||||
        # ci: CallerInfo|None = self._caller_info
 | 
					        # ci: CallerInfo|None = self._caller_info
 | 
				
			||||||
        # if ci:
 | 
					        # if ci:
 | 
				
			||||||
        #     return (
 | 
					        #     return (
 | 
				
			||||||
| 
						 | 
					@ -1048,19 +929,18 @@ 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 parent (who entered the `Portal.open_context()`)
 | 
					        If the caller (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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        side: str = self.side
 | 
					        side: str = self.side
 | 
				
			||||||
        self._cancel_called = True
 | 
					        # XXX for debug via the `@.setter`
 | 
				
			||||||
        # ^ XXX for debug via the `@.setter`
 | 
					        self.cancel_called = True
 | 
				
			||||||
        # self.cancel_called = True
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        header: str = (
 | 
					        header: str = (
 | 
				
			||||||
            f'Cancelling ctx from {side!r}-side\n'
 | 
					            f'Cancelling ctx from {side.upper()}-side\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        reminfo: str = (
 | 
					        reminfo: str = (
 | 
				
			||||||
            # ' =>\n'
 | 
					            # ' =>\n'
 | 
				
			||||||
| 
						 | 
					@ -1068,7 +948,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
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -1121,6 +1001,7 @@ 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}'
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1131,7 +1012,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 parent expects a `ContextCancelled` to be sent
 | 
					        # since the caller 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()`
 | 
				
			||||||
| 
						 | 
					@ -1188,25 +1069,9 @@ 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 and
 | 
					        Maybe raise a remote error depending on the type of error
 | 
				
			||||||
        *who*, i.e. which side of the task pair across actors,
 | 
					        and *who* (i.e. which task from which actor) requested
 | 
				
			||||||
        requested a cancellation (if any).
 | 
					        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
 | 
				
			||||||
| 
						 | 
					@ -1248,19 +1113,18 @@ class Context:
 | 
				
			||||||
            # for this ^, NO right?
 | 
					            # for this ^, NO right?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ) or (
 | 
					        ) or (
 | 
				
			||||||
            # NOTE: whenever this side is the cause of an
 | 
					            # NOTE: whenever this context is the cause of an
 | 
				
			||||||
            # overrun on the peer side, i.e. we sent msgs too
 | 
					            # overrun on the remote side (aka we sent msgs too
 | 
				
			||||||
            # fast and the peer task was overrun according
 | 
					            # fast that the remote task was overrun according
 | 
				
			||||||
            # to `MsgStream` buffer settings, AND this was
 | 
					            # to `MsgStream` buffer settings) AND the caller
 | 
				
			||||||
            # called with `raise_overrun_from_self=True` (the
 | 
					            # has requested to not raise overruns this side
 | 
				
			||||||
            # default), silently absorb any `StreamOverrun`.
 | 
					            # caused, we also silently absorb any remotely
 | 
				
			||||||
            #
 | 
					            # boxed `StreamOverrun`. This is mostly useful for
 | 
				
			||||||
            # XXX, this is namely useful for supressing such faults
 | 
					            # supressing such faults during
 | 
				
			||||||
            # during cancellation/error/final-result handling inside
 | 
					            # 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 during a cancellation-request, i.e. when
 | 
					            # raise such errors particularly in the case where
 | 
				
			||||||
            # `._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
 | 
				
			||||||
| 
						 | 
					@ -1304,8 +1168,8 @@ class Context:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> Any|Exception:
 | 
					    ) -> Any|Exception:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        From some (parent) side task, wait for and return the final
 | 
					        From some (caller) side task, wait for and return the final
 | 
				
			||||||
        result from the remote (child) side's task.
 | 
					        result from the remote (callee) 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.
 | 
				
			||||||
| 
						 | 
					@ -1370,8 +1234,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
 | 
				
			||||||
| 
						 | 
					@ -1601,12 +1465,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -1720,7 +1578,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 parent?
 | 
					        # here in caller?
 | 
				
			||||||
        await self.chan.send(started_msg)
 | 
					        await self.chan.send(started_msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # set msg-related internal runtime-state
 | 
					        # set msg-related internal runtime-state
 | 
				
			||||||
| 
						 | 
					@ -1796,7 +1654,7 @@ class Context:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
         XXX RULES XXX
 | 
					         XXX RULES XXX
 | 
				
			||||||
        ------ - ------
 | 
					        ------ - ------
 | 
				
			||||||
        - NEVER raise remote errors from this method; a calling runtime-task.
 | 
					        - NEVER raise remote errors from this method; a runtime task caller.
 | 
				
			||||||
          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.
 | 
				
			||||||
| 
						 | 
					@ -1872,7 +1730,7 @@ class Context:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                report = (
 | 
					                report = (
 | 
				
			||||||
                    'Queueing OVERRUN msg on parent task:\n\n'
 | 
					                    'Queueing OVERRUN msg on caller task:\n\n'
 | 
				
			||||||
                    + report
 | 
					                    + report
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                log.debug(report)
 | 
					                log.debug(report)
 | 
				
			||||||
| 
						 | 
					@ -2068,12 +1926,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 "child" task via a call
 | 
					    and any first value "sent" by the "callee" 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 "child" task calls
 | 
					    context does not unblock until the "callee" task calls
 | 
				
			||||||
    `.started()` in similar style to `trio.Nursery.start()`.
 | 
					    `.started()` in similar style to `trio.Nursery.start()`.
 | 
				
			||||||
    When the "child" (side that is "called"/started by a call
 | 
					    When the "callee" (side that is "called"/started by a call
 | 
				
			||||||
    to *this* method) returns, the parent side (this) unblocks
 | 
					    to *this* method) returns, the caller 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2086,7 +1944,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 parent code in logging
 | 
					    # introspection where we report the caller 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
 | 
				
			||||||
| 
						 | 
					@ -2121,9 +1979,6 @@ async def open_context_from_portal(
 | 
				
			||||||
            f'|_{portal.actor}\n'
 | 
					            f'|_{portal.actor}\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ?TODO? could we move this to inside the `tn` block?
 | 
					 | 
				
			||||||
    # -> would allow doing `ctx.parent_task = tn.parent_task` ?
 | 
					 | 
				
			||||||
    # -> would allow a `if not ._scope: => raise RTE` ?
 | 
					 | 
				
			||||||
    ctx: Context = await portal.actor.start_remote_task(
 | 
					    ctx: Context = await portal.actor.start_remote_task(
 | 
				
			||||||
        portal.channel,
 | 
					        portal.channel,
 | 
				
			||||||
        nsf=nsf,
 | 
					        nsf=nsf,
 | 
				
			||||||
| 
						 | 
					@ -2148,12 +2003,13 @@ 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_child: ContextCancelled|None = None
 | 
					    ctxc_from_callee: ContextCancelled|None = None
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        # from .devx import pause
 | 
					 | 
				
			||||||
        async with (
 | 
					        async with (
 | 
				
			||||||
            collapse_eg(),
 | 
					            trio.open_nursery(
 | 
				
			||||||
            trio.open_nursery() as tn,
 | 
					                strict_exception_groups=False,
 | 
				
			||||||
 | 
					            ) 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'),
 | 
				
			||||||
| 
						 | 
					@ -2173,10 +2029,6 @@ async def open_context_from_portal(
 | 
				
			||||||
            # the dialog, the `Error` msg should be raised from the `msg`
 | 
					            # the dialog, the `Error` msg should be raised from the `msg`
 | 
				
			||||||
            # handling block below.
 | 
					            # handling block below.
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                log.runtime(
 | 
					 | 
				
			||||||
                    f'IPC ctx parent waiting on Started msg..\n'
 | 
					 | 
				
			||||||
                    f'ctx.cid: {ctx.cid!r}\n'
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                started_msg, first = await ctx._pld_rx.recv_msg(
 | 
					                started_msg, first = await ctx._pld_rx.recv_msg(
 | 
				
			||||||
                    ipc=ctx,
 | 
					                    ipc=ctx,
 | 
				
			||||||
                    expect_msg=Started,
 | 
					                    expect_msg=Started,
 | 
				
			||||||
| 
						 | 
					@ -2185,16 +2037,16 @@ async def open_context_from_portal(
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            except trio.Cancelled as taskc:
 | 
					            except trio.Cancelled as taskc:
 | 
				
			||||||
                ctx_cs: trio.CancelScope = ctx._scope
 | 
					                ctx_cs: trio.CancelScope = ctx._scope
 | 
				
			||||||
                log.cancel(
 | 
					 | 
				
			||||||
                    f'IPC ctx was cancelled during "child" task sync due to\n\n'
 | 
					 | 
				
			||||||
                    f'.cid: {ctx.cid!r}\n'
 | 
					 | 
				
			||||||
                    f'.maybe_error: {ctx.maybe_error!r}\n'
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                # await pause(shield=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if not ctx_cs.cancel_called:
 | 
					                if not ctx_cs.cancel_called:
 | 
				
			||||||
                    raise
 | 
					                    raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # from .devx import pause
 | 
				
			||||||
 | 
					                # await pause(shield=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                log.cancel(
 | 
				
			||||||
 | 
					                    'IPC ctx was cancelled during "child" task sync due to\n\n'
 | 
				
			||||||
 | 
					                    f'{ctx.maybe_error}\n'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
                # OW if the ctx's scope was cancelled manually,
 | 
					                # OW if the ctx's scope was cancelled manually,
 | 
				
			||||||
                # likely the `Context` was cancelled via a call to
 | 
					                # likely the `Context` was cancelled via a call to
 | 
				
			||||||
                # `._maybe_cancel_and_set_remote_error()` so ensure
 | 
					                # `._maybe_cancel_and_set_remote_error()` so ensure
 | 
				
			||||||
| 
						 | 
					@ -2232,7 +2084,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 parent exiting and
 | 
					            # maybe TODO NOTE: between the caller 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
 | 
				
			||||||
| 
						 | 
					@ -2298,16 +2150,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 "parent" task somewhere invoked `Context.cancel()`
 | 
					    # 2-THIS "caller" task somewhere invoked `Context.cancel()`
 | 
				
			||||||
    #   and received a `ContextCanclled` from the "child"
 | 
					    #   and received a `ContextCanclled` from the "callee"
 | 
				
			||||||
    #   task, in which case we mask the `ContextCancelled` from
 | 
					    #   task, in which case we mask the `ContextCancelled` from
 | 
				
			||||||
    #   bubbling to this "parent" (much like how `trio.Nursery`
 | 
					    #   bubbling to this "caller" (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_child = ctxc
 | 
					        ctxc_from_callee = 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
 | 
				
			||||||
| 
						 | 
					@ -2318,7 +2170,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
 | 
				
			||||||
| 
						 | 
					@ -2344,11 +2196,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 "child"-side remote error, possibly also a cancellation
 | 
					    # 2. any "callee"-side remote error, possibly also a cancellation
 | 
				
			||||||
    #    request by some peer,
 | 
					    #    request by some peer,
 | 
				
			||||||
    # 3. any "parent" (aka THIS scope's) local error raised in the above `yield`
 | 
					    # 3. any "caller" (aka THIS scope's) local error raised in the above `yield`
 | 
				
			||||||
    except (
 | 
					    except (
 | 
				
			||||||
        # CASE 3: standard local error in this parent/yieldee
 | 
					        # CASE 3: standard local error in this caller/yieldee
 | 
				
			||||||
        Exception,
 | 
					        Exception,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # CASES 1 & 2: can manifest as a `ctx._scope_nursery`
 | 
					        # CASES 1 & 2: can manifest as a `ctx._scope_nursery`
 | 
				
			||||||
| 
						 | 
					@ -2362,9 +2214,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 "child" side
 | 
					        #    from any error delivered from the "callee" 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 "parent" / opener
 | 
					        #    tasks started *here* in the "caller" / 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
 | 
				
			||||||
| 
						 | 
					@ -2377,37 +2229,34 @@ 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 rent_err:
 | 
					    ) as caller_err:
 | 
				
			||||||
        scope_err = rent_err
 | 
					        scope_err = caller_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
 | 
				
			||||||
                cause: str = 'cancelled'
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # XXX explicitly report on any non-graceful-taskc cases
 | 
					            # XXX explicitly report on any non-graceful-taskc cases
 | 
				
			||||||
            case _:
 | 
					            case _:
 | 
				
			||||||
                cause: str = 'errored'
 | 
					 | 
				
			||||||
                logmeth = log.exception
 | 
					                logmeth = log.exception
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        logmeth(
 | 
					        logmeth(
 | 
				
			||||||
            f'ctx {ctx.side!r}-side {cause!r} with,\n'
 | 
					            f'ctx {ctx.side!r}-side exited with {ctx.repr_outcome()}\n'
 | 
				
			||||||
            f'{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 '
 | 
				
			||||||
| 
						 | 
					@ -2420,11 +2269,10 @@ async def open_context_from_portal(
 | 
				
			||||||
                    'Calling `ctx.cancel()`!\n'
 | 
					                    'Calling `ctx.cancel()`!\n'
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # we don't need to cancel the child if it already
 | 
					        # we don't need to cancel the callee if it already
 | 
				
			||||||
        # told us it's cancelled ;p
 | 
					        # told us it's cancelled ;p
 | 
				
			||||||
        if ctxc_from_child is None:
 | 
					        if ctxc_from_callee is None:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                # await pause(shield=True)
 | 
					 | 
				
			||||||
                await ctx.cancel()
 | 
					                await ctx.cancel()
 | 
				
			||||||
            except (
 | 
					            except (
 | 
				
			||||||
                trio.BrokenResourceError,
 | 
					                trio.BrokenResourceError,
 | 
				
			||||||
| 
						 | 
					@ -2454,8 +2302,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 "child" side fails and causes
 | 
					            # ALWAYS SET any time "callee" side fails and causes "caller
 | 
				
			||||||
            # "parent side" cancellation via a `ContextCancelled` here.
 | 
					            # 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:
 | 
				
			||||||
| 
						 | 
					@ -2471,8 +2319,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)
 | 
				
			||||||
| 
						 | 
					@ -2491,7 +2339,7 @@ async def open_context_from_portal(
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                case (None, _):
 | 
					                case (None, _):
 | 
				
			||||||
                    log.runtime(
 | 
					                    log.runtime(
 | 
				
			||||||
                        'Context returned final result from child task:\n'
 | 
					                        'Context returned final result from callee task:\n'
 | 
				
			||||||
                        f'<= peer: {uid}\n'
 | 
					                        f'<= peer: {uid}\n'
 | 
				
			||||||
                        f'  |_ {nsf}()\n\n'
 | 
					                        f'  |_ {nsf}()\n\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2507,7 +2355,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
 | 
				
			||||||
| 
						 | 
					@ -2581,14 +2429,12 @@ async def open_context_from_portal(
 | 
				
			||||||
                log.cancel(
 | 
					                log.cancel(
 | 
				
			||||||
                    f'Context cancelled by local {ctx.side!r}-side task\n'
 | 
					                    f'Context cancelled by local {ctx.side!r}-side task\n'
 | 
				
			||||||
                    f'c)>\n'
 | 
					                    f'c)>\n'
 | 
				
			||||||
                    f'  |_{ctx.parent_task}\n'
 | 
					                    f' |_{ctx._task}\n\n'
 | 
				
			||||||
                    f'   .cid={ctx.cid!r}\n'
 | 
					                    f'{repr(scope_err)}\n'
 | 
				
			||||||
                    f'\n'
 | 
					 | 
				
			||||||
                    f'{scope_err!r}\n'
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # TODO: should we add a `._cancel_req_received`
 | 
					            # TODO: should we add a `._cancel_req_received`
 | 
				
			||||||
            # flag to determine if the child manually called
 | 
					            # flag to determine if the callee manually called
 | 
				
			||||||
            # `ctx.cancel()`?
 | 
					            # `ctx.cancel()`?
 | 
				
			||||||
            # -[ ] going to need a cid check no?
 | 
					            # -[ ] going to need a cid check no?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2644,7 +2490,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 parent-info if log level so high!
 | 
					    # TODO: only scan caller-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,10 +28,7 @@ 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 (
 | 
					from .trionics import gather_contexts
 | 
				
			||||||
    gather_contexts,
 | 
					 | 
				
			||||||
    collapse_eg,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from .ipc import _connect_chan, Channel
 | 
					from .ipc import _connect_chan, Channel
 | 
				
			||||||
from ._addr import (
 | 
					from ._addr import (
 | 
				
			||||||
    UnwrappedAddress,
 | 
					    UnwrappedAddress,
 | 
				
			||||||
| 
						 | 
					@ -51,6 +48,7 @@ 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__)
 | 
				
			||||||
| 
						 | 
					@ -82,7 +80,7 @@ async def get_registry(
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        # TODO: try to look pre-existing connection from
 | 
					        # TODO: try to look pre-existing connection from
 | 
				
			||||||
        # `Server._peers` and use it instead?
 | 
					        # `IPCServer._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,
 | 
				
			||||||
| 
						 | 
					@ -90,6 +88,7 @@ async def get_registry(
 | 
				
			||||||
            yield regstr_ptl
 | 
					            yield regstr_ptl
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@acm
 | 
					@acm
 | 
				
			||||||
async def get_root(
 | 
					async def get_root(
 | 
				
			||||||
    **kwargs,
 | 
					    **kwargs,
 | 
				
			||||||
| 
						 | 
					@ -113,14 +112,14 @@ 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 `Server._peers: dict`.
 | 
					    and return any channels from `IPCServer._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()
 | 
				
			||||||
    to_scan: dict[tuple, list[Channel]] = actor.ipc_server._peers.copy()
 | 
					    to_scan: dict[tuple, list[Channel]] = actor._peers.copy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # TODO: is this ever needed? creates a duplicate channel on actor._peers
 | 
					    # TODO: is this ever needed? creates a duplicate channel on actor._peers
 | 
				
			||||||
    # when multiple find_actor calls are made to same actor from a single ctx
 | 
					    # when multiple find_actor calls are made to same actor from a single ctx
 | 
				
			||||||
| 
						 | 
					@ -255,12 +254,9 @@ async def find_actor(
 | 
				
			||||||
        for addr in registry_addrs
 | 
					        for addr in registry_addrs
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    portals: list[Portal]
 | 
					    portals: list[Portal]
 | 
				
			||||||
    async with (
 | 
					    async with gather_contexts(
 | 
				
			||||||
        collapse_eg(),
 | 
					        mngrs=maybe_portals,
 | 
				
			||||||
        gather_contexts(
 | 
					    ) as portals:
 | 
				
			||||||
            mngrs=maybe_portals,
 | 
					 | 
				
			||||||
        ) as portals,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        # log.runtime(
 | 
					        # log.runtime(
 | 
				
			||||||
        #     'Gathered portals:\n'
 | 
					        #     'Gathered portals:\n'
 | 
				
			||||||
        #     f'{portals}'
 | 
					        #     f'{portals}'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,8 @@ 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
 | 
				
			||||||
 | 
					import textwrap
 | 
				
			||||||
from typing import (
 | 
					from typing import (
 | 
				
			||||||
    Any,
 | 
					    Any,
 | 
				
			||||||
    TYPE_CHECKING,
 | 
					    TYPE_CHECKING,
 | 
				
			||||||
| 
						 | 
					@ -34,11 +35,7 @@ from .log import (
 | 
				
			||||||
    get_logger,
 | 
					    get_logger,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from . import _state
 | 
					from . import _state
 | 
				
			||||||
from .devx import (
 | 
					from .devx import _debug
 | 
				
			||||||
    _frame_stack,
 | 
					 | 
				
			||||||
    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 (
 | 
				
			||||||
| 
						 | 
					@ -106,6 +103,107 @@ def _mp_main(
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TODO: move this func to some kinda `.devx._conc_lang.py` eventually
 | 
				
			||||||
 | 
					# as we work out our multi-domain state-flow-syntax!
 | 
				
			||||||
 | 
					def nest_from_op(
 | 
				
			||||||
 | 
					    input_op: str,
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # ?TODO? an idea for a syntax to the state of concurrent systems
 | 
				
			||||||
 | 
					    # as a "3-domain" (execution, scope, storage) model and using
 | 
				
			||||||
 | 
					    # a minimal ascii/utf-8 operator-set.
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # try not to take any of this seriously yet XD
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # > is a "play operator" indicating (CPU bound)
 | 
				
			||||||
 | 
					    #   exec/work/ops required at the "lowest level computing"
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # execution primititves (tasks, threads, actors..) denote their
 | 
				
			||||||
 | 
					    # lifetime with '(' and ')' since parentheses normally are used
 | 
				
			||||||
 | 
					    # in many langs to denote function calls.
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # starting = (
 | 
				
			||||||
 | 
					    # >(  opening/starting; beginning of the thread-of-exec (toe?)
 | 
				
			||||||
 | 
					    # (>  opened/started,  (finished spawning toe)
 | 
				
			||||||
 | 
					    # |_<Task: blah blah..>  repr of toe, in py these look like <objs>
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # >) closing/exiting/stopping,
 | 
				
			||||||
 | 
					    # )> closed/exited/stopped,
 | 
				
			||||||
 | 
					    # |_<Task: blah blah..>
 | 
				
			||||||
 | 
					    #   [OR <), )< ?? ]
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # ending = )
 | 
				
			||||||
 | 
					    # >c) cancelling to close/exit
 | 
				
			||||||
 | 
					    # c)> cancelled (caused close), OR?
 | 
				
			||||||
 | 
					    #  |_<Actor: ..>
 | 
				
			||||||
 | 
					    #   OR maybe "<c)" which better indicates the cancel being
 | 
				
			||||||
 | 
					    #   "delivered/returned" / returned" to LHS?
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # >x)  erroring to eventuall exit
 | 
				
			||||||
 | 
					    # x)>  errored and terminated
 | 
				
			||||||
 | 
					    #  |_<Actor: ...>
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # scopes: supers/nurseries, IPC-ctxs, sessions, perms, etc.
 | 
				
			||||||
 | 
					    # >{  opening
 | 
				
			||||||
 | 
					    # {>  opened
 | 
				
			||||||
 | 
					    # }>  closed
 | 
				
			||||||
 | 
					    # >}  closing
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # storage: like queues, shm-buffers, files, etc..
 | 
				
			||||||
 | 
					    # >[  opening
 | 
				
			||||||
 | 
					    # [>  opened
 | 
				
			||||||
 | 
					    #  |_<FileObj: ..>
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # >]  closing
 | 
				
			||||||
 | 
					    # ]>  closed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # IPC ops: channels, transports, msging
 | 
				
			||||||
 | 
					    # =>  req msg
 | 
				
			||||||
 | 
					    # <=  resp msg
 | 
				
			||||||
 | 
					    # <=> 2-way streaming (of msgs)
 | 
				
			||||||
 | 
					    # <-  recv 1 msg
 | 
				
			||||||
 | 
					    # ->  send 1 msg
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # TODO: still not sure on R/L-HS approach..?
 | 
				
			||||||
 | 
					    # =>(  send-req to exec start (task, actor, thread..)
 | 
				
			||||||
 | 
					    # (<=  recv-req to ^
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # (<=  recv-req ^
 | 
				
			||||||
 | 
					    # <=(  recv-resp opened remote exec primitive
 | 
				
			||||||
 | 
					    # <=)  recv-resp closed
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # )<=c req to stop due to cancel
 | 
				
			||||||
 | 
					    # c=>) req to stop due to cancel
 | 
				
			||||||
 | 
					    #
 | 
				
			||||||
 | 
					    # =>{  recv-req to open
 | 
				
			||||||
 | 
					    # <={  send-status that it closed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tree_str: str,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # NOTE: so move back-from-the-left of the `input_op` by
 | 
				
			||||||
 | 
					    # this amount.
 | 
				
			||||||
 | 
					    back_from_op: int = 0,
 | 
				
			||||||
 | 
					) -> 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    '''
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        f'{input_op}\n'
 | 
				
			||||||
 | 
					        +
 | 
				
			||||||
 | 
					        textwrap.indent(
 | 
				
			||||||
 | 
					            tree_str,
 | 
				
			||||||
 | 
					            prefix=(
 | 
				
			||||||
 | 
					                len(input_op)
 | 
				
			||||||
 | 
					                -
 | 
				
			||||||
 | 
					                (back_from_op + 1)
 | 
				
			||||||
 | 
					            ) * ' ',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def _trio_main(
 | 
					def _trio_main(
 | 
				
			||||||
    actor: Actor,
 | 
					    actor: Actor,
 | 
				
			||||||
    *,
 | 
					    *,
 | 
				
			||||||
| 
						 | 
					@ -117,7 +215,7 @@ def _trio_main(
 | 
				
			||||||
    Entry point for a `trio_run_in_process` subactor.
 | 
					    Entry point for a `trio_run_in_process` subactor.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    _frame_stack.hide_runtime_frames()
 | 
					    _debug.hide_runtime_frames()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _state._current_actor = actor
 | 
					    _state._current_actor = actor
 | 
				
			||||||
    trio_main = partial(
 | 
					    trio_main = partial(
 | 
				
			||||||
| 
						 | 
					@ -128,23 +226,30 @@ 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(
 | 
				
			||||||
            f'Starting `trio` subactor from parent @ '
 | 
					            'Starting new `trio` subactor:\n'
 | 
				
			||||||
            f'{parent_addr}\n'
 | 
					 | 
				
			||||||
            +
 | 
					            +
 | 
				
			||||||
            pformat.nest_from_op(
 | 
					            nest_from_op(
 | 
				
			||||||
                input_op='>(',  # see syntax ideas above
 | 
					                input_op='>(',  # see syntax ideas above
 | 
				
			||||||
                text=f'{actor}',
 | 
					                tree_str=actor_info,
 | 
				
			||||||
 | 
					                back_from_op=2,  # since "complete"
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    logmeth = log.info
 | 
					    logmeth = log.info
 | 
				
			||||||
    exit_status: str = (
 | 
					    exit_status: str = (
 | 
				
			||||||
        'Subactor exited\n'
 | 
					        'Subactor exited\n'
 | 
				
			||||||
        +
 | 
					        +
 | 
				
			||||||
        pformat.nest_from_op(
 | 
					        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
 | 
				
			||||||
            text=f'{actor}',
 | 
					            tree_str=actor_info,
 | 
				
			||||||
            nest_indent=1,
 | 
					            back_from_op=1,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
| 
						 | 
					@ -159,9 +264,9 @@ def _trio_main(
 | 
				
			||||||
        exit_status: str = (
 | 
					        exit_status: str = (
 | 
				
			||||||
            'Actor received KBI (aka an OS-cancel)\n'
 | 
					            'Actor received KBI (aka an OS-cancel)\n'
 | 
				
			||||||
            +
 | 
					            +
 | 
				
			||||||
            pformat.nest_from_op(
 | 
					            nest_from_op(
 | 
				
			||||||
                input_op='c)>',  # closed due to cancel (see above)
 | 
					                input_op='c)>',  # closed due to cancel (see above)
 | 
				
			||||||
                text=f'{actor}',
 | 
					                tree_str=actor_info,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    except BaseException as err:
 | 
					    except BaseException as err:
 | 
				
			||||||
| 
						 | 
					@ -169,9 +274,9 @@ def _trio_main(
 | 
				
			||||||
        exit_status: str = (
 | 
					        exit_status: str = (
 | 
				
			||||||
            'Main actor task exited due to crash?\n'
 | 
					            'Main actor task exited due to crash?\n'
 | 
				
			||||||
            +
 | 
					            +
 | 
				
			||||||
            pformat.nest_from_op(
 | 
					            nest_from_op(
 | 
				
			||||||
                input_op='x)>',  # closed by error
 | 
					                input_op='x)>',  # closed by error
 | 
				
			||||||
                text=f'{actor}',
 | 
					                tree_str=actor_info,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # 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,6 +1246,55 @@ 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,10 +39,7 @@ import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import trio
 | 
					import trio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .trionics import (
 | 
					from .trionics import maybe_open_nursery
 | 
				
			||||||
    maybe_open_nursery,
 | 
					 | 
				
			||||||
    collapse_eg,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from ._state import (
 | 
					from ._state import (
 | 
				
			||||||
    current_actor,
 | 
					    current_actor,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -118,10 +115,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -181,17 +174,10 @@ 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 to peer {peer_id} will not deliver a final result?\n'
 | 
					                f"Portal for {self.channel.aid} not expecting a final"
 | 
				
			||||||
                f'\n'
 | 
					                " result?\nresult() should only be called if subactor"
 | 
				
			||||||
                f'Context.result() can only be called by the parent of '
 | 
					                " was spawned with `ActorNursery.run_in_actor()`")
 | 
				
			||||||
                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
 | 
				
			||||||
| 
						 | 
					@ -224,7 +210,6 @@ 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(
 | 
				
			||||||
| 
						 | 
					@ -236,10 +221,8 @@ class Portal:
 | 
				
			||||||
        # terminate all locally running async generator
 | 
					        # terminate all locally running async generator
 | 
				
			||||||
        # IPC calls
 | 
					        # IPC calls
 | 
				
			||||||
        if self._streams:
 | 
					        if self._streams:
 | 
				
			||||||
            peer_id: str = f'{self.channel.aid.reprol()!r}'
 | 
					            log.cancel(
 | 
				
			||||||
            report: str = (
 | 
					                f"Cancelling all streams with {self.channel.aid}")
 | 
				
			||||||
                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()
 | 
				
			||||||
| 
						 | 
					@ -248,18 +231,10 @@ 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?)
 | 
				
			||||||
                    report += (
 | 
					                    log.debug(f"{stream} was already closed.")
 | 
				
			||||||
                        f'->) {stream!r} already closed\n'
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            log.cancel(report)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def aclose(self):
 | 
					    async def aclose(self):
 | 
				
			||||||
        log.debug(
 | 
					        log.debug(f"Closing {self}")
 | 
				
			||||||
            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
 | 
				
			||||||
| 
						 | 
					@ -285,22 +260,23 @@ 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(
 | 
				
			||||||
                'Peer {peer_id} is already disconnected\n'
 | 
					                'This channel is already closed, skipping cancel request..'
 | 
				
			||||||
                '-> skipping cancel request..\n'
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reminfo: str = (
 | 
				
			||||||
 | 
					            f'c)=> {self.channel.aid}\n'
 | 
				
			||||||
 | 
					            f'  |_{chan}\n'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        log.cancel(
 | 
					        log.cancel(
 | 
				
			||||||
            f'Sending actor-runtime-cancel-req to peer\n'
 | 
					            f'Requesting actor-runtime cancel for peer\n\n'
 | 
				
			||||||
            f'\n'
 | 
					            f'{reminfo}'
 | 
				
			||||||
            f'c)=> {peer_id}\n'
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # XXX the one spot we set it?
 | 
					        # XXX the one spot we set it?
 | 
				
			||||||
        chan._cancel_called: bool = True
 | 
					        self.channel._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
 | 
				
			||||||
| 
						 | 
					@ -321,9 +297,8 @@ 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(
 | 
				
			||||||
                    f'May have failed to cancel peer?\n'
 | 
					                    'May have failed to cancel peer?\n'
 | 
				
			||||||
                    f'\n'
 | 
					                    f'{reminfo}'
 | 
				
			||||||
                    f'c)=?> {peer_id}\n'
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # if we get here some weird cancellation case happened
 | 
					            # if we get here some weird cancellation case happened
 | 
				
			||||||
| 
						 | 
					@ -341,22 +316,22 @@ class Portal:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            TransportClosed,
 | 
					            TransportClosed,
 | 
				
			||||||
        ) as tpt_err:
 | 
					        ) as tpt_err:
 | 
				
			||||||
            ipc_borked_report: str = (
 | 
					            report: str = (
 | 
				
			||||||
                f'IPC for actor already closed/broken?\n\n'
 | 
					                f'IPC chan for actor already closed or broken?\n\n'
 | 
				
			||||||
                f'\n'
 | 
					                f'{self.channel.aid}\n'
 | 
				
			||||||
                f'c)=x> {peer_id}\n'
 | 
					                f' |_{self.channel}\n'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            match tpt_err:
 | 
					            match tpt_err:
 | 
				
			||||||
                case TransportClosed():
 | 
					                case TransportClosed():
 | 
				
			||||||
                    log.debug(ipc_borked_report)
 | 
					                    log.debug(report)
 | 
				
			||||||
                case _:
 | 
					                case _:
 | 
				
			||||||
                    ipc_borked_report += (
 | 
					                    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(ipc_borked_report)
 | 
					                    log.warning(report)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return False
 | 
					            return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -513,13 +488,10 @@ class Portal:
 | 
				
			||||||
                with trio.CancelScope(shield=True):
 | 
					                with trio.CancelScope(shield=True):
 | 
				
			||||||
                    await ctx.cancel()
 | 
					                    await ctx.cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            except trio.ClosedResourceError as cre:
 | 
					            except trio.ClosedResourceError:
 | 
				
			||||||
                # 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(
 | 
					                log.cancel(f'Context {ctx} was already closed?')
 | 
				
			||||||
                    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()
 | 
				
			||||||
| 
						 | 
					@ -586,13 +558,14 @@ async def open_portal(
 | 
				
			||||||
    assert actor
 | 
					    assert actor
 | 
				
			||||||
    was_connected: bool = False
 | 
					    was_connected: bool = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async with (
 | 
					    async with maybe_open_nursery(
 | 
				
			||||||
        collapse_eg(),
 | 
					        tn,
 | 
				
			||||||
        maybe_open_nursery(
 | 
					        shield=shield,
 | 
				
			||||||
            tn,
 | 
					        strict_exception_groups=False,
 | 
				
			||||||
            shield=shield,
 | 
					        # ^XXX^ TODO? soo roll our own then ??
 | 
				
			||||||
        ) as tn,
 | 
					        # -> since we kinda want the "if only one `.exception` then
 | 
				
			||||||
    ):
 | 
					        # just raise that" interface?
 | 
				
			||||||
 | 
					    ) as tn:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not channel.connected():
 | 
					        if not channel.connected():
 | 
				
			||||||
            await channel.connect()
 | 
					            await channel.connect()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										212
									
								
								tractor/_root.py
								
								
								
								
							
							
						
						
									
										212
									
								
								tractor/_root.py
								
								
								
								
							| 
						 | 
					@ -37,12 +37,14 @@ import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import trio
 | 
					import trio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import _runtime
 | 
					from ._runtime import (
 | 
				
			||||||
from .devx import (
 | 
					    Actor,
 | 
				
			||||||
    debug,
 | 
					    Arbiter,
 | 
				
			||||||
    _frame_stack,
 | 
					    # TODO: rename and make a non-actor subtype?
 | 
				
			||||||
    pformat as _pformat,
 | 
					    # Arbiter as Registry,
 | 
				
			||||||
 | 
					    async_main,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					from .devx import _debug
 | 
				
			||||||
from . import _spawn
 | 
					from . import _spawn
 | 
				
			||||||
from . import _state
 | 
					from . import _state
 | 
				
			||||||
from . import log
 | 
					from . import log
 | 
				
			||||||
| 
						 | 
					@ -56,19 +58,16 @@ 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?
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
| 
						 | 
					@ -90,17 +89,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'
 | 
				
			||||||
            f'Enabling `tractor.pause_from_sync()` support!\n'
 | 
					            '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
 | 
				
			||||||
| 
						 | 
					@ -164,9 +163,7 @@ 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 = False,  # `.pause_from_sync()/breakpoint()` support
 | 
					    maybe_enable_greenback: bool = True,  # `.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
 | 
				
			||||||
| 
						 | 
					@ -181,7 +178,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],
 | 
				
			||||||
| 
						 | 
					@ -192,19 +189,13 @@ 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,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> _runtime.Actor:
 | 
					) -> Actor:
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Initialize the `tractor` runtime by starting a "root actor" in
 | 
					    Runtime init entry point for ``tractor``.
 | 
				
			||||||
    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(
 | 
					    if already_actor := _state.current_actor(err_on_no_runtime=False):
 | 
				
			||||||
        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']
 | 
				
			||||||
| 
						 | 
					@ -226,23 +217,18 @@ 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!
 | 
					            # TODO! support multi-tpts per actor! Bo
 | 
				
			||||||
        # Bo
 | 
					            assert (
 | 
				
			||||||
        if not len(enable_transports) == 1:
 | 
					                len(enable_transports) == 1
 | 
				
			||||||
            raise RuntimeError(
 | 
					            ), 'No multi-tpt support yet!'
 | 
				
			||||||
                f'No multi-tpt support yet!\n'
 | 
					 | 
				
			||||||
                f'enable_transports={enable_transports!r}\n'
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        _frame_stack.hide_runtime_frames()
 | 
					        _debug.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
 | 
				
			||||||
| 
						 | 
					@ -274,20 +260,14 @@ async def open_root_actor(
 | 
				
			||||||
                DeprecationWarning,
 | 
					                DeprecationWarning,
 | 
				
			||||||
                stacklevel=2,
 | 
					                stacklevel=2,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            uw_reg_addrs = [arbiter_addr]
 | 
					            registry_addrs = [arbiter_addr]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        uw_reg_addrs = registry_addrs
 | 
					        if not registry_addrs:
 | 
				
			||||||
        if not uw_reg_addrs:
 | 
					            registry_addrs: list[UnwrappedAddress] = default_lo_addrs(
 | 
				
			||||||
            uw_reg_addrs: list[UnwrappedAddress] = default_lo_addrs(
 | 
					 | 
				
			||||||
                enable_transports
 | 
					                enable_transports
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # must exist by now since all below code is dependent
 | 
					        assert registry_addrs
 | 
				
			||||||
        assert uw_reg_addrs
 | 
					 | 
				
			||||||
        registry_addrs: list[Address] = [
 | 
					 | 
				
			||||||
            wrap_address(uw_addr)
 | 
					 | 
				
			||||||
            for uw_addr in uw_reg_addrs
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        loglevel = (
 | 
					        loglevel = (
 | 
				
			||||||
            loglevel
 | 
					            loglevel
 | 
				
			||||||
| 
						 | 
					@ -296,14 +276,13 @@ async def open_root_actor(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            debug_mode
 | 
					            debug_mode
 | 
				
			||||||
            and
 | 
					            and _spawn._spawn_method == 'trio'
 | 
				
			||||||
            _spawn._spawn_method == 'trio'
 | 
					 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            _state._runtime_vars['_debug_mode'] = True
 | 
					            _state._runtime_vars['_debug_mode'] = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # 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._tty_lock')
 | 
					            enable_modules.append('tractor.devx._debug')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # 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.
 | 
				
			||||||
| 
						 | 
					@ -336,10 +315,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[Address] = []
 | 
					        ponged_addrs: list[UnwrappedAddress] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        async def ping_tpt_socket(
 | 
					        async def ping_tpt_socket(
 | 
				
			||||||
            addr: Address,
 | 
					            addr: UnwrappedAddress,
 | 
				
			||||||
            timeout: float = 1,
 | 
					            timeout: float = 1,
 | 
				
			||||||
        ) -> None:
 | 
					        ) -> None:
 | 
				
			||||||
            '''
 | 
					            '''
 | 
				
			||||||
| 
						 | 
					@ -359,22 +338,17 @@ 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.unwrap()):
 | 
					                    async with _connect_chan(addr):
 | 
				
			||||||
                        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 root-actor registry found @ {addr!r}\n'
 | 
					                    f'No actor registry found @ {addr}\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 uw_addr in uw_reg_addrs:
 | 
					            for addr in registry_addrs:
 | 
				
			||||||
                addr: Address = wrap_address(uw_addr)
 | 
					 | 
				
			||||||
                tn.start_soon(
 | 
					                tn.start_soon(
 | 
				
			||||||
                    ping_tpt_socket,
 | 
					                    ping_tpt_socket,
 | 
				
			||||||
                    addr,
 | 
					                    addr,
 | 
				
			||||||
| 
						 | 
					@ -396,35 +370,31 @@ async def open_root_actor(
 | 
				
			||||||
                f'Registry(s) seem(s) to exist @ {ponged_addrs}'
 | 
					                f'Registry(s) seem(s) to exist @ {ponged_addrs}'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            actor = _runtime.Actor(
 | 
					            actor = 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
 | 
					            # DO NOT use the registry_addrs as the transport server
 | 
				
			||||||
            # ipc-transport-server's bind-addrs as this is
 | 
					            # addrs for this new non-registar, root-actor.
 | 
				
			||||||
            # 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(
 | 
				
			||||||
                    addr.get_random(
 | 
					                    waddr.get_random(bindspace=waddr.bindspace)
 | 
				
			||||||
                        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 = uw_reg_addrs
 | 
					            trans_bind_addrs = registry_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)
 | 
				
			||||||
| 
						 | 
					@ -435,8 +405,7 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # TODO: rename as `RootActor` or is that even necessary?
 | 
					            actor = Arbiter(
 | 
				
			||||||
            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,
 | 
				
			||||||
| 
						 | 
					@ -448,16 +417,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -465,28 +424,21 @@ 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
 | 
				
			||||||
            report: str = f'Starting actor-runtime for {actor.aid.reprol()!r}\n'
 | 
					            ml_addrs_str: str = '\n'.join(
 | 
				
			||||||
            if reg_addrs := actor.registry_addrs:
 | 
					                f'@{addr}' for addr in trans_bind_addrs
 | 
				
			||||||
                report += (
 | 
					            )
 | 
				
			||||||
                    '-> Opening new registry @ '
 | 
					            logger.info(
 | 
				
			||||||
                    +
 | 
					                f'Starting local {actor.uid} on the following transport addrs:\n'
 | 
				
			||||||
                    '\n'.join(
 | 
					                f'{ml_addrs_str}'
 | 
				
			||||||
                        f'{addr}' for addr in reg_addrs
 | 
					            )
 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            logger.info(f'{report}\n')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # start runtime in a bg sub-task, yield to caller.
 | 
					            # start the actor runtime in a new task
 | 
				
			||||||
            async with (
 | 
					            async with trio.open_nursery(
 | 
				
			||||||
                collapse_eg(),
 | 
					                strict_exception_groups=False,
 | 
				
			||||||
                trio.open_nursery() as root_tn,
 | 
					                # ^XXX^ TODO? instead unpack any RAE as per "loose" style?
 | 
				
			||||||
 | 
					            ) as nursery:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # ?TODO? finally-footgun below?
 | 
					                # ``_runtime.async_main()`` creates an internal nursery
 | 
				
			||||||
                # -> 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
 | 
				
			||||||
| 
						 | 
					@ -494,9 +446,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 root_tn.start(
 | 
					                await nursery.start(
 | 
				
			||||||
                    partial(
 | 
					                    partial(
 | 
				
			||||||
                        _runtime.async_main,
 | 
					                        async_main,
 | 
				
			||||||
                        actor,
 | 
					                        actor,
 | 
				
			||||||
                        accept_addrs=trans_bind_addrs,
 | 
					                        accept_addrs=trans_bind_addrs,
 | 
				
			||||||
                        parent_addr=None
 | 
					                        parent_addr=None
 | 
				
			||||||
| 
						 | 
					@ -512,7 +464,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
 | 
				
			||||||
| 
						 | 
					@ -520,15 +472,10 @@ 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 (
 | 
				
			||||||
| 
						 | 
					@ -549,7 +496,7 @@ async def open_root_actor(
 | 
				
			||||||
                    raise
 | 
					                    raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                finally:
 | 
					                finally:
 | 
				
			||||||
                    # NOTE/TODO?, not sure if we'll ever need this but it's
 | 
					                    # NOTE: 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..')
 | 
				
			||||||
| 
						 | 
					@ -558,44 +505,19 @@ 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'{op_nested_actor_repr}'
 | 
					                        f'>)\n'
 | 
				
			||||||
 | 
					                        f'|_{actor}\n'
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    # XXX, THIS IS A *finally-footgun*!
 | 
					                    await actor.cancel(None)  # self cancel
 | 
				
			||||||
                    # (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
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
                debug_mode
 | 
					 | 
				
			||||||
                and
 | 
					 | 
				
			||||||
                _spawn._spawn_method == 'trio'
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                _state._runtime_vars['_debug_mode'] = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            _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'{sclang_repr}'
 | 
					                f')>\n'
 | 
				
			||||||
 | 
					                f' |_{actor}\n'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										276
									
								
								tractor/_rpc.py
								
								
								
								
							
							
						
						
									
										276
									
								
								tractor/_rpc.py
								
								
								
								
							| 
						 | 
					@ -37,7 +37,6 @@ import warnings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import trio
 | 
					import trio
 | 
				
			||||||
from trio import (
 | 
					from trio import (
 | 
				
			||||||
    Cancelled,
 | 
					 | 
				
			||||||
    CancelScope,
 | 
					    CancelScope,
 | 
				
			||||||
    Nursery,
 | 
					    Nursery,
 | 
				
			||||||
    TaskStatus,
 | 
					    TaskStatus,
 | 
				
			||||||
| 
						 | 
					@ -53,18 +52,13 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -73,7 +67,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 (
 | 
				
			||||||
| 
						 | 
					@ -221,18 +215,11 @@ 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(
 | 
				
			||||||
                f'RPC task complete\n'
 | 
					                'RPC complete:\n'
 | 
				
			||||||
                f'\n'
 | 
					                f'task: {ctx._task}\n'
 | 
				
			||||||
                f'{op_nested_task}\n'
 | 
					                f'|_cid={ctx.cid}\n'
 | 
				
			||||||
                f'\n'
 | 
					                f'|_{fname}() -> {pformat(result)}\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
 | 
				
			||||||
| 
						 | 
					@ -263,7 +250,7 @@ async def _errors_relayed_via_ipc(
 | 
				
			||||||
    ctx: Context,
 | 
					    ctx: Context,
 | 
				
			||||||
    is_rpc: bool,
 | 
					    is_rpc: bool,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    hide_tb: bool = True,
 | 
					    hide_tb: bool = False,
 | 
				
			||||||
    debug_kbis: bool = False,
 | 
					    debug_kbis: bool = False,
 | 
				
			||||||
    task_status: TaskStatus[
 | 
					    task_status: TaskStatus[
 | 
				
			||||||
        Context | BaseException
 | 
					        Context | BaseException
 | 
				
			||||||
| 
						 | 
					@ -279,7 +266,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
 | 
				
			||||||
| 
						 | 
					@ -331,7 +318,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(),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
| 
						 | 
					@ -384,13 +371,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_tn`, we add "handles" to each such that
 | 
					    # `Actor._service_n`, 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 of
 | 
					        # if the error is not from user code and instead a failure
 | 
				
			||||||
        # an internal-runtime-RPC or IPC-connection, we do (prolly) want
 | 
					        # of a runtime RPC or transport failure we do prolly want to
 | 
				
			||||||
        # to show this frame!
 | 
					        # show this frame
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            rpc_err
 | 
					            rpc_err
 | 
				
			||||||
            and (
 | 
					            and (
 | 
				
			||||||
| 
						 | 
					@ -462,7 +449,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_tn: Nursery`.
 | 
					    remotely invoked function, normally in `Actor._service_n: Nursery`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    __tracebackhide__: bool = hide_tb
 | 
					    __tracebackhide__: bool = hide_tb
 | 
				
			||||||
| 
						 | 
					@ -475,7 +462,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..)?
 | 
				
			||||||
| 
						 | 
					@ -629,39 +616,32 @@ 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:
 | 
				
			||||||
            # TODO: better `trionics` primitive/tooling usage here!
 | 
					            tn: trio.Nursery
 | 
				
			||||||
            # -[ ] 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 (
 | 
				
			||||||
                collapse_eg(hide_tb=False),
 | 
					                trio.open_nursery(
 | 
				
			||||||
                trio.open_nursery() as tn,
 | 
					                    strict_exception_groups=False,
 | 
				
			||||||
 | 
					                    # ^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(
 | 
					 | 
				
			||||||
                    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # invoke user endpoint fn.
 | 
					                # TODO: better `trionics` tooling:
 | 
				
			||||||
 | 
					                # -[ ] 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,
 | 
				
			||||||
| 
						 | 
					@ -671,8 +651,7 @@ 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'\n'
 | 
					                    f'{return_msg}\n'
 | 
				
			||||||
                    f'{pretty_struct.pformat(return_msg)}\n'
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                await chan.send(return_msg)
 | 
					                await chan.send(return_msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -764,52 +743,43 @@ async def _invoke(
 | 
				
			||||||
            BaseExceptionGroup,
 | 
					            BaseExceptionGroup,
 | 
				
			||||||
            BaseException,
 | 
					            BaseException,
 | 
				
			||||||
            trio.Cancelled,
 | 
					            trio.Cancelled,
 | 
				
			||||||
        ) as _scope_err:
 | 
					
 | 
				
			||||||
            scope_err = _scope_err
 | 
					        ) as scope_error:
 | 
				
			||||||
            if (
 | 
					            if (
 | 
				
			||||||
                isinstance(scope_err, RuntimeError)
 | 
					                isinstance(scope_error, RuntimeError)
 | 
				
			||||||
                and
 | 
					                and scope_error.args
 | 
				
			||||||
                scope_err.args
 | 
					                and 'Cancel scope stack corrupted' in scope_error.args[0]
 | 
				
			||||||
                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_err
 | 
					            ctx._local_error: BaseException = scope_error
 | 
				
			||||||
            # ^-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?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # XXX if a remote error was set then likely the
 | 
					            # if a remote error was set then likely the
 | 
				
			||||||
            # exc group was raised due to that, so
 | 
					            # exception group was raised due to that, so
 | 
				
			||||||
            # and we instead raise that error immediately!
 | 
					            # and we instead raise that error immediately!
 | 
				
			||||||
            maybe_re: (
 | 
					            ctx.maybe_raise()
 | 
				
			||||||
                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 scope_err
 | 
					            raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # `@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.aid
 | 
					            assert chan.uid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # 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,
 | 
				
			||||||
| 
						 | 
					@ -822,49 +792,26 @@ 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 (
 | 
					                if isinstance(merr, ContextCancelled):
 | 
				
			||||||
                    # ctxc: by `Context.cancel()`
 | 
					                    logmeth: Callable = log.runtime
 | 
				
			||||||
                    isinstance(merr, ContextCancelled)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    # out-of-layer cancellation, one of:
 | 
					                if not isinstance(merr, RemoteActorError):
 | 
				
			||||||
                    # - actorc: by `Portal.cancel_actor()`
 | 
					                    tb_str: str = ''.join(traceback.format_exception(merr))
 | 
				
			||||||
                    # - 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 += (
 | 
					                    descr_str += f'\n{merr!r}\n'
 | 
				
			||||||
                        f'{merr!r}\n'
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                descr_str += (
 | 
					                descr_str += f'\nand final result {ctx.outcome!r}\n'
 | 
				
			||||||
                    f'\n'
 | 
					 | 
				
			||||||
                    f'with final result {ctx.outcome!r}\n'
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            logmeth(
 | 
					            logmeth(
 | 
				
			||||||
                f'{message}\n'
 | 
					                message
 | 
				
			||||||
                f'\n'
 | 
					                +
 | 
				
			||||||
                f'{descr_str}\n'
 | 
					                descr_str
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -935,7 +882,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_tn: Nursery`.
 | 
					    `trio.Task`s inside the `Actor._service_n: 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`)
 | 
				
			||||||
| 
						 | 
					@ -960,7 +907,7 @@ async def process_messages(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    actor: Actor = _state.current_actor()
 | 
					    actor: Actor = _state.current_actor()
 | 
				
			||||||
    assert actor._service_tn  # runtime state sanity
 | 
					    assert actor._service_n  # 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?
 | 
				
			||||||
| 
						 | 
					@ -1031,10 +978,12 @@ 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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1049,14 +998,14 @@ async def process_messages(
 | 
				
			||||||
                                cid,
 | 
					                                cid,
 | 
				
			||||||
                                chan,
 | 
					                                chan,
 | 
				
			||||||
                                actor.cancel,
 | 
					                                actor.cancel,
 | 
				
			||||||
                                kwargs | {'req_chan': chan},
 | 
					                                kwargs,
 | 
				
			||||||
                                is_rpc=False,
 | 
					                                is_rpc=False,
 | 
				
			||||||
                                return_msg_type=CancelAck,
 | 
					                                return_msg_type=CancelAck,
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        log.runtime(
 | 
					                        log.runtime(
 | 
				
			||||||
                            'Cancelling RPC-msg-loop with peer\n'
 | 
					                            'Cancelling IPC transport msg-loop with peer:\n'
 | 
				
			||||||
                            f'->c}} {chan.aid.reprol()}@[{chan.maddr}]\n'
 | 
					                            f'|_{chan}\n'
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        loop_cs.cancel()
 | 
					                        loop_cs.cancel()
 | 
				
			||||||
                        break
 | 
					                        break
 | 
				
			||||||
| 
						 | 
					@ -1069,7 +1018,7 @@ async def process_messages(
 | 
				
			||||||
                    ):
 | 
					                    ):
 | 
				
			||||||
                        target_cid: str = kwargs['cid']
 | 
					                        target_cid: str = kwargs['cid']
 | 
				
			||||||
                        kwargs |= {
 | 
					                        kwargs |= {
 | 
				
			||||||
                            'requesting_aid': chan.aid,
 | 
					                            'requesting_uid': chan.uid,
 | 
				
			||||||
                            'ipc_msg': msg,
 | 
					                            'ipc_msg': msg,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            # XXX NOTE! ONLY the rpc-task-owning
 | 
					                            # XXX NOTE! ONLY the rpc-task-owning
 | 
				
			||||||
| 
						 | 
					@ -1105,34 +1054,21 @@ 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=actor_uuid,
 | 
					                        uid=actorid,
 | 
				
			||||||
                    ):
 | 
					                    ):
 | 
				
			||||||
                        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 request\n'
 | 
					                            'Handling RPC `Start` request\n'
 | 
				
			||||||
                            f'{req_repr}\n'
 | 
					                            f'<= peer: {actorid}\n\n'
 | 
				
			||||||
                            f'\n'
 | 
					                            f'  |_{chan}\n'
 | 
				
			||||||
                            f'->{{ ipc-context-id: {cid!r}\n'
 | 
					                            f'  |_cid: {cid}\n\n'
 | 
				
			||||||
                            f'->{{ nsp for fn: `{ns}.{funcname}({kwargs})`\n'
 | 
					                            # f'  |_{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>`
 | 
				
			||||||
| 
						 | 
					@ -1161,6 +1097,10 @@ 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".
 | 
				
			||||||
                        #
 | 
					                        #
 | 
				
			||||||
| 
						 | 
					@ -1168,10 +1108,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_tn.start(
 | 
					                            ctx: Context = await actor._service_n.start(
 | 
				
			||||||
                                partial(
 | 
					                                partial(
 | 
				
			||||||
                                    _invoke,
 | 
					                                    _invoke,
 | 
				
			||||||
                                    actor,
 | 
					                                    actor,
 | 
				
			||||||
| 
						 | 
					@ -1252,24 +1192,12 @@ 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'IPC channel disconnected\n'
 | 
					                f'channel for {chan.uid} disconnected, cancelling RPC tasks\n'
 | 
				
			||||||
                f'{chan_repr}\n'
 | 
					                f'|_{chan}\n'
 | 
				
			||||||
                f'\n'
 | 
					 | 
				
			||||||
                f'->c) cancelling RPC tasks.\n'
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            await actor.cancel_rpc_tasks(
 | 
					            await actor.cancel_rpc_tasks(
 | 
				
			||||||
                req_aid=actor.aid,
 | 
					                req_uid=actor.uid,
 | 
				
			||||||
                # 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.
 | 
				
			||||||
| 
						 | 
					@ -1311,7 +1239,7 @@ async def process_messages(
 | 
				
			||||||
    ) as err:
 | 
					    ) as err:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if nursery_cancelled_before_task:
 | 
					        if nursery_cancelled_before_task:
 | 
				
			||||||
            sn: Nursery = actor._service_tn
 | 
					            sn: Nursery = actor._service_n
 | 
				
			||||||
            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}'
 | 
				
			||||||
| 
						 | 
					@ -1341,37 +1269,13 @@ 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 RPC-loop without receiving a msg?'
 | 
					            message: str = 'Exiting IPC msg 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 = (
 | 
				
			||||||
                f'Exiting RPC-loop with final msg\n'
 | 
					                'Exiting IPC msg loop with final msg\n\n'
 | 
				
			||||||
                f'\n'
 | 
					                f'<= peer: {chan.uid}\n'
 | 
				
			||||||
                # f'{chan_repr}\n'
 | 
					                f'  |_{chan}\n\n'
 | 
				
			||||||
                f'{task_repr}\n'
 | 
					                # f'{pretty_struct.pformat(msg)}'
 | 
				
			||||||
                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 import (
 | 
					from .devx._debug import (
 | 
				
			||||||
    debug,
 | 
					    maybe_wait_for_debugger,
 | 
				
			||||||
    pformat as _pformat
 | 
					    acquire_debug_lock,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from tractor._state import (
 | 
					from tractor._state import (
 | 
				
			||||||
    current_actor,
 | 
					    current_actor,
 | 
				
			||||||
| 
						 | 
					@ -51,17 +51,14 @@ 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 import (
 | 
					from tractor.msg.types import (
 | 
				
			||||||
    types as msgtypes,
 | 
					    Aid,
 | 
				
			||||||
    pretty_struct,
 | 
					    SpawnSpec,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
    from ipc import (
 | 
					    from ipc import IPCServer
 | 
				
			||||||
        _server,
 | 
					 | 
				
			||||||
        Channel,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    from ._supervise import ActorNursery
 | 
					    from ._supervise import ActorNursery
 | 
				
			||||||
    ProcessType = TypeVar('ProcessType', mp.Process, trio.Process)
 | 
					    ProcessType = TypeVar('ProcessType', mp.Process, trio.Process)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -236,6 +233,10 @@ 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.
 | 
				
			||||||
| 
						 | 
					@ -297,23 +298,6 @@ 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'
 | 
				
			||||||
| 
						 | 
					@ -344,21 +328,20 @@ async def soft_kill(
 | 
				
			||||||
    see `.hard_kill()`).
 | 
					    see `.hard_kill()`).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    chan: Channel = portal.channel
 | 
					    peer_aid: Aid = portal.channel.aid
 | 
				
			||||||
    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.reprol()}@[{chan.maddr}]\n'
 | 
					            f'(c=> {peer_aid}\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 debug.maybe_wait_for_debugger(
 | 
					            await maybe_wait_for_debugger(
 | 
				
			||||||
                child_in_debug=_runtime_vars.get(
 | 
					                child_in_debug=_runtime_vars.get(
 | 
				
			||||||
                    '_debug_mode', False
 | 
					                    '_debug_mode', False
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
| 
						 | 
					@ -482,7 +465,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 `msgtypes.Aid.min_tuple()` ?
 | 
					        # -[ ] maybe we can add an `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",
 | 
				
			||||||
| 
						 | 
					@ -500,14 +483,13 @@ 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: _server.Server = actor_nursery._actor.ipc_server
 | 
					    ipc_server: IPCServer = 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(
 | 
				
			||||||
                f'Started new child subproc\n'
 | 
					                'Started new child\n'
 | 
				
			||||||
                f'(>\n'
 | 
					                f'|_{proc}\n'
 | 
				
			||||||
                f' |_{proc}\n'
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # wait for actor to spawn and connect back to us
 | 
					            # wait for actor to spawn and connect back to us
 | 
				
			||||||
| 
						 | 
					@ -525,10 +507,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 debug.maybe_wait_for_debugger()
 | 
					                        await maybe_wait_for_debugger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    elif proc is not None:
 | 
					                    elif proc is not None:
 | 
				
			||||||
                        async with debug.acquire_debug_lock(subactor.uid):
 | 
					                        async with 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()
 | 
				
			||||||
| 
						 | 
					@ -546,19 +528,14 @@ 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 = msgtypes.SpawnSpec(
 | 
					        sspec = 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(
 | 
					        log.runtime(f'Sending spawn spec: {str(sspec)}')
 | 
				
			||||||
            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
 | 
				
			||||||
| 
						 | 
					@ -586,7 +563,7 @@ async def trio_proc(
 | 
				
			||||||
            # condition.
 | 
					            # condition.
 | 
				
			||||||
            await soft_kill(
 | 
					            await soft_kill(
 | 
				
			||||||
                proc,
 | 
					                proc,
 | 
				
			||||||
                trio.Process.wait,  # XXX, uses `pidfd_open()` below.
 | 
					                trio.Process.wait,
 | 
				
			||||||
                portal
 | 
					                portal
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -594,7 +571,8 @@ 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)> {subactor.aid.reprol()!r}\n'
 | 
					                f'>c)\n'
 | 
				
			||||||
 | 
					                f' |_{subactor.uid}\n'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            nursery.cancel_scope.cancel()
 | 
					            nursery.cancel_scope.cancel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -603,24 +581,21 @@ 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'{reap_repr}'
 | 
					                f'>x)\n'
 | 
				
			||||||
 | 
					                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 debug.acquire_debug_lock(subactor.uid):
 | 
					                    async with 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 debug.maybe_wait_for_debugger(
 | 
					                await maybe_wait_for_debugger(
 | 
				
			||||||
                    child_in_debug=_runtime_vars.get(
 | 
					                    child_in_debug=_runtime_vars.get(
 | 
				
			||||||
                        '_debug_mode', False
 | 
					                        '_debug_mode', False
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
| 
						 | 
					@ -649,7 +624,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 debug.acquire_debug_lock(this_uid)
 | 
					                # await 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}")
 | 
				
			||||||
| 
						 | 
					@ -752,7 +727,7 @@ async def mp_proc(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log.runtime(f"Started {proc}")
 | 
					    log.runtime(f"Started {proc}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ipc_server: _server.Server = actor_nursery._actor.ipc_server
 | 
					    ipc_server: IPCServer = 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,39 +37,20 @@ 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] = {
 | 
				
			||||||
    # root of actor-process tree info
 | 
					    '_debug_mode': False,
 | 
				
			||||||
    '_is_root': False,  # bool
 | 
					    '_is_root': False,
 | 
				
			||||||
    '_root_mailbox': (None, None),  # tuple[str|None, str|None]
 | 
					    '_root_mailbox': (None, None),
 | 
				
			||||||
    '_root_addrs': [],  # tuple[str|None, str|None]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # parent->chld ipc protocol caps
 | 
					 | 
				
			||||||
    '_enable_tpts': [_def_tpt_proto],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # registrar info
 | 
					 | 
				
			||||||
    '_registry_addrs': [],
 | 
					    '_registry_addrs': [],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # `debug_mode: bool` settings
 | 
					    '_is_infected_aio': False,
 | 
				
			||||||
    '_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,
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -191,6 +172,14 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -200,4 +189,4 @@ def current_ipc_protos() -> list[str]:
 | 
				
			||||||
    concrete-backend sub-types defined throughout `tractor.ipc`.
 | 
					    concrete-backend sub-types defined throughout `tractor.ipc`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    return _runtime_vars['_enable_tpts']
 | 
					    return [_def_tpt_proto]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,6 +21,7 @@
 | 
				
			||||||
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,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -30,10 +31,7 @@ import warnings
 | 
				
			||||||
import trio
 | 
					import trio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .devx import (
 | 
					from .devx._debug import maybe_wait_for_debugger
 | 
				
			||||||
    debug,
 | 
					 | 
				
			||||||
    pformat as _pformat,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from ._addr import (
 | 
					from ._addr import (
 | 
				
			||||||
    UnwrappedAddress,
 | 
					    UnwrappedAddress,
 | 
				
			||||||
    mk_uuid,
 | 
					    mk_uuid,
 | 
				
			||||||
| 
						 | 
					@ -42,11 +40,8 @@ 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 .trionics import (
 | 
					 | 
				
			||||||
    is_multi_cancelled,
 | 
					 | 
				
			||||||
    collapse_eg,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from ._exceptions import (
 | 
					from ._exceptions import (
 | 
				
			||||||
 | 
					    is_multi_cancelled,
 | 
				
			||||||
    ContextCancelled,
 | 
					    ContextCancelled,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from ._root import (
 | 
					from ._root import (
 | 
				
			||||||
| 
						 | 
					@ -117,6 +112,7 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -134,53 +130,10 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -244,7 +197,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().registry_addrs,
 | 
					            registry_addrs=current_actor().reg_addrs,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        parent_addr: UnwrappedAddress = self._actor.accept_addr
 | 
					        parent_addr: UnwrappedAddress = self._actor.accept_addr
 | 
				
			||||||
        assert parent_addr
 | 
					        assert parent_addr
 | 
				
			||||||
| 
						 | 
					@ -358,7 +311,7 @@ class ActorNursery:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        __runtimeframe__: int = 1  # noqa
 | 
					        __runtimeframe__: int = 1  # noqa
 | 
				
			||||||
        self._cancel_called = True
 | 
					        self.cancelled = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # TODO: impl a repr for spawn more compact
 | 
					        # TODO: impl a repr for spawn more compact
 | 
				
			||||||
        # then `._children`..
 | 
					        # then `._children`..
 | 
				
			||||||
| 
						 | 
					@ -369,10 +322,9 @@ 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 (
 | 
					            async with trio.open_nursery(
 | 
				
			||||||
                collapse_eg(),
 | 
					                strict_exception_groups=False,
 | 
				
			||||||
                trio.open_nursery() as tn,
 | 
					            ) as tn:
 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                subactor: Actor
 | 
					                subactor: Actor
 | 
				
			||||||
                proc: trio.Process
 | 
					                proc: trio.Process
 | 
				
			||||||
| 
						 | 
					@ -436,8 +388,6 @@ 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()
 | 
				
			||||||
| 
						 | 
					@ -446,12 +396,12 @@ class ActorNursery:
 | 
				
			||||||
@acm
 | 
					@acm
 | 
				
			||||||
async def _open_and_supervise_one_cancels_all_nursery(
 | 
					async def _open_and_supervise_one_cancels_all_nursery(
 | 
				
			||||||
    actor: Actor,
 | 
					    actor: Actor,
 | 
				
			||||||
    hide_tb: bool = True,
 | 
					    tb_hide: bool = False,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> typing.AsyncGenerator[ActorNursery, None]:
 | 
					) -> typing.AsyncGenerator[ActorNursery, None]:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # normally don't need to show user by default
 | 
					    # normally don't need to show user by default
 | 
				
			||||||
    __tracebackhide__: bool = hide_tb
 | 
					    __tracebackhide__: bool = tb_hide
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    outer_err: BaseException|None = None
 | 
					    outer_err: BaseException|None = None
 | 
				
			||||||
    inner_err: BaseException|None = None
 | 
					    inner_err: BaseException|None = None
 | 
				
			||||||
| 
						 | 
					@ -467,10 +417,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 (
 | 
					    async with trio.open_nursery(
 | 
				
			||||||
        collapse_eg(),
 | 
					        strict_exception_groups=False,
 | 
				
			||||||
        trio.open_nursery() as da_nursery,
 | 
					        # ^XXX^ TODO? instead unpack any RAE as per "loose" style?
 | 
				
			||||||
    ):
 | 
					    ) 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
 | 
				
			||||||
| 
						 | 
					@ -480,10 +430,11 @@ 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 (
 | 
					            async with trio.open_nursery(
 | 
				
			||||||
                collapse_eg(),
 | 
					                strict_exception_groups=False,
 | 
				
			||||||
                trio.open_nursery() as ria_nursery,
 | 
					                # ^XXX^ TODO? instead unpack any RAE as per "loose" style?
 | 
				
			||||||
            ):
 | 
					            ) as ria_nursery:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                an = ActorNursery(
 | 
					                an = ActorNursery(
 | 
				
			||||||
                    actor,
 | 
					                    actor,
 | 
				
			||||||
                    ria_nursery,
 | 
					                    ria_nursery,
 | 
				
			||||||
| 
						 | 
					@ -500,7 +451,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'>}} {len(an._children)}\n'
 | 
					                        f'{pformat(an._children)}\n'
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    an._join_procs.set()
 | 
					                    an._join_procs.set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -514,7 +465,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 debug.maybe_wait_for_debugger(
 | 
					                    await 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,7 +541,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 debug.maybe_wait_for_debugger(
 | 
					            await maybe_wait_for_debugger(
 | 
				
			||||||
                child_in_debug=an._at_least_one_child_in_debug
 | 
					                child_in_debug=an._at_least_one_child_in_debug
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -639,15 +590,9 @@ 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()`
 | 
				
			||||||
| 
						 | 
					@ -732,26 +677,17 @@ async def open_nursery(
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            __tracebackhide__: bool = False
 | 
					            __tracebackhide__: bool = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        msg: str = (
 | 
				
			||||||
        op_nested_an_repr: str = _pformat.nest_from_op(
 | 
					            'Actor-nursery exited\n'
 | 
				
			||||||
            input_op=')>',
 | 
					            f'|_{an}\n'
 | 
				
			||||||
            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: str = (
 | 
					            msg += '=> Shutting down actor runtime <=\n'
 | 
				
			||||||
                '\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 (
 | 
				
			||||||
| 
						 | 
					@ -37,9 +37,6 @@ from .fault_simulation import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO, use dulwhich for this instead?
 | 
					 | 
				
			||||||
# -> we're going to likely need it (or something similar)
 | 
					 | 
				
			||||||
#   for supporting hot-coad reload feats eventually anyway!
 | 
					 | 
				
			||||||
def repodir() -> pathlib.Path:
 | 
					def repodir() -> pathlib.Path:
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Return the abspath to the repo directory.
 | 
					    Return the abspath to the repo directory.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,70 +0,0 @@
 | 
				
			||||||
# 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/>.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
'''
 | 
					 | 
				
			||||||
Random IPC addr generation for isolating
 | 
					 | 
				
			||||||
the discovery space between test sessions.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Might be eventually useful to expose as a util set from
 | 
					 | 
				
			||||||
our `tractor.discovery` subsys?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
'''
 | 
					 | 
				
			||||||
import random
 | 
					 | 
				
			||||||
from typing import (
 | 
					 | 
				
			||||||
    Type,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from tractor import (
 | 
					 | 
				
			||||||
    _addr,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def get_rando_addr(
 | 
					 | 
				
			||||||
    tpt_proto: str,
 | 
					 | 
				
			||||||
    *,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # choose random port at import time
 | 
					 | 
				
			||||||
    _rando_port: str = random.randint(1000, 9999)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
) -> tuple[str, str|int]:
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    Used to globally override the runtime to the
 | 
					 | 
				
			||||||
    per-test-session-dynamic addr so that all tests never conflict
 | 
					 | 
				
			||||||
    with any other actor tree using the default.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    addr_type: Type[_addr.Addres] = _addr._address_types[tpt_proto]
 | 
					 | 
				
			||||||
    def_reg_addr: tuple[str, int] = _addr._default_lo_addrs[tpt_proto]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # this is the "unwrapped" form expected to be passed to
 | 
					 | 
				
			||||||
    # `.open_root_actor()` by test body.
 | 
					 | 
				
			||||||
    testrun_reg_addr: tuple[str, int|str]
 | 
					 | 
				
			||||||
    match tpt_proto:
 | 
					 | 
				
			||||||
        case 'tcp':
 | 
					 | 
				
			||||||
            testrun_reg_addr = (
 | 
					 | 
				
			||||||
                addr_type.def_bindspace,
 | 
					 | 
				
			||||||
                _rando_port,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # NOTE, file-name uniqueness (no-collisions) will be based on
 | 
					 | 
				
			||||||
        # the runtime-directory and root (pytest-proc's) pid.
 | 
					 | 
				
			||||||
        case 'uds':
 | 
					 | 
				
			||||||
            testrun_reg_addr = addr_type.get_random().unwrap()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # XXX, as sanity it should never the same as the default for the
 | 
					 | 
				
			||||||
    # host-singleton registry actor.
 | 
					 | 
				
			||||||
    assert def_reg_addr != testrun_reg_addr
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return testrun_reg_addr
 | 
					 | 
				
			||||||
| 
						 | 
					@ -26,46 +26,29 @@ from functools import (
 | 
				
			||||||
import inspect
 | 
					import inspect
 | 
				
			||||||
import platform
 | 
					import platform
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pytest
 | 
					 | 
				
			||||||
import tractor
 | 
					import tractor
 | 
				
			||||||
import trio
 | 
					import trio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def tractor_test(fn):
 | 
					def tractor_test(fn):
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Decorator for async test fns to decorator-wrap them as "native"
 | 
					    Decorator for async test funcs to present them as "native"
 | 
				
			||||||
    looking sync funcs runnable by `pytest` and auto invoked with
 | 
					    looking sync funcs runnable by `pytest` using `trio.run()`.
 | 
				
			||||||
    `trio.run()` (much like the `pytest-trio` plugin's approach).
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Further the test fn body will be invoked AFTER booting the actor
 | 
					    Use:
 | 
				
			||||||
    runtime, i.e. from inside a `tractor.open_root_actor()` block AND
 | 
					 | 
				
			||||||
    with various runtime and tooling parameters implicitly passed as
 | 
					 | 
				
			||||||
    requested by by the test session's config; see immediately below.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Basic deco use:
 | 
					    @tractor_test
 | 
				
			||||||
    ---------------
 | 
					    async def test_whatever():
 | 
				
			||||||
 | 
					        await ...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @tractor_test
 | 
					    If fixtures:
 | 
				
			||||||
      async def test_whatever():
 | 
					 | 
				
			||||||
          await ...
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        - ``reg_addr`` (a socket addr tuple where arbiter is listening)
 | 
				
			||||||
 | 
					        - ``loglevel`` (logging level passed to tractor internals)
 | 
				
			||||||
 | 
					        - ``start_method`` (subprocess spawning backend)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Runtime config via special fixtures:
 | 
					    are defined in the `pytest` fixture space they will be automatically
 | 
				
			||||||
    ------------------------------------
 | 
					    injected to tests declaring these funcargs.
 | 
				
			||||||
    If any of the following fixture are requested by the wrapped test
 | 
					 | 
				
			||||||
    fn (via normal func-args declaration),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    - `reg_addr` (a socket addr tuple where arbiter is listening)
 | 
					 | 
				
			||||||
    - `loglevel` (logging level passed to tractor internals)
 | 
					 | 
				
			||||||
    - `start_method` (subprocess spawning backend)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    (TODO support)
 | 
					 | 
				
			||||||
    - `tpt_proto` (IPC transport protocol key)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    they will be automatically injected to each test as normally
 | 
					 | 
				
			||||||
    expected as well as passed to the initial
 | 
					 | 
				
			||||||
    `tractor.open_root_actor()` funcargs.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    @wraps(fn)
 | 
					    @wraps(fn)
 | 
				
			||||||
    def wrapper(
 | 
					    def wrapper(
 | 
				
			||||||
| 
						 | 
					@ -128,164 +111,3 @@ def tractor_test(fn):
 | 
				
			||||||
        return trio.run(main)
 | 
					        return trio.run(main)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return wrapper
 | 
					    return wrapper
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def pytest_addoption(
 | 
					 | 
				
			||||||
    parser: pytest.Parser,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    # parser.addoption(
 | 
					 | 
				
			||||||
    #     "--ll",
 | 
					 | 
				
			||||||
    #     action="store",
 | 
					 | 
				
			||||||
    #     dest='loglevel',
 | 
					 | 
				
			||||||
    #     default='ERROR', help="logging level to set when testing"
 | 
					 | 
				
			||||||
    # )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parser.addoption(
 | 
					 | 
				
			||||||
        "--spawn-backend",
 | 
					 | 
				
			||||||
        action="store",
 | 
					 | 
				
			||||||
        dest='spawn_backend',
 | 
					 | 
				
			||||||
        default='trio',
 | 
					 | 
				
			||||||
        help="Processing spawning backend to use for test run",
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    parser.addoption(
 | 
					 | 
				
			||||||
        "--tpdb",
 | 
					 | 
				
			||||||
        "--debug-mode",
 | 
					 | 
				
			||||||
        action="store_true",
 | 
					 | 
				
			||||||
        dest='tractor_debug_mode',
 | 
					 | 
				
			||||||
        # default=False,
 | 
					 | 
				
			||||||
        help=(
 | 
					 | 
				
			||||||
            'Enable a flag that can be used by tests to to set the '
 | 
					 | 
				
			||||||
            '`debug_mode: bool` for engaging the internal '
 | 
					 | 
				
			||||||
            'multi-proc debugger sys.'
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # provide which IPC transport protocols opting-in test suites
 | 
					 | 
				
			||||||
    # should accumulatively run against.
 | 
					 | 
				
			||||||
    parser.addoption(
 | 
					 | 
				
			||||||
        "--tpt-proto",
 | 
					 | 
				
			||||||
        nargs='+',  # accumulate-multiple-args
 | 
					 | 
				
			||||||
        action="store",
 | 
					 | 
				
			||||||
        dest='tpt_protos',
 | 
					 | 
				
			||||||
        default=['tcp'],
 | 
					 | 
				
			||||||
        help="Transport protocol to use under the `tractor.ipc.Channel`",
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def pytest_configure(config):
 | 
					 | 
				
			||||||
    backend = config.option.spawn_backend
 | 
					 | 
				
			||||||
    tractor._spawn.try_set_start_method(backend)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.fixture(scope='session')
 | 
					 | 
				
			||||||
def debug_mode(request) -> bool:
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    Flag state for whether `--tpdb` (for `tractor`-py-debugger)
 | 
					 | 
				
			||||||
    was passed to the test run.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Normally tests should pass this directly to `.open_root_actor()`
 | 
					 | 
				
			||||||
    to allow the user to opt into suite-wide crash handling.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    debug_mode: bool = request.config.option.tractor_debug_mode
 | 
					 | 
				
			||||||
    return debug_mode
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.fixture(scope='session')
 | 
					 | 
				
			||||||
def spawn_backend(request) -> str:
 | 
					 | 
				
			||||||
    return request.config.option.spawn_backend
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.fixture(scope='session')
 | 
					 | 
				
			||||||
def tpt_protos(request) -> list[str]:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # allow quoting on CLI
 | 
					 | 
				
			||||||
    proto_keys: list[str] = [
 | 
					 | 
				
			||||||
        proto_key.replace('"', '').replace("'", "")
 | 
					 | 
				
			||||||
        for proto_key in request.config.option.tpt_protos
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # ?TODO, eventually support multiple protos per test-sesh?
 | 
					 | 
				
			||||||
    if len(proto_keys) > 1:
 | 
					 | 
				
			||||||
        pytest.fail(
 | 
					 | 
				
			||||||
            'We only support one `--tpt-proto <key>` atm!\n'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # XXX ensure we support the protocol by name via lookup!
 | 
					 | 
				
			||||||
    for proto_key in proto_keys:
 | 
					 | 
				
			||||||
        addr_type = tractor._addr._address_types[proto_key]
 | 
					 | 
				
			||||||
        assert addr_type.proto_key == proto_key
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    yield proto_keys
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.fixture(
 | 
					 | 
				
			||||||
    scope='session',
 | 
					 | 
				
			||||||
    autouse=True,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
def tpt_proto(
 | 
					 | 
				
			||||||
    tpt_protos: list[str],
 | 
					 | 
				
			||||||
) -> str:
 | 
					 | 
				
			||||||
    proto_key: str = tpt_protos[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    from tractor import _state
 | 
					 | 
				
			||||||
    if _state._def_tpt_proto != proto_key:
 | 
					 | 
				
			||||||
        _state._def_tpt_proto = proto_key
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    yield proto_key
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@pytest.fixture(scope='session')
 | 
					 | 
				
			||||||
def reg_addr(
 | 
					 | 
				
			||||||
    tpt_proto: str,
 | 
					 | 
				
			||||||
) -> tuple[str, int|str]:
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    Deliver a test-sesh unique registry address such
 | 
					 | 
				
			||||||
    that each run's (tests which use this fixture) will
 | 
					 | 
				
			||||||
    have no conflicts/cross-talk when running simultaneously
 | 
					 | 
				
			||||||
    nor will interfere with other live `tractor` apps active
 | 
					 | 
				
			||||||
    on the same network-host (namespace).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    from tractor._testing.addr import get_rando_addr
 | 
					 | 
				
			||||||
    return get_rando_addr(
 | 
					 | 
				
			||||||
        tpt_proto=tpt_proto,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def pytest_generate_tests(
 | 
					 | 
				
			||||||
    metafunc: pytest.Metafunc,
 | 
					 | 
				
			||||||
):
 | 
					 | 
				
			||||||
    spawn_backend: str = metafunc.config.option.spawn_backend
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not spawn_backend:
 | 
					 | 
				
			||||||
        # XXX some weird windows bug with `pytest`?
 | 
					 | 
				
			||||||
        spawn_backend = 'trio'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # TODO: maybe just use the literal `._spawn.SpawnMethodKey`?
 | 
					 | 
				
			||||||
    assert spawn_backend in (
 | 
					 | 
				
			||||||
        'mp_spawn',
 | 
					 | 
				
			||||||
        'mp_forkserver',
 | 
					 | 
				
			||||||
        'trio',
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # NOTE: used-to-be-used-to dyanmically parametrize tests for when
 | 
					 | 
				
			||||||
    # you just passed --spawn-backend=`mp` on the cli, but now we expect
 | 
					 | 
				
			||||||
    # that cli input to be manually specified, BUT, maybe we'll do
 | 
					 | 
				
			||||||
    # something like this again in the future?
 | 
					 | 
				
			||||||
    if 'start_method' in metafunc.fixturenames:
 | 
					 | 
				
			||||||
        metafunc.parametrize(
 | 
					 | 
				
			||||||
            "start_method",
 | 
					 | 
				
			||||||
            [spawn_backend],
 | 
					 | 
				
			||||||
            scope='module',
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # TODO, parametrize any `tpt_proto: str` declaring tests!
 | 
					 | 
				
			||||||
    # proto_tpts: list[str] = metafunc.config.option.proto_tpts
 | 
					 | 
				
			||||||
    # if 'tpt_proto' in metafunc.fixturenames:
 | 
					 | 
				
			||||||
    #     metafunc.parametrize(
 | 
					 | 
				
			||||||
    #         'tpt_proto',
 | 
					 | 
				
			||||||
    #         proto_tpts,  # TODO, double check this list usage!
 | 
					 | 
				
			||||||
    #         scope='module',
 | 
					 | 
				
			||||||
    #     )
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,18 +20,13 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -39,9 +34,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -49,8 +41,6 @@ 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..
 | 
				
			||||||
| 
						 | 
					@ -311,70 +301,3 @@ 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,8 +238,7 @@ def enable_stack_on_sig(
 | 
				
			||||||
        import stackscope
 | 
					        import stackscope
 | 
				
			||||||
    except ImportError:
 | 
					    except ImportError:
 | 
				
			||||||
        log.warning(
 | 
					        log.warning(
 | 
				
			||||||
            'The `stackscope` lib is not installed!\n'
 | 
					            '`stackscope` not installed for use in debug mode!'
 | 
				
			||||||
            '`Ignoring enable_stack_on_sig() call!\n'
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -256,8 +255,8 @@ def enable_stack_on_sig(
 | 
				
			||||||
        dump_tree_on_sig,
 | 
					        dump_tree_on_sig,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    log.devx(
 | 
					    log.devx(
 | 
				
			||||||
        f'Enabling trace-trees on `SIGUSR1` '
 | 
					        'Enabling trace-trees on `SIGUSR1` '
 | 
				
			||||||
        f'since `stackscope` is installed @ \n'
 | 
					        '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'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,100 +0,0 @@
 | 
				
			||||||
# 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?
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,412 +0,0 @@
 | 
				
			||||||
# 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
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,207 +0,0 @@
 | 
				
			||||||
# 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
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,333 +0,0 @@
 | 
				
			||||||
# 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')
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,220 +0,0 @@
 | 
				
			||||||
# 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,10 +15,8 @@
 | 
				
			||||||
# 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 our internals.
 | 
					Pretty formatters for use throughout the code base.
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
| 
						 | 
					@ -226,8 +224,8 @@ def pformat_cs(
 | 
				
			||||||
    field_prefix: str = ' |_',
 | 
					    field_prefix: str = ' |_',
 | 
				
			||||||
) -> str:
 | 
					) -> str:
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Pretty format info about a `trio.CancelScope` including most of
 | 
					    Pretty format info about a `trio.CancelScope` including most
 | 
				
			||||||
    its public state and `._cancel_status`.
 | 
					    of 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,279 +247,3 @@ def pformat_cs(
 | 
				
			||||||
        +
 | 
					        +
 | 
				
			||||||
        fields
 | 
					        fields
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def nest_from_op(
 | 
					 | 
				
			||||||
    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? aa more fomal idea for a syntax to the state of
 | 
					 | 
				
			||||||
    # concurrent systems as a "3-domain" (execution, scope, storage)
 | 
					 | 
				
			||||||
    # model and using a minimal ascii/utf-8 operator-set.
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # try not to take any of this seriously yet XD
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # > is a "play operator" indicating (CPU bound)
 | 
					 | 
				
			||||||
    #   exec/work/ops required at the "lowest level computing"
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # execution primititves (tasks, threads, actors..) denote their
 | 
					 | 
				
			||||||
    # lifetime with '(' and ')' since parentheses normally are used
 | 
					 | 
				
			||||||
    # in many langs to denote function calls.
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # starting = (
 | 
					 | 
				
			||||||
    # >(  opening/starting; beginning of the thread-of-exec (toe?)
 | 
					 | 
				
			||||||
    # (>  opened/started,  (finished spawning toe)
 | 
					 | 
				
			||||||
    # |_<Task: blah blah..>  repr of toe, in py these look like <objs>
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # >) closing/exiting/stopping,
 | 
					 | 
				
			||||||
    # )> closed/exited/stopped,
 | 
					 | 
				
			||||||
    # |_<Task: blah blah..>
 | 
					 | 
				
			||||||
    #   [OR <), )< ?? ]
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # ending = )
 | 
					 | 
				
			||||||
    # >c) cancelling to close/exit
 | 
					 | 
				
			||||||
    # c)> cancelled (caused close), OR?
 | 
					 | 
				
			||||||
    #  |_<Actor: ..>
 | 
					 | 
				
			||||||
    #   OR maybe "<c)" which better indicates the cancel being
 | 
					 | 
				
			||||||
    #   "delivered/returned" / returned" to LHS?
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # >x)  erroring to eventuall exit
 | 
					 | 
				
			||||||
    # x)>  errored and terminated
 | 
					 | 
				
			||||||
    #  |_<Actor: ...>
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # scopes: supers/nurseries, IPC-ctxs, sessions, perms, etc.
 | 
					 | 
				
			||||||
    # >{  opening
 | 
					 | 
				
			||||||
    # {>  opened
 | 
					 | 
				
			||||||
    # }>  closed
 | 
					 | 
				
			||||||
    # >}  closing
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # storage: like queues, shm-buffers, files, etc..
 | 
					 | 
				
			||||||
    # >[  opening
 | 
					 | 
				
			||||||
    # [>  opened
 | 
					 | 
				
			||||||
    #  |_<FileObj: ..>
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # >]  closing
 | 
					 | 
				
			||||||
    # ]>  closed
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # IPC ops: channels, transports, msging
 | 
					 | 
				
			||||||
    # =>  req msg
 | 
					 | 
				
			||||||
    # <=  resp msg
 | 
					 | 
				
			||||||
    # <=> 2-way streaming (of msgs)
 | 
					 | 
				
			||||||
    # <-  recv 1 msg
 | 
					 | 
				
			||||||
    # ->  send 1 msg
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # TODO: still not sure on R/L-HS approach..?
 | 
					 | 
				
			||||||
    # =>(  send-req to exec start (task, actor, thread..)
 | 
					 | 
				
			||||||
    # (<=  recv-req to ^
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # (<=  recv-req ^
 | 
					 | 
				
			||||||
    # <=(  recv-resp opened remote exec primitive
 | 
					 | 
				
			||||||
    # <=)  recv-resp closed
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # )<=c req to stop due to cancel
 | 
					 | 
				
			||||||
    # c=>) req to stop due to cancel
 | 
					 | 
				
			||||||
    #
 | 
					 | 
				
			||||||
    # =>{  recv-req to open
 | 
					 | 
				
			||||||
    # <={  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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # 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)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    indented_tree_str: str = text
 | 
					 | 
				
			||||||
    if tree_str_indent:
 | 
					 | 
				
			||||||
        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:
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    The `pprint.pformat()` version of `pprint.pp()`, namely
 | 
					 | 
				
			||||||
    a default `sort_dicts=False`.. (which i think should be
 | 
					 | 
				
			||||||
    the normal default in the stdlib).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    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 (
 | 
					 | 
				
			||||||
        'FrameInfo(\n'
 | 
					 | 
				
			||||||
        '  frame={!r},\n'
 | 
					 | 
				
			||||||
        '  filename={!r},\n'
 | 
					 | 
				
			||||||
        '  lineno={!r},\n'
 | 
					 | 
				
			||||||
        '  function={!r},\n'
 | 
					 | 
				
			||||||
        '  code_context={!r},\n'
 | 
					 | 
				
			||||||
        '  index={!r},\n'
 | 
					 | 
				
			||||||
        '  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,27 +101,11 @@ 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 runtime.
 | 
					        # (possibly peer) cancellation of the far end actor
 | 
				
			||||||
 | 
					        # 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]:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
| 
						 | 
					@ -185,27 +169,13 @@ class Channel:
 | 
				
			||||||
            addr,
 | 
					            addr,
 | 
				
			||||||
            **kwargs,
 | 
					            **kwargs,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # XXX, for UDS *no!* since we recv the peer-pid and build out
 | 
					        assert transport.raddr == addr
 | 
				
			||||||
        # a new addr..
 | 
					 | 
				
			||||||
        # assert transport.raddr == addr
 | 
					 | 
				
			||||||
        chan = Channel(transport=transport)
 | 
					        chan = Channel(transport=transport)
 | 
				
			||||||
 | 
					        log.runtime(
 | 
				
			||||||
        # ?TODO, compact this into adapter level-methods?
 | 
					            f'Connected channel IPC transport\n'
 | 
				
			||||||
        # -[ ] would avoid extra repr-calcs if level not active?
 | 
					            f'[>\n'
 | 
				
			||||||
        #   |_ how would the `calc_if_level` look though? func?
 | 
					            f' |_{chan}\n'
 | 
				
			||||||
        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
 | 
				
			||||||
| 
						 | 
					@ -226,12 +196,9 @@ 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(
 | 
					    def pformat(self) -> str:
 | 
				
			||||||
        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__
 | 
				
			||||||
| 
						 | 
					@ -239,35 +206,26 @@ class Channel:
 | 
				
			||||||
            'connected' if self.connected()
 | 
					            'connected' if self.connected()
 | 
				
			||||||
            else 'closed'
 | 
					            else 'closed'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        repr_str: str = (
 | 
					        return (
 | 
				
			||||||
            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'
 | 
				
			||||||
            if privates else ''
 | 
					            f'\n'
 | 
				
			||||||
        ) + (  # peer-actor (processs) section
 | 
					            f' |_peer: {self.aid}\n'
 | 
				
			||||||
            f' |_peer: {self.aid.reprol()!r}\n'
 | 
					            f'\n'
 | 
				
			||||||
            if self.aid else ' |_peer: <unknown>\n'
 | 
					 | 
				
			||||||
        ) + (
 | 
					 | 
				
			||||||
            f' |_msgstream: {tpt_name}\n'
 | 
					            f' |_msgstream: {tpt_name}\n'
 | 
				
			||||||
            f'   maddr: {tpt.maddr!r}\n'
 | 
					            f'   proto={tpt.laddr.proto_key!r}\n'
 | 
				
			||||||
            f'   proto: {tpt.laddr.proto_key!r}\n'
 | 
					            f'   layer={tpt.layer_key!r}\n'
 | 
				
			||||||
            f'   layer: {tpt.layer_key!r}\n'
 | 
					            f'   laddr={tpt.laddr}\n'
 | 
				
			||||||
            f'   codec: {tpt.codec_key!r}\n'
 | 
					            f'   raddr={tpt.raddr}\n'
 | 
				
			||||||
            f'   .laddr={tpt.laddr}\n'
 | 
					            f'   codec={tpt.codec_key!r}\n'
 | 
				
			||||||
            f'   .raddr={tpt.raddr}\n'
 | 
					            f'   stream={tpt.stream}\n'
 | 
				
			||||||
        ) + (
 | 
					            f'   maddr={tpt.maddr!r}\n'
 | 
				
			||||||
            f'   ._transport.stream={tpt.stream}\n'
 | 
					            f'   drained={tpt.drained}\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'
 | 
				
			||||||
            if privates else ''
 | 
					            f')>\n'
 | 
				
			||||||
        ) + (
 | 
					 | 
				
			||||||
            ')>\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!
 | 
				
			||||||
| 
						 | 
					@ -289,10 +247,6 @@ 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..
 | 
				
			||||||
| 
						 | 
					@ -303,7 +257,7 @@ class Channel:
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        payload: Any,
 | 
					        payload: Any,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        hide_tb: bool = False,
 | 
					        hide_tb: bool = True,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
| 
						 | 
					@ -480,8 +434,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\n'
 | 
					            f'Received hanshake with peer actor,\n'
 | 
				
			||||||
            f'<= {peer_aid.reprol(sin_uuid=False)}\n'
 | 
					            f'{peer_aid}\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,59 +17,29 @@
 | 
				
			||||||
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.shared_memory import SharedMemory
 | 
					    from multiprocessing import resource_tracker as mantracker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Tell the "resource tracker" thing to fuck off.
 | 
				
			||||||
 | 
					    class ManTracker(mantracker.ResourceTracker):
 | 
				
			||||||
 | 
					        def register(self, name, rtype):
 | 
				
			||||||
 | 
					            pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # 3.13+ only.. can pass `track=False` to disable
 | 
					        def unregister(self, name, rtype):
 | 
				
			||||||
    # all the resource tracker bs.
 | 
					            pass
 | 
				
			||||||
    # 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,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # !TODO, once we drop 3.12- we can obvi remove all this!
 | 
					        def ensure_running(self):
 | 
				
			||||||
    else:
 | 
					            pass
 | 
				
			||||||
        from multiprocessing import (
 | 
					 | 
				
			||||||
            resource_tracker as mantracker,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Tell the "resource tracker" thing to fuck off.
 | 
					    # "know your land and know your prey"
 | 
				
			||||||
        class ManTracker(mantracker.ResourceTracker):
 | 
					    # https://www.dailymotion.com/video/x6ozzco
 | 
				
			||||||
            def register(self, name, rtype):
 | 
					    mantracker._resource_tracker = ManTracker()
 | 
				
			||||||
                pass
 | 
					    mantracker.register = mantracker._resource_tracker.register
 | 
				
			||||||
 | 
					    mantracker.ensure_running = mantracker._resource_tracker.ensure_running
 | 
				
			||||||
            def unregister(self, name, rtype):
 | 
					    mantracker.unregister = mantracker._resource_tracker.unregister
 | 
				
			||||||
                pass
 | 
					    mantracker.getfd = mantracker._resource_tracker.getfd
 | 
				
			||||||
 | 
					 | 
				
			||||||
            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
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@ from contextlib import (
 | 
				
			||||||
from functools import partial
 | 
					from functools import partial
 | 
				
			||||||
from itertools import chain
 | 
					from itertools import chain
 | 
				
			||||||
import inspect
 | 
					import inspect
 | 
				
			||||||
import textwrap
 | 
					from pprint import pformat
 | 
				
			||||||
from types import (
 | 
					from types import (
 | 
				
			||||||
    ModuleType,
 | 
					    ModuleType,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -43,10 +43,7 @@ from trio import (
 | 
				
			||||||
    SocketListener,
 | 
					    SocketListener,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ..devx.pformat import (
 | 
					# from ..devx import _debug
 | 
				
			||||||
    ppfmt,
 | 
					 | 
				
			||||||
    nest_from_op,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from .._exceptions import (
 | 
					from .._exceptions import (
 | 
				
			||||||
    TransportClosed,
 | 
					    TransportClosed,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -144,8 +141,9 @@ async def maybe_wait_on_canced_subs(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        log.cancel(
 | 
					        log.cancel(
 | 
				
			||||||
            'Waiting on cancel request to peer\n'
 | 
					            'Waiting on cancel request to peer..\n'
 | 
				
			||||||
            f'c)=> {chan.aid.reprol()}@[{chan.maddr}]\n'
 | 
					            f'c)=>\n'
 | 
				
			||||||
 | 
					            f'  |_{chan.uid}\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # XXX: this is a soft wait on the channel (and its
 | 
					        # XXX: this is a soft wait on the channel (and its
 | 
				
			||||||
| 
						 | 
					@ -158,7 +156,7 @@ async def maybe_wait_on_canced_subs(
 | 
				
			||||||
        # local runtime here is now cancelled while
 | 
					        # local runtime here is now cancelled while
 | 
				
			||||||
        # (presumably) in the middle of msg loop processing.
 | 
					        # (presumably) in the middle of msg loop processing.
 | 
				
			||||||
        chan_info: str = (
 | 
					        chan_info: str = (
 | 
				
			||||||
            f'{chan.aid}\n'
 | 
					            f'{chan.uid}\n'
 | 
				
			||||||
            f'|_{chan}\n'
 | 
					            f'|_{chan}\n'
 | 
				
			||||||
            f'  |_{chan.transport}\n\n'
 | 
					            f'  |_{chan.transport}\n\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -181,7 +179,7 @@ async def maybe_wait_on_canced_subs(
 | 
				
			||||||
                log.warning(
 | 
					                log.warning(
 | 
				
			||||||
                    'Draining msg from disconnected peer\n'
 | 
					                    'Draining msg from disconnected peer\n'
 | 
				
			||||||
                    f'{chan_info}'
 | 
					                    f'{chan_info}'
 | 
				
			||||||
                    f'{ppfmt(msg)}\n'
 | 
					                    f'{pformat(msg)}\n'
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # cid: str|None = msg.get('cid')
 | 
					                # cid: str|None = msg.get('cid')
 | 
				
			||||||
                cid: str|None = msg.cid
 | 
					                cid: str|None = msg.cid
 | 
				
			||||||
| 
						 | 
					@ -250,7 +248,7 @@ async def maybe_wait_on_canced_subs(
 | 
				
			||||||
                if children := local_nursery._children:
 | 
					                if children := local_nursery._children:
 | 
				
			||||||
                    # indent from above local-nurse repr
 | 
					                    # indent from above local-nurse repr
 | 
				
			||||||
                    report += (
 | 
					                    report += (
 | 
				
			||||||
                        f'   |_{ppfmt(children)}\n'
 | 
					                        f'   |_{pformat(children)}\n'
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                log.warning(report)
 | 
					                log.warning(report)
 | 
				
			||||||
| 
						 | 
					@ -281,9 +279,8 @@ async def maybe_wait_on_canced_subs(
 | 
				
			||||||
                    log.runtime(
 | 
					                    log.runtime(
 | 
				
			||||||
                        f'Peer IPC broke but subproc is alive?\n\n'
 | 
					                        f'Peer IPC broke but subproc is alive?\n\n'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        f'<=x {chan.aid.reprol()}@[{chan.maddr}]\n'
 | 
					                        f'<=x {chan.uid}@{chan.raddr}\n'
 | 
				
			||||||
                        f'\n'
 | 
					                        f'   |_{proc}\n'
 | 
				
			||||||
                        f'{proc}\n'
 | 
					 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return local_nursery
 | 
					    return local_nursery
 | 
				
			||||||
| 
						 | 
					@ -292,7 +289,7 @@ async def maybe_wait_on_canced_subs(
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# -[x] maybe change to mod-func and rename for implied
 | 
					# -[x] maybe change to mod-func and rename for implied
 | 
				
			||||||
#    multi-transport semantics?
 | 
					#    multi-transport semantics?
 | 
				
			||||||
# -[ ] register each stream/tpt/chan with the owning `Endpoint`
 | 
					# -[ ] register each stream/tpt/chan with the owning `IPCEndpoint`
 | 
				
			||||||
#     so that we can query per tpt all peer contact infos?
 | 
					#     so that we can query per tpt all peer contact infos?
 | 
				
			||||||
#  |_[ ] possibly provide a global viewing via a
 | 
					#  |_[ ] possibly provide a global viewing via a
 | 
				
			||||||
#        `collections.ChainMap`?
 | 
					#        `collections.ChainMap`?
 | 
				
			||||||
| 
						 | 
					@ -312,7 +309,7 @@ async def handle_stream_from_peer(
 | 
				
			||||||
    any `IPCServer.listen_on()` passed `stream_handler_tn: Nursery`
 | 
					    any `IPCServer.listen_on()` passed `stream_handler_tn: Nursery`
 | 
				
			||||||
    such that it is invoked as,
 | 
					    such that it is invoked as,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Endpoint.stream_handler_tn.start_soon(
 | 
					      IPCEndpoint.stream_handler_tn.start_soon(
 | 
				
			||||||
          handle_stream,
 | 
					          handle_stream,
 | 
				
			||||||
          stream,
 | 
					          stream,
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
| 
						 | 
					@ -321,16 +318,15 @@ async def handle_stream_from_peer(
 | 
				
			||||||
    server._no_more_peers = trio.Event()  # unset by making new
 | 
					    server._no_more_peers = trio.Event()  # unset by making new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # TODO, debug_mode tooling for when hackin this lower layer?
 | 
					    # TODO, debug_mode tooling for when hackin this lower layer?
 | 
				
			||||||
    # with debug.maybe_open_crash_handler(
 | 
					    # with _debug.maybe_open_crash_handler(
 | 
				
			||||||
    #     pdb=True,
 | 
					    #     pdb=True,
 | 
				
			||||||
    # ) as boxerr:
 | 
					    # ) as boxerr:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    chan = Channel.from_stream(stream)
 | 
					    chan = Channel.from_stream(stream)
 | 
				
			||||||
    con_status: str = (
 | 
					    con_status: str = (
 | 
				
			||||||
        f'New inbound IPC transport connection\n'
 | 
					        'New inbound IPC connection <=\n'
 | 
				
			||||||
        f'<=( {stream!r}\n'
 | 
					        f'|_{chan}\n'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    con_status_steps: str = ''
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # initial handshake with peer phase
 | 
					    # initial handshake with peer phase
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
| 
						 | 
					@ -376,7 +372,7 @@ async def handle_stream_from_peer(
 | 
				
			||||||
    if _pre_chan := server._peers.get(uid):
 | 
					    if _pre_chan := server._peers.get(uid):
 | 
				
			||||||
        familiar: str = 'pre-existing-peer'
 | 
					        familiar: str = 'pre-existing-peer'
 | 
				
			||||||
    uid_short: str = f'{uid[0]}[{uid[1][-6:]}]'
 | 
					    uid_short: str = f'{uid[0]}[{uid[1][-6:]}]'
 | 
				
			||||||
    con_status_steps += (
 | 
					    con_status += (
 | 
				
			||||||
        f' -> Handshake with {familiar} `{uid_short}` complete\n'
 | 
					        f' -> Handshake with {familiar} `{uid_short}` complete\n'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -401,7 +397,7 @@ async def handle_stream_from_peer(
 | 
				
			||||||
        None,
 | 
					        None,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    if event:
 | 
					    if event:
 | 
				
			||||||
        con_status_steps += (
 | 
					        con_status += (
 | 
				
			||||||
            ' -> Waking subactor spawn waiters: '
 | 
					            ' -> Waking subactor spawn waiters: '
 | 
				
			||||||
            f'{event.statistics().tasks_waiting}\n'
 | 
					            f'{event.statistics().tasks_waiting}\n'
 | 
				
			||||||
            f' -> Registered IPC chan for child actor {uid}@{chan.raddr}\n'
 | 
					            f' -> Registered IPC chan for child actor {uid}@{chan.raddr}\n'
 | 
				
			||||||
| 
						 | 
					@ -412,7 +408,7 @@ async def handle_stream_from_peer(
 | 
				
			||||||
        event.set()
 | 
					        event.set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        con_status_steps += (
 | 
					        con_status += (
 | 
				
			||||||
            f' -> Registered IPC chan for peer actor {uid}@{chan.raddr}\n'
 | 
					            f' -> Registered IPC chan for peer actor {uid}@{chan.raddr}\n'
 | 
				
			||||||
        )  # type: ignore
 | 
					        )  # type: ignore
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -426,15 +422,8 @@ async def handle_stream_from_peer(
 | 
				
			||||||
    # TODO: can we just use list-ref directly?
 | 
					    # TODO: can we just use list-ref directly?
 | 
				
			||||||
    chans.append(chan)
 | 
					    chans.append(chan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    con_status_steps += ' -> Entering RPC msg loop..\n'
 | 
					    con_status += ' -> Entering RPC msg loop..\n'
 | 
				
			||||||
    log.runtime(
 | 
					    log.runtime(con_status)
 | 
				
			||||||
        con_status
 | 
					 | 
				
			||||||
        +
 | 
					 | 
				
			||||||
        textwrap.indent(
 | 
					 | 
				
			||||||
            con_status_steps,
 | 
					 | 
				
			||||||
            prefix=' '*3,  # align to first-ln
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Begin channel management - respond to remote requests and
 | 
					    # Begin channel management - respond to remote requests and
 | 
				
			||||||
    # process received reponses.
 | 
					    # process received reponses.
 | 
				
			||||||
| 
						 | 
					@ -467,67 +456,41 @@ async def handle_stream_from_peer(
 | 
				
			||||||
            disconnected=disconnected,
 | 
					            disconnected=disconnected,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # `Channel` teardown and closure sequence
 | 
					        # ``Channel`` teardown and closure sequence
 | 
				
			||||||
        # drop ref to channel so it can be gc-ed and disconnected
 | 
					        # drop ref to channel so it can be gc-ed and disconnected
 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        # -[x]TODO mk this be like
 | 
					 | 
				
			||||||
        # <=x Channel(
 | 
					 | 
				
			||||||
        #     |_field: blah
 | 
					 | 
				
			||||||
        # )>
 | 
					 | 
				
			||||||
        op_repr: str = '<=x '
 | 
					 | 
				
			||||||
        chan_repr: str = nest_from_op(
 | 
					 | 
				
			||||||
            input_op=op_repr,
 | 
					 | 
				
			||||||
            op_suffix='',
 | 
					 | 
				
			||||||
            nest_prefix='',
 | 
					 | 
				
			||||||
            text=chan.pformat(),
 | 
					 | 
				
			||||||
            nest_indent=len(op_repr)-1,
 | 
					 | 
				
			||||||
            rm_from_first_ln='<',
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        con_teardown_status: str = (
 | 
					        con_teardown_status: str = (
 | 
				
			||||||
            f'IPC channel disconnect\n'
 | 
					            f'IPC channel disconnected:\n'
 | 
				
			||||||
            f'\n'
 | 
					            f'<=x uid: {chan.uid}\n'
 | 
				
			||||||
            f'{chan_repr}\n'
 | 
					            f'   |_{pformat(chan)}\n\n'
 | 
				
			||||||
            f'\n'
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					 | 
				
			||||||
        chans.remove(chan)
 | 
					        chans.remove(chan)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # TODO: do we need to be this pedantic?
 | 
					        # TODO: do we need to be this pedantic?
 | 
				
			||||||
        if not chans:
 | 
					        if not chans:
 | 
				
			||||||
            con_teardown_status += (
 | 
					            con_teardown_status += (
 | 
				
			||||||
                f'-> No more channels with {chan.aid.reprol()!r}\n'
 | 
					                f'-> No more channels with {chan.uid}'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            server._peers.pop(uid, None)
 | 
					            server._peers.pop(uid, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if peers := list(server._peers.values()):
 | 
					        peers_str: str = ''
 | 
				
			||||||
            peer_cnt: int = len(peers)
 | 
					        for uid, chans in server._peers.items():
 | 
				
			||||||
            if (
 | 
					            peers_str += (
 | 
				
			||||||
                (first := peers[0][0]) is not chan
 | 
					                f'uid: {uid}\n'
 | 
				
			||||||
                and
 | 
					            )
 | 
				
			||||||
                not disconnected
 | 
					            for i, chan in enumerate(chans):
 | 
				
			||||||
                and
 | 
					                peers_str += (
 | 
				
			||||||
                peer_cnt > 1
 | 
					                    f' |_[{i}] {pformat(chan)}\n'
 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                con_teardown_status += (
 | 
					 | 
				
			||||||
                    f'-> Remaining IPC {peer_cnt-1!r} peers:\n'
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                for chans in server._peers.values():
 | 
					
 | 
				
			||||||
                    first: Channel = chans[0]
 | 
					        con_teardown_status += (
 | 
				
			||||||
                    if not (
 | 
					            f'-> Remaining IPC {len(server._peers)} peers: {peers_str}\n'
 | 
				
			||||||
                        first is chan
 | 
					        )
 | 
				
			||||||
                        and
 | 
					 | 
				
			||||||
                        disconnected
 | 
					 | 
				
			||||||
                    ):
 | 
					 | 
				
			||||||
                        con_teardown_status += (
 | 
					 | 
				
			||||||
                            f'  |_{first.aid.reprol()!r} -> {len(chans)!r} chans\n'
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # No more channels to other actors (at all) registered
 | 
					        # No more channels to other actors (at all) registered
 | 
				
			||||||
        # as connected.
 | 
					        # as connected.
 | 
				
			||||||
        if not server._peers:
 | 
					        if not server._peers:
 | 
				
			||||||
            con_teardown_status += (
 | 
					            con_teardown_status += (
 | 
				
			||||||
                '-> Signalling no more peer connections!\n'
 | 
					                'Signalling no more peer channel connections'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            server._no_more_peers.set()
 | 
					            server._no_more_peers.set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -541,8 +504,8 @@ async def handle_stream_from_peer(
 | 
				
			||||||
                and
 | 
					                and
 | 
				
			||||||
                _state.is_debug_mode()
 | 
					                _state.is_debug_mode()
 | 
				
			||||||
            ):
 | 
					            ):
 | 
				
			||||||
                from ..devx import debug
 | 
					                from ..devx import _debug
 | 
				
			||||||
                pdb_lock = debug.Lock
 | 
					                pdb_lock = _debug.Lock
 | 
				
			||||||
                pdb_lock._blocked.add(uid)
 | 
					                pdb_lock._blocked.add(uid)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # TODO: NEEEDS TO BE TESTED!
 | 
					                # TODO: NEEEDS TO BE TESTED!
 | 
				
			||||||
| 
						 | 
					@ -556,7 +519,7 @@ async def handle_stream_from_peer(
 | 
				
			||||||
                    and
 | 
					                    and
 | 
				
			||||||
                    (ctx_in_debug := pdb_lock.ctx_in_debug)
 | 
					                    (ctx_in_debug := pdb_lock.ctx_in_debug)
 | 
				
			||||||
                    and
 | 
					                    and
 | 
				
			||||||
                    (pdb_user_uid := ctx_in_debug.chan.aid)
 | 
					                    (pdb_user_uid := ctx_in_debug.chan.uid)
 | 
				
			||||||
                ):
 | 
					                ):
 | 
				
			||||||
                    entry: tuple|None = local_nursery._children.get(
 | 
					                    entry: tuple|None = local_nursery._children.get(
 | 
				
			||||||
                        tuple(pdb_user_uid)
 | 
					                        tuple(pdb_user_uid)
 | 
				
			||||||
| 
						 | 
					@ -577,7 +540,7 @@ async def handle_stream_from_peer(
 | 
				
			||||||
                                f'last disconnected child uid: {uid}\n'
 | 
					                                f'last disconnected child uid: {uid}\n'
 | 
				
			||||||
                                f'locking child uid: {pdb_user_uid}\n'
 | 
					                                f'locking child uid: {pdb_user_uid}\n'
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
                            await debug.maybe_wait_for_debugger(
 | 
					                            await _debug.maybe_wait_for_debugger(
 | 
				
			||||||
                                child_in_debug=True
 | 
					                                child_in_debug=True
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -614,12 +577,12 @@ async def handle_stream_from_peer(
 | 
				
			||||||
    # finally block closure
 | 
					    # finally block closure
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Endpoint(Struct):
 | 
					class IPCEndpoint(Struct):
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    An instance of an IPC "bound" address where the lifetime of an
 | 
					    An instance of an IPC "bound" address where the lifetime of the
 | 
				
			||||||
    "ability to accept connections" and handle the subsequent
 | 
					    "ability to accept connections" (from clients) and then handle
 | 
				
			||||||
    sequence-of-packets (maybe oriented as sessions) is determined by
 | 
					    those inbound sessions or sequences-of-packets is determined by
 | 
				
			||||||
    the underlying nursery scope(s).
 | 
					    a (maybe pair of) nurser(y/ies).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    addr: Address
 | 
					    addr: Address
 | 
				
			||||||
| 
						 | 
					@ -637,24 +600,6 @@ class Endpoint(Struct):
 | 
				
			||||||
        MsgTransport,  # handle to encoded-msg transport stream
 | 
					        MsgTransport,  # handle to encoded-msg transport stream
 | 
				
			||||||
    ] = {}
 | 
					    ] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def pformat(
 | 
					 | 
				
			||||||
        self,
 | 
					 | 
				
			||||||
        indent: int = 0,
 | 
					 | 
				
			||||||
        privates: bool = False,
 | 
					 | 
				
			||||||
    ) -> str:
 | 
					 | 
				
			||||||
        type_repr: str = type(self).__name__
 | 
					 | 
				
			||||||
        fmtstr: str = (
 | 
					 | 
				
			||||||
            # !TODO, always be ns aware!
 | 
					 | 
				
			||||||
            # f'|_netns: {netns}\n'
 | 
					 | 
				
			||||||
            f' |.addr: {self.addr!r}\n'
 | 
					 | 
				
			||||||
            f' |_peers: {len(self.peer_tpts)}\n'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
            f'<{type_repr}(\n'
 | 
					 | 
				
			||||||
            f'{fmtstr}'
 | 
					 | 
				
			||||||
            f')>'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async def start_listener(self) -> SocketListener:
 | 
					    async def start_listener(self) -> SocketListener:
 | 
				
			||||||
        tpt_mod: ModuleType = inspect.getmodule(self.addr)
 | 
					        tpt_mod: ModuleType = inspect.getmodule(self.addr)
 | 
				
			||||||
        lstnr: SocketListener = await tpt_mod.start_listener(
 | 
					        lstnr: SocketListener = await tpt_mod.start_listener(
 | 
				
			||||||
| 
						 | 
					@ -691,43 +636,35 @@ class Endpoint(Struct):
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Server(Struct):
 | 
					class IPCServer(Struct):
 | 
				
			||||||
    _parent_tn: Nursery
 | 
					    _parent_tn: Nursery
 | 
				
			||||||
    _stream_handler_tn: Nursery
 | 
					    _stream_handler_tn: Nursery
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # level-triggered sig for whether "no peers are currently
 | 
					    # level-triggered sig for whether "no peers are currently
 | 
				
			||||||
    # connected"; field is **always** set to an instance but
 | 
					    # connected"; field is **always** set to an instance but
 | 
				
			||||||
    # initialized with `.is_set() == True`.
 | 
					    # initialized with `.is_set() == True`.
 | 
				
			||||||
    _no_more_peers: trio.Event
 | 
					    _no_more_peers: trio.Event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # active eps as allocated by `.listen_on()`
 | 
					    _endpoints: list[IPCEndpoint] = []
 | 
				
			||||||
    _endpoints: list[Endpoint] = []
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # connection tracking & mgmt
 | 
					    # connection tracking & mgmt
 | 
				
			||||||
    _peers: defaultdict[
 | 
					    _peers: defaultdict[
 | 
				
			||||||
        str,  # uaid
 | 
					        str,  # uaid
 | 
				
			||||||
        list[Channel],  # IPC conns from peer
 | 
					        list[Channel],  # IPC conns from peer
 | 
				
			||||||
    ] = defaultdict(list)
 | 
					    ] = defaultdict(list)
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # events-table with entries registered unset while the local
 | 
					 | 
				
			||||||
    # actor is waiting on a new actor to inbound connect, often
 | 
					 | 
				
			||||||
    # a parent waiting on its child just after spawn.
 | 
					 | 
				
			||||||
    _peer_connected: dict[
 | 
					    _peer_connected: dict[
 | 
				
			||||||
        tuple[str, str],
 | 
					        tuple[str, str],
 | 
				
			||||||
        trio.Event,
 | 
					        trio.Event,
 | 
				
			||||||
    ] = {}
 | 
					    ] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # syncs for setup/teardown sequences
 | 
					    # syncs for setup/teardown sequences
 | 
				
			||||||
    # - null when not yet booted,
 | 
					 | 
				
			||||||
    # - unset when active,
 | 
					 | 
				
			||||||
    # - set when fully shutdown with 0 eps active.
 | 
					 | 
				
			||||||
    _shutdown: trio.Event|None = None
 | 
					    _shutdown: trio.Event|None = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # TODO, maybe just make `._endpoints: list[Endpoint]` and
 | 
					    # TODO, maybe just make `._endpoints: list[IPCEndpoint]` and
 | 
				
			||||||
    # provide dict-views onto it?
 | 
					    # provide dict-views onto it?
 | 
				
			||||||
    # @property
 | 
					    # @property
 | 
				
			||||||
    # def addrs2eps(self) -> dict[Address, Endpoint]:
 | 
					    # def addrs2eps(self) -> dict[Address, IPCEndpoint]:
 | 
				
			||||||
    #     ...
 | 
					    #     ...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def proto_keys(self) -> list[str]:
 | 
					    def proto_keys(self) -> list[str]:
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
| 
						 | 
					@ -753,7 +690,7 @@ class Server(Struct):
 | 
				
			||||||
            # TODO: obvi a different server type when we eventually
 | 
					            # TODO: obvi a different server type when we eventually
 | 
				
			||||||
            # support some others XD
 | 
					            # support some others XD
 | 
				
			||||||
            log.runtime(
 | 
					            log.runtime(
 | 
				
			||||||
                f'Cancelling server(s) for tpt-protos\n'
 | 
					                f'Cancelling server(s) for\n'
 | 
				
			||||||
                f'{self.proto_keys!r}\n'
 | 
					                f'{self.proto_keys!r}\n'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            self._parent_tn.cancel_scope.cancel()
 | 
					            self._parent_tn.cancel_scope.cancel()
 | 
				
			||||||
| 
						 | 
					@ -771,7 +708,7 @@ class Server(Struct):
 | 
				
			||||||
            await self._shutdown.wait()
 | 
					            await self._shutdown.wait()
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            tpt_protos: list[str] = []
 | 
					            tpt_protos: list[str] = []
 | 
				
			||||||
            ep: Endpoint
 | 
					            ep: IPCEndpoint
 | 
				
			||||||
            for ep in self._endpoints:
 | 
					            for ep in self._endpoints:
 | 
				
			||||||
                tpt_protos.append(ep.addr.proto_key)
 | 
					                tpt_protos.append(ep.addr.proto_key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -780,14 +717,6 @@ class Server(Struct):
 | 
				
			||||||
                f'protos: {tpt_protos!r}\n'
 | 
					                f'protos: {tpt_protos!r}\n'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def len_peers(
 | 
					 | 
				
			||||||
        self,
 | 
					 | 
				
			||||||
    ) -> int:
 | 
					 | 
				
			||||||
        return len([
 | 
					 | 
				
			||||||
            chan.connected()
 | 
					 | 
				
			||||||
            for chan in chain(*self._peers.values())
 | 
					 | 
				
			||||||
        ])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def has_peers(
 | 
					    def has_peers(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        check_chans: bool = False,
 | 
					        check_chans: bool = False,
 | 
				
			||||||
| 
						 | 
					@ -801,11 +730,13 @@ class Server(Struct):
 | 
				
			||||||
            has_peers
 | 
					            has_peers
 | 
				
			||||||
            and
 | 
					            and
 | 
				
			||||||
            check_chans
 | 
					            check_chans
 | 
				
			||||||
            and
 | 
					 | 
				
			||||||
            (peer_cnt := self.len_peers())
 | 
					 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            has_peers: bool = (
 | 
					            has_peers: bool = (
 | 
				
			||||||
                peer_cnt > 0
 | 
					                any(chan.connected()
 | 
				
			||||||
 | 
					                    for chan in chain(
 | 
				
			||||||
 | 
					                        *self._peers.values()
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
                and
 | 
					                and
 | 
				
			||||||
                has_peers
 | 
					                has_peers
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					@ -814,14 +745,10 @@ class Server(Struct):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def wait_for_no_more_peers(
 | 
					    async def wait_for_no_more_peers(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        # XXX, should this even be allowed?
 | 
					        shield: bool = False,
 | 
				
			||||||
        # -> i've seen it cause hangs on teardown
 | 
					 | 
				
			||||||
        #    in `test_resource_cache.py`
 | 
					 | 
				
			||||||
        # _shield: bool = False,
 | 
					 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        await self._no_more_peers.wait()
 | 
					        with trio.CancelScope(shield=shield):
 | 
				
			||||||
        # with trio.CancelScope(shield=_shield):
 | 
					            await self._no_more_peers.wait()
 | 
				
			||||||
        #     await self._no_more_peers.wait()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async def wait_for_peer(
 | 
					    async def wait_for_peer(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
| 
						 | 
					@ -863,7 +790,7 @@ class Server(Struct):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def epsdict(self) -> dict[
 | 
					    def epsdict(self) -> dict[
 | 
				
			||||||
        Address,
 | 
					        Address,
 | 
				
			||||||
        Endpoint,
 | 
					        IPCEndpoint,
 | 
				
			||||||
    ]:
 | 
					    ]:
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            ep.addr: ep
 | 
					            ep.addr: ep
 | 
				
			||||||
| 
						 | 
					@ -876,66 +803,30 @@ class Server(Struct):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return ev.is_set()
 | 
					        return ev.is_set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    def pformat(self) -> str:
 | 
				
			||||||
    def repr_state(self) -> str:
 | 
					        eps: list[IPCEndpoint] = self._endpoints
 | 
				
			||||||
        '''
 | 
					 | 
				
			||||||
        A `str`-status describing the current state of this
 | 
					 | 
				
			||||||
        IPC server in terms of the current operating "phase".
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        '''
 | 
					        state_repr: str = (
 | 
				
			||||||
        status = 'server is active'
 | 
					            f'{len(eps)!r} IPC-endpoints active'
 | 
				
			||||||
        if self.has_peers():
 | 
					 | 
				
			||||||
            peer_cnt: int = self.len_peers()
 | 
					 | 
				
			||||||
            status: str = (
 | 
					 | 
				
			||||||
                f'{peer_cnt!r} peer chans'
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            status: str = 'No peer chans'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if self.is_shutdown():
 | 
					 | 
				
			||||||
            status: str = 'server-shutdown'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return status
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def pformat(
 | 
					 | 
				
			||||||
        self,
 | 
					 | 
				
			||||||
        privates: bool = False,
 | 
					 | 
				
			||||||
    ) -> str:
 | 
					 | 
				
			||||||
        eps: list[Endpoint] = self._endpoints
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # state_repr: str = (
 | 
					 | 
				
			||||||
        #     f'{len(eps)!r} endpoints active'
 | 
					 | 
				
			||||||
        # )
 | 
					 | 
				
			||||||
        fmtstr = (
 | 
					 | 
				
			||||||
            f' |_state: {self.repr_state!r}\n'
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        if privates:
 | 
					        fmtstr = (
 | 
				
			||||||
            fmtstr += f'   no_more_peers: {self.has_peers()}\n'
 | 
					            f' |_state: {state_repr}\n'
 | 
				
			||||||
 | 
					            f'   no_more_peers: {self.has_peers()}\n'
 | 
				
			||||||
            if self._shutdown is not None:
 | 
					        )
 | 
				
			||||||
                shutdown_stats: EventStatistics = self._shutdown.statistics()
 | 
					        if self._shutdown is not None:
 | 
				
			||||||
                fmtstr += (
 | 
					            shutdown_stats: EventStatistics = self._shutdown.statistics()
 | 
				
			||||||
                    f'   task_waiting_on_shutdown: {shutdown_stats}\n'
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if eps := self._endpoints:
 | 
					 | 
				
			||||||
            addrs: list[tuple] = [
 | 
					 | 
				
			||||||
                ep.addr for ep in eps
 | 
					 | 
				
			||||||
            ]
 | 
					 | 
				
			||||||
            repr_eps: str = ppfmt(addrs)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            fmtstr += (
 | 
					            fmtstr += (
 | 
				
			||||||
                f' |_endpoints: {repr_eps}\n'
 | 
					                f'   task_waiting_on_shutdown: {shutdown_stats}\n'
 | 
				
			||||||
                # ^TODO? how to indent closing ']'..
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if peers := self._peers:
 | 
					        fmtstr += (
 | 
				
			||||||
            fmtstr += (
 | 
					            # TODO, use the `ppfmt()` helper from `modden`!
 | 
				
			||||||
                f' |_peers: {len(peers)} connected\n'
 | 
					            f' |_endpoints: {pformat(self._endpoints)}\n'
 | 
				
			||||||
            )
 | 
					            f' |_peers: {len(self._peers)} connected\n'
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
            f'<Server(\n'
 | 
					            f'<IPCServer(\n'
 | 
				
			||||||
            f'{fmtstr}'
 | 
					            f'{fmtstr}'
 | 
				
			||||||
            f')>\n'
 | 
					            f')>\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -944,13 +835,13 @@ class Server(Struct):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # TODO? maybe allow shutting down a `.listen_on()`s worth of
 | 
					    # TODO? maybe allow shutting down a `.listen_on()`s worth of
 | 
				
			||||||
    # listeners by cancelling the corresponding
 | 
					    # listeners by cancelling the corresponding
 | 
				
			||||||
    # `Endpoint._listen_tn` only ?
 | 
					    # `IPCEndpoint._listen_tn` only ?
 | 
				
			||||||
    # -[ ] in theory you could use this to
 | 
					    # -[ ] in theory you could use this to
 | 
				
			||||||
    #     "boot-and-wait-for-reconnect" of all current and connecting
 | 
					    #     "boot-and-wait-for-reconnect" of all current and connecting
 | 
				
			||||||
    #     peers?
 | 
					    #     peers?
 | 
				
			||||||
    #  |_ would require that the stream-handler is intercepted so we
 | 
					    #  |_ would require that the stream-handler is intercepted so we
 | 
				
			||||||
    #     can intercept every `MsgTransport` (stream) and track per
 | 
					    #     can intercept every `MsgTransport` (stream) and track per
 | 
				
			||||||
    #     `Endpoint` likely?
 | 
					    #     `IPCEndpoint` likely?
 | 
				
			||||||
    #
 | 
					    #
 | 
				
			||||||
    # async def unlisten(
 | 
					    # async def unlisten(
 | 
				
			||||||
    #     self,
 | 
					    #     self,
 | 
				
			||||||
| 
						 | 
					@ -963,7 +854,7 @@ class Server(Struct):
 | 
				
			||||||
        *,
 | 
					        *,
 | 
				
			||||||
        accept_addrs: list[tuple[str, int|str]]|None = None,
 | 
					        accept_addrs: list[tuple[str, int|str]]|None = None,
 | 
				
			||||||
        stream_handler_nursery: Nursery|None = None,
 | 
					        stream_handler_nursery: Nursery|None = None,
 | 
				
			||||||
    ) -> list[Endpoint]:
 | 
					    ) -> list[IPCEndpoint]:
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        Start `SocketListeners` (i.e. bind and call `socket.listen()`)
 | 
					        Start `SocketListeners` (i.e. bind and call `socket.listen()`)
 | 
				
			||||||
        for all IPC-transport-protocol specific `Address`-types
 | 
					        for all IPC-transport-protocol specific `Address`-types
 | 
				
			||||||
| 
						 | 
					@ -994,36 +885,26 @@ class Server(Struct):
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        log.runtime(
 | 
					        log.runtime(
 | 
				
			||||||
            f'Binding endpoints\n'
 | 
					            f'Binding to endpoints for,\n'
 | 
				
			||||||
            f'{ppfmt(accept_addrs)}\n'
 | 
					            f'{accept_addrs}\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        eps: list[Endpoint] = await self._parent_tn.start(
 | 
					        eps: list[IPCEndpoint] = await self._parent_tn.start(
 | 
				
			||||||
            partial(
 | 
					            partial(
 | 
				
			||||||
                _serve_ipc_eps,
 | 
					                _serve_ipc_eps,
 | 
				
			||||||
                server=self,
 | 
					                server=self,
 | 
				
			||||||
                stream_handler_tn=(
 | 
					                stream_handler_tn=stream_handler_nursery,
 | 
				
			||||||
                    stream_handler_nursery
 | 
					 | 
				
			||||||
                    or
 | 
					 | 
				
			||||||
                    self._stream_handler_tn
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                listen_addrs=accept_addrs,
 | 
					                listen_addrs=accept_addrs,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self._endpoints.extend(eps)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        serv_repr: str = nest_from_op(
 | 
					 | 
				
			||||||
            input_op='(>',
 | 
					 | 
				
			||||||
            text=self.pformat(),
 | 
					 | 
				
			||||||
            nest_indent=1,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        log.runtime(
 | 
					        log.runtime(
 | 
				
			||||||
            f'Started IPC server\n'
 | 
					            f'Started IPC endpoints\n'
 | 
				
			||||||
            f'{serv_repr}'
 | 
					            f'{eps}\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # XXX, a little sanity on new ep allocations
 | 
					        self._endpoints.extend(eps)
 | 
				
			||||||
 | 
					        # XXX, just a little bit of sanity
 | 
				
			||||||
        group_tn: Nursery|None = None
 | 
					        group_tn: Nursery|None = None
 | 
				
			||||||
        ep: Endpoint
 | 
					        ep: IPCEndpoint
 | 
				
			||||||
        for ep in eps:
 | 
					        for ep in eps:
 | 
				
			||||||
            if ep.addr not in self.addrs:
 | 
					            if ep.addr not in self.addrs:
 | 
				
			||||||
                breakpoint()
 | 
					                breakpoint()
 | 
				
			||||||
| 
						 | 
					@ -1036,10 +917,6 @@ class Server(Struct):
 | 
				
			||||||
        return eps
 | 
					        return eps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# alias until we decide on final naming
 | 
					 | 
				
			||||||
IPCServer = Server
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async def _serve_ipc_eps(
 | 
					async def _serve_ipc_eps(
 | 
				
			||||||
    *,
 | 
					    *,
 | 
				
			||||||
    server: IPCServer,
 | 
					    server: IPCServer,
 | 
				
			||||||
| 
						 | 
					@ -1064,24 +941,20 @@ async def _serve_ipc_eps(
 | 
				
			||||||
        listen_tn: Nursery
 | 
					        listen_tn: Nursery
 | 
				
			||||||
        async with trio.open_nursery() as listen_tn:
 | 
					        async with trio.open_nursery() as listen_tn:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            eps: list[Endpoint] = []
 | 
					            eps: list[IPCEndpoint] = []
 | 
				
			||||||
            # XXX NOTE, required to call `serve_listeners()` below.
 | 
					            # XXX NOTE, required to call `serve_listeners()` below.
 | 
				
			||||||
            # ?TODO, maybe just pass `list(eps.values()` tho?
 | 
					            # ?TODO, maybe just pass `list(eps.values()` tho?
 | 
				
			||||||
            listeners: list[trio.abc.Listener] = []
 | 
					            listeners: list[trio.abc.Listener] = []
 | 
				
			||||||
            for addr in listen_addrs:
 | 
					            for addr in listen_addrs:
 | 
				
			||||||
                ep = Endpoint(
 | 
					                ep = IPCEndpoint(
 | 
				
			||||||
                    addr=addr,
 | 
					                    addr=addr,
 | 
				
			||||||
                    listen_tn=listen_tn,
 | 
					                    listen_tn=listen_tn,
 | 
				
			||||||
                    stream_handler_tn=stream_handler_tn,
 | 
					                    stream_handler_tn=stream_handler_tn,
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    ep_sclang: str = nest_from_op(
 | 
					 | 
				
			||||||
                        input_op='>[',
 | 
					 | 
				
			||||||
                        text=f'{ep.pformat()}',
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                    log.runtime(
 | 
					                    log.runtime(
 | 
				
			||||||
                        f'Starting new endpoint listener\n'
 | 
					                        f'Starting new endpoint listener\n'
 | 
				
			||||||
                        f'{ep_sclang}\n'
 | 
					                        f'{ep}\n'
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    listener: trio.abc.Listener = await ep.start_listener()
 | 
					                    listener: trio.abc.Listener = await ep.start_listener()
 | 
				
			||||||
                    assert listener is ep._listener
 | 
					                    assert listener is ep._listener
 | 
				
			||||||
| 
						 | 
					@ -1119,6 +992,17 @@ async def _serve_ipc_eps(
 | 
				
			||||||
                    handler_nursery=stream_handler_tn
 | 
					                    handler_nursery=stream_handler_tn
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					            # TODO, wow make this message better! XD
 | 
				
			||||||
 | 
					            log.runtime(
 | 
				
			||||||
 | 
					                'Started server(s)\n'
 | 
				
			||||||
 | 
					                +
 | 
				
			||||||
 | 
					                '\n'.join([f'|_{addr}' for addr in listen_addrs])
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            log.runtime(
 | 
				
			||||||
 | 
					                f'Started IPC endpoints\n'
 | 
				
			||||||
 | 
					                f'{eps}\n'
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
            task_status.started(
 | 
					            task_status.started(
 | 
				
			||||||
                eps,
 | 
					                eps,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
| 
						 | 
					@ -1126,7 +1010,7 @@ async def _serve_ipc_eps(
 | 
				
			||||||
    finally:
 | 
					    finally:
 | 
				
			||||||
        if eps:
 | 
					        if eps:
 | 
				
			||||||
            addr: Address
 | 
					            addr: Address
 | 
				
			||||||
            ep: Endpoint
 | 
					            ep: IPCEndpoint
 | 
				
			||||||
            for addr, ep in server.epsdict().items():
 | 
					            for addr, ep in server.epsdict().items():
 | 
				
			||||||
                ep.close_listener()
 | 
					                ep.close_listener()
 | 
				
			||||||
                server._endpoints.remove(ep)
 | 
					                server._endpoints.remove(ep)
 | 
				
			||||||
| 
						 | 
					@ -1149,23 +1033,20 @@ async def open_ipc_server(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async with maybe_open_nursery(
 | 
					    async with maybe_open_nursery(
 | 
				
			||||||
        nursery=parent_tn,
 | 
					        nursery=parent_tn,
 | 
				
			||||||
    ) as parent_tn:
 | 
					    ) as rent_tn:
 | 
				
			||||||
        no_more_peers = trio.Event()
 | 
					        no_more_peers = trio.Event()
 | 
				
			||||||
        no_more_peers.set()
 | 
					        no_more_peers.set()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ipc_server = IPCServer(
 | 
					        ipc_server = IPCServer(
 | 
				
			||||||
            _parent_tn=parent_tn,
 | 
					            _parent_tn=rent_tn,
 | 
				
			||||||
            _stream_handler_tn=(
 | 
					            _stream_handler_tn=stream_handler_tn or rent_tn,
 | 
				
			||||||
                stream_handler_tn
 | 
					 | 
				
			||||||
                or
 | 
					 | 
				
			||||||
                parent_tn
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            _no_more_peers=no_more_peers,
 | 
					            _no_more_peers=no_more_peers,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            yield ipc_server
 | 
					            yield ipc_server
 | 
				
			||||||
            log.runtime(
 | 
					            log.runtime(
 | 
				
			||||||
                'Server-tn running until terminated\n'
 | 
					                f'Waiting on server to shutdown or be cancelled..\n'
 | 
				
			||||||
 | 
					                f'{ipc_server}'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            # TODO? when if ever would we want/need this?
 | 
					            # TODO? when if ever would we want/need this?
 | 
				
			||||||
            # with trio.CancelScope(shield=True):
 | 
					            # with trio.CancelScope(shield=True):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -23,15 +23,14 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -62,7 +61,7 @@ except ImportError:
 | 
				
			||||||
log = get_logger(__name__)
 | 
					log = get_logger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SharedMemory = disable_mantracker()
 | 
					disable_mantracker()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SharedInt:
 | 
					class SharedInt:
 | 
				
			||||||
| 
						 | 
					@ -790,23 +789,11 @@ 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,7 +18,6 @@ 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,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -51,45 +50,13 @@ 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:
 | 
				
			||||||
| 
						 | 
					@ -160,9 +127,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(
 | 
					    log.info(
 | 
				
			||||||
        f'Trying socket bind\n'
 | 
					        f'Attempting to bind TCP socket\n'
 | 
				
			||||||
        f'>[ {addr}\n'
 | 
					        f'>[\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?
 | 
				
			||||||
| 
						 | 
					@ -177,10 +145,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(
 | 
					    log.info(
 | 
				
			||||||
        f'Listening on TCP socket\n'
 | 
					        f'Listening on TCP socket\n'
 | 
				
			||||||
        f'[> {bound_addr}\n'
 | 
					        f'[>\n'
 | 
				
			||||||
 | 
					        f' |_{addr}\n'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    return listener
 | 
					    return listener
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -430,25 +430,20 @@ 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,
 | 
				
			||||||
                trio.ClosedResourceError,
 | 
					            ) as bre:
 | 
				
			||||||
            ) as _re:
 | 
					                trans_err = bre
 | 
				
			||||||
                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'
 | 
					                        '[Errno 32] Broken pipe' in trans_err.args[0]
 | 
				
			||||||
                        in
 | 
					                        # ^XXX, specifc to UDS transport and its,
 | 
				
			||||||
                        trans_err.args[0]
 | 
					                        # 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.
 | 
				
			||||||
                    ):
 | 
					                    ):
 | 
				
			||||||
                        tpt_closed = TransportClosed.from_src_exc(
 | 
					                        raise TransportClosed.from_src_exc(
 | 
				
			||||||
                            message=(
 | 
					                            message=(
 | 
				
			||||||
                                f'{tpt_name} already closed by peer\n'
 | 
					                                f'{tpt_name} already closed by peer\n'
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
| 
						 | 
					@ -456,31 +451,14 @@ 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(
 | 
				
			||||||
                            f'{tpt_name} layer failed pre-send ??\n'
 | 
					                            '{tpt_name} layer failed pre-send ??\n'
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        raise trans_err
 | 
					                        raise trans_err
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -525,7 +503,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: 1\n'
 | 
					            f' |_peers: 2\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,9 +18,6 @@ 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 (
 | 
				
			||||||
| 
						 | 
					@ -32,7 +29,6 @@ from socket import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
import struct
 | 
					import struct
 | 
				
			||||||
from typing import (
 | 
					from typing import (
 | 
				
			||||||
    Type,
 | 
					 | 
				
			||||||
    TYPE_CHECKING,
 | 
					    TYPE_CHECKING,
 | 
				
			||||||
    ClassVar,
 | 
					    ClassVar,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					@ -103,6 +99,8 @@ class UDSAddress(
 | 
				
			||||||
            self.filedir
 | 
					            self.filedir
 | 
				
			||||||
            or
 | 
					            or
 | 
				
			||||||
            self.def_bindspace
 | 
					            self.def_bindspace
 | 
				
			||||||
 | 
					            # or
 | 
				
			||||||
 | 
					            # get_rt_dir()
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
| 
						 | 
					@ -207,35 +205,12 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -246,25 +221,17 @@ 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
 | 
				
			||||||
    if not (bs := addr.bindspace).is_dir():
 | 
					    try:
 | 
				
			||||||
        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(
 | 
				
			||||||
| 
						 | 
					@ -389,30 +356,27 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tpt_stream = MsgpackUDSStream(
 | 
					        stream = MsgpackUDSStream(
 | 
				
			||||||
            stream,
 | 
					            stream,
 | 
				
			||||||
            prefix_size=prefix_size,
 | 
					            prefix_size=prefix_size,
 | 
				
			||||||
            codec=codec
 | 
					            codec=codec
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # XXX assign from new addrs after peer-PID extract!
 | 
					        stream._raddr = addr
 | 
				
			||||||
        (
 | 
					        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,35 +81,10 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -295,9 +270,7 @@ def get_logger(
 | 
				
			||||||
    subsys_spec: str|None = None,
 | 
					    subsys_spec: str|None = None,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> StackLevelAdapter:
 | 
					) -> StackLevelAdapter:
 | 
				
			||||||
    '''
 | 
					    '''Return the package log or a sub-logger for ``name`` if provided.
 | 
				
			||||||
    Return the `tractor`-library root logger or a sub-logger for
 | 
					 | 
				
			||||||
    `name` if provided.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    log: Logger
 | 
					    log: Logger
 | 
				
			||||||
| 
						 | 
					@ -309,7 +282,7 @@ def get_logger(
 | 
				
			||||||
        name != _proj_name
 | 
					        name != _proj_name
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # NOTE: for handling for modules that use `get_logger(__name__)`
 | 
					        # NOTE: for handling for modules that use ``get_logger(__name__)``
 | 
				
			||||||
        # we make the following stylistic choice:
 | 
					        # we make the following stylistic choice:
 | 
				
			||||||
        # - always avoid duplicate project-package token
 | 
					        # - always avoid duplicate project-package token
 | 
				
			||||||
        #   in msg output: i.e. tractor.tractor.ipc._chan.py in header
 | 
					        #   in msg output: i.e. tractor.tractor.ipc._chan.py in header
 | 
				
			||||||
| 
						 | 
					@ -358,7 +331,7 @@ def get_logger(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_console_log(
 | 
					def get_console_log(
 | 
				
			||||||
    level: str|None = None,
 | 
					    level: str|None = None,
 | 
				
			||||||
    logger: Logger|StackLevelAdapter|None = None,
 | 
					    logger: Logger|None = None,
 | 
				
			||||||
    **kwargs,
 | 
					    **kwargs,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> LoggerAdapter:
 | 
					) -> LoggerAdapter:
 | 
				
			||||||
| 
						 | 
					@ -371,23 +344,12 @@ def get_console_log(
 | 
				
			||||||
    Yeah yeah, i know we can use `logging.config.dictConfig()`. You do it.
 | 
					    Yeah yeah, i know we can use `logging.config.dictConfig()`. You do it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    # get/create a stack-aware-adapter
 | 
					    log = get_logger(
 | 
				
			||||||
    if (
 | 
					        logger=logger,
 | 
				
			||||||
        logger
 | 
					        **kwargs
 | 
				
			||||||
        and
 | 
					    )  # set a root logger
 | 
				
			||||||
        isinstance(logger, StackLevelAdapter)
 | 
					    logger: Logger = log.logger
 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        # XXX, for ex. when passed in by a caller wrapping some
 | 
					 | 
				
			||||||
        # other lib's logger instance with our level-adapter.
 | 
					 | 
				
			||||||
        log = logger
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        log: StackLevelAdapter = get_logger(
 | 
					 | 
				
			||||||
            logger=logger,
 | 
					 | 
				
			||||||
            **kwargs
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    logger: Logger|StackLevelAdapter = log.logger
 | 
					 | 
				
			||||||
    if not level:
 | 
					    if not level:
 | 
				
			||||||
        return log
 | 
					        return log
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -405,7 +367,10 @@ def get_console_log(
 | 
				
			||||||
            None,
 | 
					            None,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        fmt: str = LOG_FORMAT  # always apply our format?
 | 
					        fmt = LOG_FORMAT
 | 
				
			||||||
 | 
					        # if logger:
 | 
				
			||||||
 | 
					        #     fmt = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        handler = StreamHandler()
 | 
					        handler = StreamHandler()
 | 
				
			||||||
        formatter = colorlog.ColoredFormatter(
 | 
					        formatter = colorlog.ColoredFormatter(
 | 
				
			||||||
            fmt=fmt,
 | 
					            fmt=fmt,
 | 
				
			||||||
| 
						 | 
					@ -426,3 +391,19 @@ 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,14 +210,12 @@ 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:
 | 
				
			||||||
| 
						 | 
					@ -263,9 +261,8 @@ class PldRx(Struct):
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            type(msg) is Return
 | 
					            type(msg) is Return
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            log.runtime(
 | 
					            log.info(
 | 
				
			||||||
                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(
 | 
				
			||||||
| 
						 | 
					@ -307,13 +304,10 @@ class PldRx(Struct):
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    pld: PayloadT = self._pld_dec.decode(pld)
 | 
					                    pld: PayloadT = self._pld_dec.decode(pld)
 | 
				
			||||||
                    log.runtime(
 | 
					                    log.runtime(
 | 
				
			||||||
                        f'Decoded payload for\n'
 | 
					                        'Decoded msg payload\n\n'
 | 
				
			||||||
                        # f'\n'
 | 
					 | 
				
			||||||
                        f'{msg}\n'
 | 
					                        f'{msg}\n'
 | 
				
			||||||
                        # ^TODO?, ideally just render with `,
 | 
					                        f'where payload decoded as\n'
 | 
				
			||||||
                        # pld={decode}` in the `msg.pformat()`??
 | 
					                        f'|_pld={pld!r}\n'
 | 
				
			||||||
                        f'where, '
 | 
					 | 
				
			||||||
                        f'{type(msg).__name__}.pld={pld!r}\n'
 | 
					 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    return pld
 | 
					                    return pld
 | 
				
			||||||
                except TypeError as typerr:
 | 
					                except TypeError as typerr:
 | 
				
			||||||
| 
						 | 
					@ -500,8 +494,7 @@ def limit_plds(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    finally:
 | 
					    finally:
 | 
				
			||||||
        log.runtime(
 | 
					        log.runtime(
 | 
				
			||||||
            f'Reverted to previous payload-decoder\n'
 | 
					            'Reverted to previous payload-decoder\n\n'
 | 
				
			||||||
            f'\n'
 | 
					 | 
				
			||||||
            f'{orig_pldec}\n'
 | 
					            f'{orig_pldec}\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # sanity on orig settings
 | 
					        # sanity on orig settings
 | 
				
			||||||
| 
						 | 
					@ -613,9 +606,10 @@ async def drain_to_final_msg(
 | 
				
			||||||
            #       msg: dict = await ctx._rx_chan.receive()
 | 
					            #       msg: dict = await ctx._rx_chan.receive()
 | 
				
			||||||
            #   if res_cs.cancelled_caught:
 | 
					            #   if res_cs.cancelled_caught:
 | 
				
			||||||
            #
 | 
					            #
 | 
				
			||||||
            # -[x] 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!
 | 
				
			||||||
            #  |_see masked code below in .cancel_called path
 | 
					            # |_from tractor.devx._debug import pause
 | 
				
			||||||
 | 
					            #   await pause()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # NOTE: we get here if the far end was
 | 
					        # NOTE: we get here if the far end was
 | 
				
			||||||
        # `ContextCancelled` in 2 cases:
 | 
					        # `ContextCancelled` in 2 cases:
 | 
				
			||||||
| 
						 | 
					@ -635,8 +629,7 @@ 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(
 | 
				
			||||||
                        f'RPC-ctx cancelled by local-parent scope during drain!\n'
 | 
					                        'RPC-ctx cancelled by local-parent scope during drain!\n\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'
 | 
				
			||||||
| 
						 | 
					@ -651,10 +644,6 @@ async def drain_to_final_msg(
 | 
				
			||||||
                    f'IPC ctx cancelled externally during result drain ?\n'
 | 
					                    f'IPC ctx cancelled externally during result drain ?\n'
 | 
				
			||||||
                    f'{ctx}'
 | 
					                    f'{ctx}'
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # XXX, for tracing `Cancelled`..
 | 
					 | 
				
			||||||
                # from tractor.devx.debug import pause
 | 
					 | 
				
			||||||
                # await pause(shield=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # CASE 2: mask the local cancelled-error(s)
 | 
					            # CASE 2: mask the local cancelled-error(s)
 | 
				
			||||||
            # only when we are sure the remote error is
 | 
					            # only when we are sure the remote error is
 | 
				
			||||||
            # the source cause of this local task's
 | 
					            # the source cause of this local task's
 | 
				
			||||||
| 
						 | 
					@ -674,8 +663,7 @@ async def drain_to_final_msg(
 | 
				
			||||||
            # final result arrived!
 | 
					            # final result arrived!
 | 
				
			||||||
            case Return():
 | 
					            case Return():
 | 
				
			||||||
                log.runtime(
 | 
					                log.runtime(
 | 
				
			||||||
                    f'Context delivered final draining msg\n'
 | 
					                    '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
 | 
				
			||||||
| 
						 | 
					@ -709,14 +697,12 @@ async def drain_to_final_msg(
 | 
				
			||||||
                ):
 | 
					                ):
 | 
				
			||||||
                    log.cancel(
 | 
					                    log.cancel(
 | 
				
			||||||
                        'Cancelling `MsgStream` drain since '
 | 
					                        'Cancelling `MsgStream` drain since '
 | 
				
			||||||
                        f'{reason}\n'
 | 
					                        f'{reason}\n\n'
 | 
				
			||||||
                        f'\n'
 | 
					 | 
				
			||||||
                        f'<= {ctx.chan.uid}\n'
 | 
					                        f'<= {ctx.chan.uid}\n'
 | 
				
			||||||
                        f'  |_{ctx._nsf}()\n'
 | 
					                        f'  |_{ctx._nsf}()\n\n'
 | 
				
			||||||
                        f'\n'
 | 
					 | 
				
			||||||
                        f'=> {ctx._task}\n'
 | 
					                        f'=> {ctx._task}\n'
 | 
				
			||||||
                        f'  |_{ctx._stream}\n'
 | 
					                        f'  |_{ctx._stream}\n\n'
 | 
				
			||||||
                        f'\n'
 | 
					
 | 
				
			||||||
                        f'{pretty_struct.pformat(msg)}\n'
 | 
					                        f'{pretty_struct.pformat(msg)}\n'
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
                    break
 | 
					                    break
 | 
				
			||||||
| 
						 | 
					@ -753,8 +739,7 @@ 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
 | 
				
			||||||
                    f'Remote stream terminated due to "stop" msg\n'
 | 
					                    'Remote stream terminated due to "stop" msg:\n\n'
 | 
				
			||||||
                    f'\n'
 | 
					 | 
				
			||||||
                    f'{pretty_struct.pformat(msg)}\n'
 | 
					                    f'{pretty_struct.pformat(msg)}\n'
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
| 
						 | 
					@ -829,8 +814,7 @@ async def drain_to_final_msg(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        log.cancel(
 | 
					        log.cancel(
 | 
				
			||||||
            f'Skipping `MsgStream` drain since final outcome is set\n'
 | 
					            'Skipping `MsgStream` drain since final outcome is set\n\n'
 | 
				
			||||||
            f'\n'
 | 
					 | 
				
			||||||
            f'{ctx.outcome}\n'
 | 
					            f'{ctx.outcome}\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -106,11 +105,27 @@ def iter_fields(struct: Struct) -> Iterator[
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def iter_struct_ppfmt_lines(
 | 
					def pformat(
 | 
				
			||||||
    struct: Struct,
 | 
					    struct: Struct,
 | 
				
			||||||
    field_indent: int = 0,
 | 
					    field_indent: int = 2,
 | 
				
			||||||
) -> Iterator[tuple[str, str]]:
 | 
					    indent: int = 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					) -> 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
 | 
				
			||||||
| 
						 | 
					@ -120,18 +135,15 @@ def iter_struct_ppfmt_lines(
 | 
				
			||||||
        # ..]` 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(
 | 
					        typ_name: str = getattr(ft, '__name__', str(ft))
 | 
				
			||||||
            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):
 | 
				
			||||||
            yield from iter_struct_ppfmt_lines(
 | 
					            val_str: str =  v.pformat(
 | 
				
			||||||
                struct=v,
 | 
					                indent=field_indent + indent,
 | 
				
			||||||
                field_indent=field_indent+field_indent,
 | 
					                field_indent=indent + field_indent,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            val_str: str = repr(v)
 | 
					            val_str: str = repr(v)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -149,39 +161,8 @@ def iter_struct_ppfmt_lines(
 | 
				
			||||||
                # raise
 | 
					                # raise
 | 
				
			||||||
                # return _Struct.__repr__(struct)
 | 
					                # return _Struct.__repr__(struct)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        yield (
 | 
					        # TODO: LOLOL use `textwrap.indent()` instead dawwwwwg!
 | 
				
			||||||
            ' '*field_indent,  # indented ws prefix
 | 
					        obj_str += (field_ws + f'{k}: {typ_name} = {val_str},\n')
 | 
				
			||||||
            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,39 +154,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -203,7 +170,6 @@ 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,6 +38,7 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -48,7 +49,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,
 | 
				
			||||||
| 
						 | 
					@ -58,9 +59,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -130,7 +128,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -209,15 +206,10 @@ class LinkedTaskChannel(
 | 
				
			||||||
    async def aclose(self) -> None:
 | 
					    async def aclose(self) -> None:
 | 
				
			||||||
        await self._from_aio.aclose()
 | 
					        await self._from_aio.aclose()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # ?TODO? async version of this?
 | 
					    def started(
 | 
				
			||||||
    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -248,7 +240,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -326,7 +317,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 = True,
 | 
					    hide_tb: bool = False,
 | 
				
			||||||
    **kwargs,
 | 
					    **kwargs,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> LinkedTaskChannel:
 | 
					) -> LinkedTaskChannel:
 | 
				
			||||||
| 
						 | 
					@ -354,6 +345,18 @@ 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()
 | 
				
			||||||
| 
						 | 
					@ -368,25 +371,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -459,23 +443,9 @@ def _run_asyncio_task(
 | 
				
			||||||
                        f'Task exited with final result: {result!r}\n'
 | 
					                        f'Task exited with final result: {result!r}\n'
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # XXX ALWAYS close the child-`asyncio`-task-side's
 | 
					                # only close the sender side which will relay
 | 
				
			||||||
                # `to_trio` handle which will in turn relay
 | 
					                # a `trio.EndOfChannel` to the trio (consumer) side.
 | 
				
			||||||
                # 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(
 | 
				
			||||||
| 
						 | 
					@ -509,12 +479,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.devx(
 | 
					        log.info(
 | 
				
			||||||
            f'Bestowing `greenback` portal for `asyncio`-task\n'
 | 
					            f'Bestowing `greenback` portal for `asyncio`-task\n'
 | 
				
			||||||
            f'{task}\n'
 | 
					            f'{task}\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -673,9 +643,8 @@ 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'\n'
 | 
					                    f'{curr_aio_err}\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'
 | 
				
			||||||
| 
						 | 
					@ -787,7 +756,6 @@ 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:
 | 
				
			||||||
| 
						 | 
					@ -819,48 +787,12 @@ async def translate_aio_errors(
 | 
				
			||||||
        # )
 | 
					        # )
 | 
				
			||||||
        # raise
 | 
					        # raise
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # XXX EoC is a special SIGNAL from the aio-side here!
 | 
					    # XXX always passthrough EoC since this translator is often
 | 
				
			||||||
    # There are 2 cases to handle:
 | 
					    # called from `LinkedTaskChannel.receive()` which we want
 | 
				
			||||||
    # 1. the "EoC passthrough" case.
 | 
					    # passthrough and further we have no special meaning for it in
 | 
				
			||||||
    #   - the aio-task actually closed the channel "gracefully" and
 | 
					    # terms of relaying errors or signals from the aio side!
 | 
				
			||||||
    #     the trio-task should unwind any ongoing channel
 | 
					    except trio.EndOfChannel as eoc:
 | 
				
			||||||
    #     iteration/receiving,
 | 
					        trio_err = chan._trio_err = eoc
 | 
				
			||||||
    #  |_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
 | 
				
			||||||
| 
						 | 
					@ -909,7 +841,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(),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -1113,7 +1045,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.debug(
 | 
					            log.info(
 | 
				
			||||||
                'asyncio-task is done and unblocked trio-side!\n'
 | 
					                'asyncio-task is done and unblocked trio-side!\n'
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1130,17 +1062,11 @@ 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 raise_from
 | 
					            raise trio_to_raise from (aio_err or trio_err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if trio_to_raise:
 | 
					        if trio_to_raise:
 | 
				
			||||||
            match (
 | 
					            match (
 | 
				
			||||||
| 
						 | 
					@ -1173,7 +1099,7 @@ async def translate_aio_errors(
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
                        return
 | 
					                        return
 | 
				
			||||||
                case _:
 | 
					                case _:
 | 
				
			||||||
                    raise trio_to_raise from raise_from
 | 
					                    raise trio_to_raise from (aio_err or trio_err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # 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.
 | 
				
			||||||
| 
						 | 
					@ -1239,6 +1165,7 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -1272,6 +1199,7 @@ 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(
 | 
				
			||||||
| 
						 | 
					@ -1478,7 +1406,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,
 | 
				
			||||||
            # )
 | 
					            # )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,9 +31,4 @@ from ._broadcast import (
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from ._beg import (
 | 
					from ._beg import (
 | 
				
			||||||
    collapse_eg as collapse_eg,
 | 
					    collapse_eg as 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,
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,94 +15,31 @@
 | 
				
			||||||
# 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` utils and helpers pertaining to
 | 
					`BaseExceptionGroup` related utils and helpers pertaining to
 | 
				
			||||||
first-class-`trio` from a "historical" perspective, like "loose
 | 
					first-class-`trio` from a historical perspective B)
 | 
				
			||||||
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,
 | 
					 | 
				
			||||||
# )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# XXX NOTE
 | 
					def maybe_collapse_eg(
 | 
				
			||||||
# 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:
 | 
					 | 
				
			||||||
    """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,
 | 
					    beg: BaseExceptionGroup,
 | 
				
			||||||
 | 
					) -> BaseException:
 | 
				
			||||||
) -> BaseException|None:
 | 
					 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    If the input beg can collapse to a single sub-exception which is
 | 
					    If the input beg can collapse to a single non-eg sub-exception,
 | 
				
			||||||
    itself **not** an eg, return it.
 | 
					    return it instead.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    maybe_exc = collapse_exception_group(beg)
 | 
					    if len(excs := beg.exceptions) == 1:
 | 
				
			||||||
    if maybe_exc is beg:
 | 
					        return excs[0]
 | 
				
			||||||
        return None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return maybe_exc
 | 
					    return beg
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@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
 | 
				
			||||||
| 
						 | 
					@ -110,114 +47,12 @@ 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 BaseExceptionGroup as _beg:
 | 
					    except* BaseException as beg:
 | 
				
			||||||
        beg = _beg
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            bp
 | 
					            exc := maybe_collapse_eg(beg)
 | 
				
			||||||
            and
 | 
					        ) is not beg:
 | 
				
			||||||
            len(beg.exceptions) > 1
 | 
					            raise exc
 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            import tractor
 | 
					 | 
				
			||||||
            if tractor.current_actor(
 | 
					 | 
				
			||||||
                err_on_no_runtime=False,
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                await tractor.pause(shield=True)
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                breakpoint()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (
 | 
					        raise beg
 | 
				
			||||||
            (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
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,6 +31,7 @@ from typing import (
 | 
				
			||||||
    AsyncIterator,
 | 
					    AsyncIterator,
 | 
				
			||||||
    Callable,
 | 
					    Callable,
 | 
				
			||||||
    Hashable,
 | 
					    Hashable,
 | 
				
			||||||
 | 
					    Optional,
 | 
				
			||||||
    Sequence,
 | 
					    Sequence,
 | 
				
			||||||
    TypeVar,
 | 
					    TypeVar,
 | 
				
			||||||
    TYPE_CHECKING,
 | 
					    TYPE_CHECKING,
 | 
				
			||||||
| 
						 | 
					@ -39,11 +40,6 @@ 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 ._beg import collapse_eg
 | 
					 | 
				
			||||||
# from ._taskc import (
 | 
					 | 
				
			||||||
#     maybe_raise_from_masking_exc,
 | 
					 | 
				
			||||||
# )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
    from tractor import ActorNursery
 | 
					    from tractor import ActorNursery
 | 
				
			||||||
| 
						 | 
					@ -108,9 +104,6 @@ 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,
 | 
				
			||||||
| 
						 | 
					@ -119,19 +112,17 @@ async def gather_contexts(
 | 
				
			||||||
    None,
 | 
					    None,
 | 
				
			||||||
]:
 | 
					]:
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Concurrently enter a sequence of async context managers (`acm`s),
 | 
					    Concurrently enter a sequence of async context managers (acms),
 | 
				
			||||||
    each scheduled in a separate `trio.Task` and deliver their
 | 
					    each from a separate `trio` task and deliver the unwrapped
 | 
				
			||||||
    unwrapped `yield`-ed values in the same order once all `@acm`s
 | 
					    `yield`-ed values in the same order once all managers have entered.
 | 
				
			||||||
    in every task have entered.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    On exit, all `acm`s are subsequently and concurrently exited with
 | 
					    On exit, all acms are subsequently and concurrently exited.
 | 
				
			||||||
    **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™*.
 | 
					    concurrently entered and exited and *cancellation just works*(R).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    seed: int = id(mngrs)
 | 
					    seed: int = id(mngrs)
 | 
				
			||||||
| 
						 | 
					@ -151,47 +142,37 @@ 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'
 | 
				
			||||||
            'Check that list({mngrs}) works!\n'
 | 
					            'Use a non-lazy iterator or sequence type intead!'
 | 
				
			||||||
            # 'or sequence-type intead!\n'
 | 
					 | 
				
			||||||
            # 'Use a non-lazy iterator or sequence-type intead!\n'
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    async with trio.open_nursery(
 | 
				
			||||||
        async with (
 | 
					        strict_exception_groups=False,
 | 
				
			||||||
            #
 | 
					        # ^XXX^ TODO? soo roll our own then ??
 | 
				
			||||||
            # ?TODO, does including these (eg-collapsing,
 | 
					        # -> since we kinda want the "if only one `.exception` then
 | 
				
			||||||
            # taskc-unmasking) improve tb noise-reduction/legibility?
 | 
					        # just raise that" interface?
 | 
				
			||||||
            #
 | 
					    ) as tn:
 | 
				
			||||||
            # collapse_eg(),
 | 
					        for mngr in mngrs:
 | 
				
			||||||
            maybe_open_nursery(
 | 
					            tn.start_soon(
 | 
				
			||||||
                nursery=tn,
 | 
					                _enter_and_wait,
 | 
				
			||||||
            ) as tn,
 | 
					                mngr,
 | 
				
			||||||
            # maybe_raise_from_masking_exc(),
 | 
					                unwrapped,
 | 
				
			||||||
        ):
 | 
					                all_entered,
 | 
				
			||||||
            for mngr in mngrs:
 | 
					                parent_exit,
 | 
				
			||||||
                tn.start_soon(
 | 
					                seed,
 | 
				
			||||||
                    _enter_and_wait,
 | 
					            )
 | 
				
			||||||
                    mngr,
 | 
					 | 
				
			||||||
                    unwrapped,
 | 
					 | 
				
			||||||
                    all_entered,
 | 
					 | 
				
			||||||
                    parent_exit,
 | 
					 | 
				
			||||||
                    seed,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # deliver control to caller once all ctx-managers have
 | 
					        # deliver control once all managers have started up
 | 
				
			||||||
            # started (yielded back to us).
 | 
					        await all_entered.wait()
 | 
				
			||||||
            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:
 | 
				
			||||||
| 
						 | 
					@ -203,7 +184,7 @@ class _Cache:
 | 
				
			||||||
    a kept-alive-while-in-use async resource.
 | 
					    a kept-alive-while-in-use async resource.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    service_tn: trio.Nursery|None = None
 | 
					    service_n: 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] = {}
 | 
				
			||||||
| 
						 | 
					@ -212,7 +193,7 @@ class _Cache:
 | 
				
			||||||
        tuple[trio.Nursery, trio.Event]
 | 
					        tuple[trio.Nursery, trio.Event]
 | 
				
			||||||
    ] = {}
 | 
					    ] = {}
 | 
				
			||||||
    # nurseries: dict[int, trio.Nursery] = {}
 | 
					    # nurseries: dict[int, trio.Nursery] = {}
 | 
				
			||||||
    no_more_users: trio.Event|None = None
 | 
					    no_more_users: Optional[trio.Event] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    async def run_ctx(
 | 
					    async def run_ctx(
 | 
				
			||||||
| 
						 | 
					@ -222,18 +203,16 @@ class _Cache:
 | 
				
			||||||
        task_status: trio.TaskStatus[T] = trio.TASK_STATUS_IGNORED,
 | 
					        task_status: trio.TaskStatus[T] = trio.TASK_STATUS_IGNORED,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        try:
 | 
					        async with mng as value:
 | 
				
			||||||
            async with mng as value:
 | 
					            _, no_more_users = cls.resources[ctx_key]
 | 
				
			||||||
                _, no_more_users = cls.resources[ctx_key]
 | 
					            cls.values[ctx_key] = value
 | 
				
			||||||
                try:
 | 
					            task_status.started(value)
 | 
				
			||||||
                    cls.values[ctx_key] = value
 | 
					            try:
 | 
				
			||||||
                    task_status.started(value)
 | 
					                await no_more_users.wait()
 | 
				
			||||||
                    await no_more_users.wait()
 | 
					            finally:
 | 
				
			||||||
                finally:
 | 
					                # discard nursery ref so it won't be re-used (an error)?
 | 
				
			||||||
                    value = cls.values.pop(ctx_key)
 | 
					                value = cls.values.pop(ctx_key)
 | 
				
			||||||
        finally:
 | 
					                cls.resources.pop(ctx_key)
 | 
				
			||||||
            # discard nursery ref so it won't be re-used (an error)?
 | 
					 | 
				
			||||||
            cls.resources.pop(ctx_key)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@acm
 | 
					@acm
 | 
				
			||||||
| 
						 | 
					@ -246,9 +225,6 @@ 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
 | 
				
			||||||
| 
						 | 
					@ -281,94 +257,40 @@ 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..)
 | 
				
			||||||
    if tn:
 | 
					    service_n: trio.Nursery = current_actor()._service_n
 | 
				
			||||||
        # 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_tn: trio.Nursery
 | 
					    # service_n: trio.Nursery
 | 
				
			||||||
    # async with maybe_open_nursery(_Cache.service_tn) as service_tn:
 | 
					    # async with maybe_open_nursery(_Cache.service_n) as service_n:
 | 
				
			||||||
    #     _Cache.service_tn = service_tn
 | 
					    #     _Cache.service_n = service_n
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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 as _ke:
 | 
					    except KeyError:
 | 
				
			||||||
        # XXX, stay mutexed up to cache-miss yield
 | 
					        log.debug(f'Allocating new {acm_func} for {ctx_key}')
 | 
				
			||||||
        try:
 | 
					        mngr = acm_func(**kwargs)
 | 
				
			||||||
            cache_miss_ke = _ke
 | 
					        resources = _Cache.resources
 | 
				
			||||||
            log.debug(
 | 
					        assert not resources.get(ctx_key), f'Resource exists? {ctx_key}'
 | 
				
			||||||
                f'Allocating new @acm-func entry\n'
 | 
					        resources[ctx_key] = (service_n, trio.Event())
 | 
				
			||||||
                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()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        # sync up to the mngr's yielded value
 | 
				
			||||||
            yield (
 | 
					        yielded = await service_n.start(
 | 
				
			||||||
                False,  # cache_hit = "no"
 | 
					            _Cache.run_ctx,
 | 
				
			||||||
                yielded,
 | 
					            mngr,
 | 
				
			||||||
            )
 | 
					            ctx_key,
 | 
				
			||||||
        except trio.Cancelled as taskc:
 | 
					        )
 | 
				
			||||||
            maybe_taskc = taskc
 | 
					        _Cache.users += 1
 | 
				
			||||||
            log.cancel(
 | 
					        lock.release()
 | 
				
			||||||
                f'Cancelled from cache-miss entry\n'
 | 
					        yield False, yielded
 | 
				
			||||||
                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.debug(
 | 
					        log.runtime(
 | 
				
			||||||
            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'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -378,19 +300,9 @@ async def maybe_open_context(
 | 
				
			||||||
            # f'{ctx_key!r} -> {yielded!r}\n'
 | 
					            # f'{ctx_key!r} -> {yielded!r}\n'
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        lock.release()
 | 
					        lock.release()
 | 
				
			||||||
        yield (
 | 
					        yield True, yielded
 | 
				
			||||||
            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:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,293 +0,0 @@
 | 
				
			||||||
# 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,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
import inspect
 | 
					 | 
				
			||||||
from types import (
 | 
					 | 
				
			||||||
    TracebackType,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from typing import (
 | 
					 | 
				
			||||||
    Type,
 | 
					 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
_mask_cases: dict[
 | 
					 | 
				
			||||||
    Type[Exception],  # masked exc type
 | 
					 | 
				
			||||||
    dict[
 | 
					 | 
				
			||||||
        int,  # inner-frame index into `inspect.getinnerframes()`
 | 
					 | 
				
			||||||
        # `FrameInfo.function/filename: str`s to match
 | 
					 | 
				
			||||||
        dict[str, str],
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
] = {
 | 
					 | 
				
			||||||
    trio.WouldBlock: {
 | 
					 | 
				
			||||||
        # `trio.Lock.acquire()` has a checkpoint inside the
 | 
					 | 
				
			||||||
        # `WouldBlock`-no_wait path's handler..
 | 
					 | 
				
			||||||
        -5: {  # "5th frame up" from checkpoint
 | 
					 | 
				
			||||||
            'filename': 'trio/_sync.py',
 | 
					 | 
				
			||||||
            'function': 'acquire',
 | 
					 | 
				
			||||||
            # 'lineno': 605,  # matters?
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def is_expected_masking_case(
 | 
					 | 
				
			||||||
    cases: dict,
 | 
					 | 
				
			||||||
    exc_ctx: Exception,
 | 
					 | 
				
			||||||
    exc_match: BaseException,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
) -> bool|inspect.FrameInfo:
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    Determine whether the provided masked exception is from a known
 | 
					 | 
				
			||||||
    bug/special/unintentional-`trio`-impl case which we do not wish
 | 
					 | 
				
			||||||
    to unmask.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Return any guilty `inspect.FrameInfo` ow `False`.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    exc_tb: TracebackType = exc_match.__traceback__
 | 
					 | 
				
			||||||
    if cases := _mask_cases.get(type(exc_ctx)):
 | 
					 | 
				
			||||||
        inner: list[inspect.FrameInfo] = inspect.getinnerframes(exc_tb)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # from tractor.devx.debug import mk_pdb
 | 
					 | 
				
			||||||
        # mk_pdb().set_trace()
 | 
					 | 
				
			||||||
        for iframe, matchon in cases.items():
 | 
					 | 
				
			||||||
            try:
 | 
					 | 
				
			||||||
                masker_frame: inspect.FrameInfo = inner[iframe]
 | 
					 | 
				
			||||||
            except IndexError:
 | 
					 | 
				
			||||||
                continue
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            for field, in_field in matchon.items():
 | 
					 | 
				
			||||||
                val = getattr(
 | 
					 | 
				
			||||||
                    masker_frame,
 | 
					 | 
				
			||||||
                    field,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                if in_field not in val:
 | 
					 | 
				
			||||||
                    break
 | 
					 | 
				
			||||||
            else:
 | 
					 | 
				
			||||||
                return masker_frame
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# XXX, relevant discussion @ `trio`-core,
 | 
					 | 
				
			||||||
# https://github.com/python-trio/trio/issues/455
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
@acm
 | 
					 | 
				
			||||||
async def maybe_raise_from_masking_exc(
 | 
					 | 
				
			||||||
    unmask_from: (
 | 
					 | 
				
			||||||
        BaseException|
 | 
					 | 
				
			||||||
        tuple[BaseException]
 | 
					 | 
				
			||||||
    ) = (trio.Cancelled,),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    raise_unmasked: bool = True,
 | 
					 | 
				
			||||||
    extra_note: str = (
 | 
					 | 
				
			||||||
        'This can occurr when,\n'
 | 
					 | 
				
			||||||
        '\n'
 | 
					 | 
				
			||||||
        ' - a `trio.Nursery/CancelScope` embeds a `finally/except:`-block '
 | 
					 | 
				
			||||||
        'which execs an un-shielded checkpoint!'
 | 
					 | 
				
			||||||
        #
 | 
					 | 
				
			||||||
        # ^TODO? other cases?
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    always_warn_on: tuple[Type[BaseException]] = (
 | 
					 | 
				
			||||||
        trio.Cancelled,
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # don't ever unmask or warn on any masking pair,
 | 
					 | 
				
			||||||
    # {<masked-excT-key> -> <masking-excT-value>}
 | 
					 | 
				
			||||||
    never_warn_on: dict[
 | 
					 | 
				
			||||||
        Type[BaseException],
 | 
					 | 
				
			||||||
        Type[BaseException],
 | 
					 | 
				
			||||||
    ] = {
 | 
					 | 
				
			||||||
        KeyboardInterrupt: trio.Cancelled,
 | 
					 | 
				
			||||||
        trio.Cancelled: 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?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    '''
 | 
					 | 
				
			||||||
    if not isinstance(unmask_from, tuple):
 | 
					 | 
				
			||||||
        raise ValueError(
 | 
					 | 
				
			||||||
            f'Invalid unmask_from = {unmask_from!r}\n'
 | 
					 | 
				
			||||||
            f'Must be a `tuple[Type[BaseException]]`.\n'
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    from tractor.devx.debug import (
 | 
					 | 
				
			||||||
        BoxedMaybeException,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    boxed_maybe_exc = BoxedMaybeException(
 | 
					 | 
				
			||||||
        raise_on_exit=raise_unmasked,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    matching: list[BaseException]|None = None
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        yield boxed_maybe_exc
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
    except BaseException as _bexc:
 | 
					 | 
				
			||||||
        bexc = _bexc
 | 
					 | 
				
			||||||
        if isinstance(bexc, BaseExceptionGroup):
 | 
					 | 
				
			||||||
            matches: ExceptionGroup
 | 
					 | 
				
			||||||
            matches, _ = bexc.split(unmask_from)
 | 
					 | 
				
			||||||
            if matches:
 | 
					 | 
				
			||||||
                matching = matches.exceptions
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        elif (
 | 
					 | 
				
			||||||
            unmask_from
 | 
					 | 
				
			||||||
            and
 | 
					 | 
				
			||||||
            type(bexc) in unmask_from
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            matching = [bexc]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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=set(unmask_from),
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            masked.append((
 | 
					 | 
				
			||||||
                exc_ctx,
 | 
					 | 
				
			||||||
                exc_match,
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
            boxed_maybe_exc.value = exc_match
 | 
					 | 
				
			||||||
            note: str = (
 | 
					 | 
				
			||||||
                f'\n'
 | 
					 | 
				
			||||||
                f'^^WARNING^^\n'
 | 
					 | 
				
			||||||
                f'the above {type(exc_ctx)!r} was masked by a {type(exc_match)!r}\n'
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            if extra_note:
 | 
					 | 
				
			||||||
                note += (
 | 
					 | 
				
			||||||
                    f'\n'
 | 
					 | 
				
			||||||
                    f'{extra_note}\n'
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            do_warn: bool = (
 | 
					 | 
				
			||||||
                never_warn_on.get(
 | 
					 | 
				
			||||||
                    type(exc_ctx)  # masking type
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                is not
 | 
					 | 
				
			||||||
                type(exc_match)  # masked type
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if do_warn:
 | 
					 | 
				
			||||||
                exc_ctx.add_note(note)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
                do_warn
 | 
					 | 
				
			||||||
                and
 | 
					 | 
				
			||||||
                type(exc_match) in always_warn_on
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                log.warning(note)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
                do_warn
 | 
					 | 
				
			||||||
                and
 | 
					 | 
				
			||||||
                raise_unmasked
 | 
					 | 
				
			||||||
            ):
 | 
					 | 
				
			||||||
                if len(masked) < 2:
 | 
					 | 
				
			||||||
                    # don't unmask already known "special" cases..
 | 
					 | 
				
			||||||
                    if (
 | 
					 | 
				
			||||||
                        _mask_cases
 | 
					 | 
				
			||||||
                        and
 | 
					 | 
				
			||||||
                        (cases := _mask_cases.get(type(exc_ctx)))
 | 
					 | 
				
			||||||
                        and
 | 
					 | 
				
			||||||
                        (masker_frame := is_expected_masking_case(
 | 
					 | 
				
			||||||
                            cases,
 | 
					 | 
				
			||||||
                            exc_ctx,
 | 
					 | 
				
			||||||
                            exc_match,
 | 
					 | 
				
			||||||
                        ))
 | 
					 | 
				
			||||||
                    ):
 | 
					 | 
				
			||||||
                        log.warning(
 | 
					 | 
				
			||||||
                            f'Ignoring already-known, non-ideal-but-valid '
 | 
					 | 
				
			||||||
                            f'masker code @\n'
 | 
					 | 
				
			||||||
                            f'{masker_frame}\n'
 | 
					 | 
				
			||||||
                            f'\n'
 | 
					 | 
				
			||||||
                            f'NOT raising {exc_ctx} from masker {exc_match!r}\n'
 | 
					 | 
				
			||||||
                        )
 | 
					 | 
				
			||||||
                        raise exc_match
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    raise exc_ctx from exc_match
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                # ??TODO, see above but, possibly unmasking sub-exc
 | 
					 | 
				
			||||||
                # entries if there are > 1
 | 
					 | 
				
			||||||
                # else:
 | 
					 | 
				
			||||||
                #     await pause(shield=True)
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        raise
 | 
					 | 
				
			||||||
							
								
								
									
										419
									
								
								uv.lock
								
								
								
								
							
							
						
						
									
										419
									
								
								uv.lock
								
								
								
								
							| 
						 | 
					@ -1,23 +1,23 @@
 | 
				
			||||||
version = 1
 | 
					version = 1
 | 
				
			||||||
revision = 2
 | 
					revision = 1
 | 
				
			||||||
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, upload-time = "2024-12-16T06:59:29.899Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/48/c8/6260f8ccc11f0917360fc0da435c5c9c7504e3db174d5a12a1494887b045/attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff", size = 805984 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/89/aa/ab0f7891a01eeb2d2e338ae8fecbe57fcebea1a24dbb64d45801bfab481d/attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308", size = 63397 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-02-18T19:09:05.748Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-09-04T20:45:21.852Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
 | 
				
			||||||
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, upload-time = "2024-09-04T20:43:51.124Z" },
 | 
					    { 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
 | 
					    { 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" },
 | 
					    { 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2022-10-25T02:36:22.414Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
 | 
				
			||||||
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, upload-time = "2022-10-25T02:36:20.889Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-10-29T18:34:51.011Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-02-20T21:23:13.239Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/dc/c1/ab3a42c0f3ed56df9cd33de1539b3198d98c6ccbaf88a73d6be0b72d85e0/greenback-1.2.1.tar.gz", hash = "sha256:de3ca656885c03b96dab36079f3de74bb5ba061da9bfe3bb69dccc866ef95ea3", size = 42597 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/71/d0/b8dc79d5ecfffacad9c844b6ae76b9c6259935796d3c561deccbf8fa421d/greenback-1.2.1-py3-none-any.whl", hash = "sha256:98768edbbe4340091a9730cf64a683fcbaa3f2cb81e4ac41d7ed28d3b6f74b79", size = 28062 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-09-20T18:21:04.506Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 }
 | 
				
			||||||
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, upload-time = "2024-09-20T17:07:22.332Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035, upload-time = "2024-09-20T17:44:26.501Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105, upload-time = "2024-09-20T17:08:42.048Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077, upload-time = "2024-09-20T17:08:33.707Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975, upload-time = "2024-09-20T17:44:15.989Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955, upload-time = "2024-09-20T17:09:25.539Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655, upload-time = "2024-09-20T17:21:22.427Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990, upload-time = "2024-09-20T17:08:26.312Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175, upload-time = "2024-09-20T17:36:48.983Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425, upload-time = "2024-09-20T17:39:22.705Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736, upload-time = "2024-09-20T17:44:28.544Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347, upload-time = "2024-09-20T17:08:45.56Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583, upload-time = "2024-09-20T17:08:36.85Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039, upload-time = "2024-09-20T17:44:18.287Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716, upload-time = "2024-09-20T17:09:27.112Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490, upload-time = "2024-09-20T17:17:09.501Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731, upload-time = "2024-09-20T17:36:50.376Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304, upload-time = "2024-09-20T17:39:24.55Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537, upload-time = "2024-09-20T17:44:31.102Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506, upload-time = "2024-09-20T17:08:47.852Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753, upload-time = "2024-09-20T17:08:38.079Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731, upload-time = "2024-09-20T17:44:20.556Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112, upload-time = "2024-09-20T17:09:28.753Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[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, upload-time = "2024-09-15T18:07:39.745Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2023-01-07T11:08:11.254Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-12-27T17:40:28.597Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934 }
 | 
				
			||||||
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, upload-time = "2024-12-27T17:39:32.347Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202, upload-time = "2024-12-27T17:39:33.633Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029, upload-time = "2024-12-27T17:39:35.023Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682, upload-time = "2024-12-27T17:39:36.384Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003, upload-time = "2024-12-27T17:39:39.097Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833, upload-time = "2024-12-27T17:39:41.203Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184, upload-time = "2024-12-27T17:39:43.702Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594, upload-time = "2024-12-27T17:39:51.204Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053, upload-time = "2024-12-27T17:39:52.866Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081, upload-time = "2024-12-27T17:39:55.142Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467, upload-time = "2024-12-27T17:39:56.531Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498, upload-time = "2024-12-27T17:40:00.427Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950, upload-time = "2024-12-27T17:40:04.219Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647, upload-time = "2024-12-27T17:40:05.606Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563, upload-time = "2024-12-27T17:40:10.516Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996, upload-time = "2024-12-27T17:40:12.244Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087, upload-time = "2024-12-27T17:40:14.881Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432, upload-time = "2024-12-27T17:40:16.256Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2023-10-26T04:26:04.361Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 }
 | 
				
			||||||
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, upload-time = "2023-10-26T04:26:02.532Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[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, upload-time = "2024-11-08T09:47:47.202Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-11-07T15:36:43.062Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/69/13/80da03638f62facbee76312ca9ee5941c017b080f2e4c6919fd4e87e16e3/pdbp-1.6.1.tar.gz", hash = "sha256:f4041642952a05df89664e166d5bd379607a0866ddd753c06874f65552bdf40b", size = 25322 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/29/93/d56fb9ba5569dc29d8263c72e46d21a2fd38741339ebf03f54cf7561828c/pdbp-1.6.1-py3-none-any.whl", hash = "sha256:f10bad2ee044c0e5c168cb0825abfdbdc01c50013e9755df5261b060bdd35c22", size = 21495 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2023-11-25T09:07:26.339Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450 }
 | 
				
			||||||
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, upload-time = "2023-11-25T06:56:14.81Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-04-20T21:34:42.531Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2025-01-20T15:55:35.072Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/a1/e1/bd15cb8ffdcfeeb2bdc215de3c3cffca11408d829e4b8416dcfe71ba8854/prompt_toolkit-3.0.50.tar.gz", hash = "sha256:544748f3860a2623ca5cd6d2795e7a14f3d0e1c3c9728359013f79877fc89bab", size = 429087 }
 | 
				
			||||||
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, upload-time = "2025-01-20T15:55:29.98Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/e4/ea/d836f008d33151c7a1f62caf3d8dd782e4d15f6a43897f64480c2b8de2ad/prompt_toolkit-3.0.50-py3-none-any.whl", hash = "sha256:9b6427eb19e479d98acff65196a307c555eb567989e6d88ebbb1b509d9779198", size = 387816 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2025-02-13T21:54:07.946Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003 }
 | 
				
			||||||
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, upload-time = "2025-02-13T21:54:12.36Z" },
 | 
					    { 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/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/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/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/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/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/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/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/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/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/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053 },
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2020-12-28T15:15:30.155Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762 }
 | 
				
			||||||
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, upload-time = "2020-12-28T15:15:28.35Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-03-30T13:22:22.564Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2025-01-06T17:26:30.443Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-06-18T20:38:48.401Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/30/23/2f0a3efc4d6a32f3b63cdff36cd398d9701d26cda58e3ab97ac79fb5e60d/pyperclip-1.9.0.tar.gz", hash = "sha256:b7de0142ddc81bfc5c7507eea19da920b92252b548b96186caf94a5e2527d310", size = 20961 }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-09-19T02:40:10.062Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2025-03-02T12:54:54.503Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-02-25T23:20:04.057Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2021-05-16T22:03:42.897Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
 | 
				
			||||||
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, upload-time = "2021-05-16T22:03:41.177Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-02-27T22:02:15.831Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/4a/fc/20dbb993353f31230138f3c63f3f0c881d1853e70d7a30cd68d2ba4cf1e2/stackscope-0.2.2.tar.gz", hash = "sha256:f508c93eb4861ada466dd3ff613ca203962ceb7587ad013759f15394e6a4e619", size = 90479 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/f1/5f/0a674fcafa03528089badb46419413f342537b5b57d2fefc9900fb8ee4e4/stackscope-0.2.2-py3-none-any.whl", hash = "sha256:c199b0cda738d39c993ee04eb01961b06b7e9aeb43ebf9fd6226cdd72ea9faf6", size = 80807 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2024-10-28T00:44:52.665Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/73/1a/ed3544579628c5709bae6fae2255e94c6982a9ff77d42d8ba59fd2f3b21a/tabcompleter-1.4.0.tar.gz", hash = "sha256:7562a9938e62f8e7c3be612c3ac4e14c5ec4307b58ba9031c148260e866e8814", size = 10431 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/65/44/bb509c3d2c0b5a87e7a5af1d5917a402a32ff026f777a6d7cb6990746cbb/tabcompleter-1.4.0-py3-none-any.whl", hash = "sha256:d744aa735b49c0a6cc2fb8fcd40077fec47425e4388301010b14e6ce3311368b", size = 6725 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
| 
						 | 
					@ -392,7 +392,6 @@ dev = [
 | 
				
			||||||
    { name = "pyperclip" },
 | 
					    { name = "pyperclip" },
 | 
				
			||||||
    { name = "pytest" },
 | 
					    { name = "pytest" },
 | 
				
			||||||
    { name = "stackscope" },
 | 
					    { name = "stackscope" },
 | 
				
			||||||
    { name = "typing-extensions" },
 | 
					 | 
				
			||||||
    { name = "xonsh" },
 | 
					    { name = "xonsh" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -417,7 +416,6 @@ 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -428,9 +426,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, upload-time = "2024-02-02T20:41:15.298Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/f8/8e/fdd7bc467b40eedd0a5f2ed36b0d692c6e6f2473be00c8160e2e9f53adc1/tricycle-0.4.1.tar.gz", hash = "sha256:f56edb4b3e1bed3e2552b1b499b24a2dab47741e92e9b4d806acc5c35c9e6066", size = 41551 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/d7/c6/7cc05d60e21c683df99167db071ce5d848f5063c2a63971a8443466f603e/tricycle-0.4.1-py3-none-any.whl", hash = "sha256:67900995a73e7445e2c70250cdca04a778d9c3923dd960a97ad4569085e0fb3f", size = 35316 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
| 
						 | 
					@ -445,91 +443,82 @@ 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, upload-time = "2025-02-14T07:13:50.724Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/a1/47/f62e62a1a6f37909aed0bf8f5d5411e06fa03846cfcb64540cd1180ccc9f/trio-0.29.0.tar.gz", hash = "sha256:ea0d3967159fc130acb6939a0be0e558e364fee26b5deeecc893a6b08c361bdf", size = 588952 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { url = "https://files.pythonhosted.org/packages/c9/55/c4d9bea8b3d7937901958f65124123512419ab0eb73695e5f382521abbfb/trio-0.29.0-py3-none-any.whl", hash = "sha256:d8c463f1a9cc776ff63e331aba44c125f423a5a13c684307e828d930e625ba66", size = 492920 },
 | 
				
			||||||
]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[[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, upload-time = "2024-01-06T02:10:57.829Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301 }
 | 
				
			||||||
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, upload-time = "2024-01-06T02:10:55.763Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2025-01-14T10:35:45.465Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 }
 | 
				
			||||||
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, upload-time = "2025-01-14T10:33:33.992Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721, upload-time = "2025-01-14T10:34:07.163Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899, upload-time = "2025-01-14T10:34:09.82Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222, upload-time = "2025-01-14T10:34:11.258Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707, upload-time = "2025-01-14T10:34:12.49Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685, upload-time = "2025-01-14T10:34:15.043Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567, upload-time = "2025-01-14T10:34:16.563Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672, upload-time = "2025-01-14T10:34:17.727Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/17/27/4fe749a54e7fae6e7146f1c7d914d28ef599dacd4416566c055564080fe2/wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9", size = 36672 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865, upload-time = "2025-01-14T10:34:19.577Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/15/06/1dbf478ea45c03e78a6a8c4be4fdc3c3bddea5c8de8a93bc971415e47f0f/wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991", size = 38865 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800, upload-time = "2025-01-14T10:34:21.571Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/ce/b9/0ffd557a92f3b11d4c5d5e0c5e4ad057bd9eb8586615cdaf901409920b14/wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125", size = 53800 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824, upload-time = "2025-01-14T10:34:22.999Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/c0/ef/8be90a0b7e73c32e550c73cfb2fa09db62234227ece47b0e80a05073b375/wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998", size = 38824 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920, upload-time = "2025-01-14T10:34:25.386Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/36/89/0aae34c10fe524cce30fe5fc433210376bce94cf74d05b0d68344c8ba46e/wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5", size = 38920 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690, upload-time = "2025-01-14T10:34:28.058Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/3b/24/11c4510de906d77e0cfb5197f1b1445d4fec42c9a39ea853d482698ac681/wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8", size = 88690 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861, upload-time = "2025-01-14T10:34:29.167Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/71/d7/cfcf842291267bf455b3e266c0c29dcb675b5540ee8b50ba1699abf3af45/wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6", size = 80861 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174, upload-time = "2025-01-14T10:34:31.702Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/d5/66/5d973e9f3e7370fd686fb47a9af3319418ed925c27d72ce16b791231576d/wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc", size = 89174 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721, upload-time = "2025-01-14T10:34:32.91Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/a7/d3/8e17bb70f6ae25dabc1aaf990f86824e4fd98ee9cadf197054e068500d27/wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2", size = 86721 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763, upload-time = "2025-01-14T10:34:34.903Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/6f/54/f170dfb278fe1c30d0ff864513cff526d624ab8de3254b20abb9cffedc24/wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b", size = 79763 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585, upload-time = "2025-01-14T10:34:36.13Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/4a/98/de07243751f1c4a9b15c76019250210dd3486ce098c3d80d5f729cba029c/wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504", size = 87585 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676, upload-time = "2025-01-14T10:34:37.962Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/f9/f0/13925f4bd6548013038cdeb11ee2cbd4e37c30f8bfd5db9e5a2a370d6e20/wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a", size = 36676 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871, upload-time = "2025-01-14T10:34:39.13Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/bf/ae/743f16ef8c2e3628df3ddfd652b7d4c555d12c84b53f3d8218498f4ade9b/wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845", size = 38871 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312, upload-time = "2025-01-14T10:34:40.604Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/3d/bc/30f903f891a82d402ffb5fda27ec1d621cc97cb74c16fea0b6141f1d4e87/wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192", size = 56312 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062, upload-time = "2025-01-14T10:34:45.011Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/8a/04/c97273eb491b5f1c918857cd26f314b74fc9b29224521f5b83f872253725/wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b", size = 40062 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155, upload-time = "2025-01-14T10:34:47.25Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/4e/ca/3b7afa1eae3a9e7fefe499db9b96813f41828b9fdb016ee836c4c379dadb/wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0", size = 40155 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471, upload-time = "2025-01-14T10:34:50.934Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/89/be/7c1baed43290775cb9030c774bc53c860db140397047cc49aedaf0a15477/wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306", size = 113471 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208, upload-time = "2025-01-14T10:34:52.297Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/32/98/4ed894cf012b6d6aae5f5cc974006bdeb92f0241775addad3f8cd6ab71c8/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb", size = 101208 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339, upload-time = "2025-01-14T10:34:53.489Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/ea/fd/0c30f2301ca94e655e5e057012e83284ce8c545df7661a78d8bfca2fac7a/wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681", size = 109339 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232, upload-time = "2025-01-14T10:34:55.327Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/75/56/05d000de894c4cfcb84bcd6b1df6214297b8089a7bd324c21a4765e49b14/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6", size = 110232 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476, upload-time = "2025-01-14T10:34:58.055Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/53/f8/c3f6b2cf9b9277fb0813418e1503e68414cd036b3b099c823379c9575e6d/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6", size = 100476 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377, upload-time = "2025-01-14T10:34:59.3Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/a7/b1/0bb11e29aa5139d90b770ebbfa167267b1fc548d2302c30c8f7572851738/wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f", size = 106377 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986, upload-time = "2025-01-14T10:35:00.498Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/6a/e1/0122853035b40b3f333bbb25f1939fc1045e21dd518f7f0922b60c156f7c/wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555", size = 37986 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750, upload-time = "2025-01-14T10:35:03.378Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/09/5e/1655cf481e079c1f22d0cabdd4e51733679932718dc23bf2db175f329b76/wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c", size = 40750 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594, upload-time = "2025-01-14T10:35:44.018Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[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, upload-time = "2025-02-11T17:10:43.563Z" }
 | 
					sdist = { url = "https://files.pythonhosted.org/packages/68/4e/56e95a5e607eb3b0da37396f87cde70588efc8ef819ab16f02d5b8378dc4/xonsh-0.19.2.tar.gz", hash = "sha256:cfdd0680d954a2c3aefd6caddcc7143a3d06aa417ed18365a08219bb71b960b0", size = 799960 }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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/6c/13/281094759df87b23b3c02dc4a16603ab08ea54d7f6acfeb69f3341137c7a/xonsh-0.19.2-py310-none-any.whl", hash = "sha256:ec7f163fd3a4943782aa34069d4e72793328c916a5975949dbec8536cbfc089b", size = 642301 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286, upload-time = "2025-02-11T17:10:41.678Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386, upload-time = "2025-02-11T17:10:43.688Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873, upload-time = "2025-02-11T17:10:39.297Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873 },
 | 
				
			||||||
    { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602, upload-time = "2025-02-11T17:10:37.004Z" },
 | 
					    { url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602 },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue