pax_global_header00006660000000000000000000000064146247266540014532gustar00rootroot0000000000000052 comment=053e8f0a0f7b0f4a47a012eb5c6b1d9d84344e6a anyio-4.4.0/000077500000000000000000000000001462472665400126565ustar00rootroot00000000000000anyio-4.4.0/.github/000077500000000000000000000000001462472665400142165ustar00rootroot00000000000000anyio-4.4.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001462472665400164015ustar00rootroot00000000000000anyio-4.4.0/.github/ISSUE_TEMPLATE/bug_report.yaml000066400000000000000000000032321462472665400214350ustar00rootroot00000000000000name: Bug Report description: File a bug report labels: ["bug"] body: - type: markdown attributes: value: > If you observed a crash in the library, or saw unexpected behavior in it, report your findings here. - type: checkboxes attributes: label: Things to check first options: - label: > I have searched the existing issues and didn't find my bug already reported there required: true - label: > I have checked that my bug is still present in the latest release required: true - type: input id: anyio-version attributes: label: AnyIO version description: What version of AnyIO were you running? validations: required: true - type: input id: python-version attributes: label: Python version description: What version of Python were you running? validations: required: true - type: textarea id: what-happened attributes: label: What happened? description: > Unless you are reporting a crash, tell us what you expected to happen instead. validations: required: true - type: textarea id: mwe attributes: label: How can we reproduce the bug? description: > In order to investigate the bug, we need to be able to reproduce it on our own. Please create a [minimum workable example](https://stackoverflow.com/help/minimal-reproducible-example) that demonstrates the problem. List any third party libraries required for this, but avoid using them unless absolutely necessary. validations: required: true anyio-4.4.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000004421462472665400203710ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: GitHub Discussions url: https://github.com/agronholm/anyio/discussions/categories/q-a about: Technical support forum - name: Support chat on Gitter url: https://gitter.im/python-trio/AnyIO about: Technical support chat anyio-4.4.0/.github/ISSUE_TEMPLATE/features_request.yaml000066400000000000000000000020341462472665400226520ustar00rootroot00000000000000name: Feature request description: Suggest a new feature labels: ["enhancement"] body: - type: markdown attributes: value: > If you have thought of a new feature that would increase the usefulness of this project, please use this form to send us your idea. - type: checkboxes attributes: label: Things to check first options: - label: > I have searched the existing issues and didn't find my feature already requested there required: true - type: textarea id: feature attributes: label: Feature description description: > Describe the feature in detail. The more specific the description you can give, the easier it should be to implement this feature. validations: required: true - type: textarea id: usecase attributes: label: Use case description: > Explain why you need this feature, and why you think it would be useful to others too. validations: required: true anyio-4.4.0/.github/pull_request_template.md000066400000000000000000000021361462472665400211610ustar00rootroot00000000000000 ## Changes Fixes #. ## Checklist If this is a user-facing code change, like a bugfix or a new feature, please ensure that you've fulfilled the following conditions (where applicable): - [ ] You've added tests (in `tests/`) added which would fail without your patch - [ ] You've updated the documentation (in `docs/`, in case of behavior changes or new features) - [ ] You've added a new changelog entry (in `docs/versionhistory.rst`). If this is a trivial change, like a typo fix or a code reformatting, then you can ignore these instructions. ### Updating the changelog If there are no entries after the last release, use `**UNRELEASED**` as the version. If, say, your patch fixes issue #123, the entry should look like this: `* Fix big bad boo-boo in task groups (#123 _; PR by @yourgithubaccount)` If there's no issue linked, just link to your pull request instead by updating the changelog after you've created the PR. anyio-4.4.0/.github/workflows/000077500000000000000000000000001462472665400162535ustar00rootroot00000000000000anyio-4.4.0/.github/workflows/publish.yml000066400000000000000000000026131462472665400204460ustar00rootroot00000000000000name: Publish packages to PyPI on: push: tags: - "[0-9]+.[0-9]+.[0-9]+" - "[0-9]+.[0-9]+.[0-9]+.post[0-9]+" - "[0-9]+.[0-9]+.[0-9]+[a-b][0-9]+" - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" jobs: build: name: Build the source tarball and the wheel runs-on: ubuntu-latest environment: release steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.x - name: Install dependencies run: pip install build - name: Create packages run: python -m build - name: Archive packages uses: actions/upload-artifact@v4 with: name: dist path: dist publish: name: Publish build artifacts to the PyPI needs: build runs-on: ubuntu-latest environment: release permissions: id-token: write steps: - name: Retrieve packages uses: actions/download-artifact@v4 - name: Upload packages uses: pypa/gh-action-pypi-publish@release/v1 release: name: Create a GitHub release needs: build runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/checkout@v4 - id: changelog uses: agronholm/release-notes@v1 with: path: docs/versionhistory.rst - uses: ncipollo/release-action@v1 with: body: ${{ steps.changelog.outputs.changelog }} anyio-4.4.0/.github/workflows/test-downstream.yml000066400000000000000000000063461462472665400221470ustar00rootroot00000000000000--- name: Test against downstream projects on: workflow_dispatch: jobs: starlette: name: "Starlette on Python ${{ matrix.python-version }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.10", "3.12"] steps: - uses: actions/checkout@v4 with: repository: encode/starlette - uses: actions/setup-python@v5 with: python-version: "${{ matrix.python-version }}" allow-prereleases: true cache: pip cache-dependency-path: requirements.txt - name: Install dependencies run: | scripts/install pip install anyio[trio]@git+https://github.com/agronholm/anyio.git@${{ github.ref_name }} - name: Run tests run: scripts/test - name: Enforce coverage run: scripts/coverage httpcore: name: "Httpcore on Python ${{ matrix.python-version }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.10", "3.12"] steps: - uses: actions/checkout@v4 with: repository: encode/httpcore - uses: actions/setup-python@v5 with: python-version: "${{ matrix.python-version }}" allow-prereleases: true cache: pip cache-dependency-path: requirements.txt - name: Install dependencies run: | scripts/install pip install anyio[trio]@git+https://github.com/agronholm/anyio.git@${{ github.ref_name }} - name: Run tests run: scripts/test - name: Enforce coverage run: scripts/coverage fastapi: name: "FastAPI on Python ${{ matrix.python-version }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.11"] steps: - uses: actions/checkout@v4 with: repository: tiangolo/fastapi - uses: actions/setup-python@v5 with: python-version: "${{ matrix.python-version }}" allow-prereleases: true cache: pip cache-dependency-path: | requirements-tests.txt pyproject.toml - name: Install dependencies run: | pip install -r requirements-tests.txt pip install anyio[trio]@git+https://github.com/agronholm/anyio.git@${{ github.ref_name }} - name: Run tests run: bash scripts/test.sh litestar: name: "Litestar on Python ${{ matrix.python-version }}" runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.10", "3.12"] steps: - uses: actions/checkout@v4 with: repository: litestar-org/litestar - uses: pdm-project/setup-pdm@v3 name: Set up PDM with: python-version: ${{ matrix.python-version }} allow-python-prereleases: false cache: true cache-dependency-path: pdm.lock - name: Install dependencies run: | pdm install -G:all pdm add --no-lock --update-reuse-installed anyio[trio]@git+https://github.com/agronholm/anyio.git@${{ github.ref_name }} - name: Test run: pdm run pytest docs/examples tests -n auto anyio-4.4.0/.github/workflows/test.yml000066400000000000000000000075301462472665400177620ustar00rootroot00000000000000name: test suite on: push: branches: [master] pull_request: jobs: changed-files: runs-on: ubuntu-latest outputs: workflow-changed: ${{ steps.changed-files.outputs.workflow_any_changed }} pyproject-changed: ${{ steps.changed-files.outputs.src_any_changed }} src-changed: ${{ steps.changed-files.outputs.src_any_changed }} tests-changed: ${{ steps.changed-files.outputs.tests_any_changed }} docs-changed: ${{ steps.changed-files.outputs.doc_any_changed }} steps: - uses: actions/checkout@v4 - name: Get changed files by category id: changed-files uses: tj-actions/changed-files@v41 with: files_yaml: | workflow: - .github/workflows/test.yml pyproject: - pyproject.toml src: - src/** tests: - tests/** doc: - README.rst - docs/** pyright: runs-on: ubuntu-latest needs: changed-files if: | ${{ (needs.changed-files.outputs.workflow-changed == 'true') || (needs.changed-files.outputs.src-changed == 'true') }} steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: 3.x - uses: actions/cache@v4 with: path: ~/.cache/pip key: pip-pyright - name: Install dependencies run: pip install -e . pyright pytest - name: Run pyright run: pyright --verifytypes anyio test: strategy: fail-fast: false matrix: os: [ubuntu-latest] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", pypy-3.10] include: - os: macos-latest python-version: "3.8" - os: macos-latest python-version: "3.12" - os: windows-latest python-version: "3.8" - os: windows-latest python-version: "3.12" runs-on: ${{ matrix.os }} needs: changed-files if: | ${{ (needs.changed-files.outputs.workflow-changed == 'true') || (needs.changed-files.outputs.pyproject-changed == 'true') || (needs.changed-files.outputs.src-changed == 'true') || (needs.changed-files.outputs.tests-changed == 'true') }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true cache: pip cache-dependency-path: pyproject.toml - name: Install dependencies run: pip install -e .[test] - name: Test with pytest run: coverage run -m pytest -v timeout-minutes: 5 env: PYTEST_DISABLE_PLUGIN_AUTOLOAD: 1 - name: Generate coverage report run: coverage xml - name: Upload Coverage uses: coverallsapp/github-action@v2 with: parallel: true file: coverage.xml docs: runs-on: ubuntu-latest needs: changed-files if: | ${{ (needs.changed-files.outputs.workflow-changed == 'true') || (needs.changed-files.outputs.pyproject-changed == 'true') || (needs.changed-files.outputs.src-changed == 'true') || (needs.changed-files.outputs.docs-changed == 'true') }} steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: "3.11" cache: pip cache-dependency-path: pyproject.toml - name: Install dependencies run: pip install -e .[doc] - name: Build documentation run: sphinx-build -W docs build/sphinx coveralls: name: Finish Coveralls needs: test runs-on: ubuntu-latest steps: - name: Finished uses: coverallsapp/github-action@v2 with: parallel-finished: true anyio-4.4.0/.gitignore000066400000000000000000000002471462472665400146510ustar00rootroot00000000000000*.egg-info *.dist-info *.pyc build dist docs/_build venv*/ __pycache__ .coverage .pytest_cache/ .mypy_cache/ .ruff_cache/ .hypothesis/ .eggs/ .tox .idea .cache .local anyio-4.4.0/.pre-commit-config.yaml000066400000000000000000000020241462472665400171350ustar00rootroot00000000000000# This is the configuration file for pre-commit (https://pre-commit.com/). # To use: # * Install pre-commit (https://pre-commit.com/#installation) # * Copy this file as ".pre-commit-config.yaml" # * Run "pre-commit install". repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-toml - id: check-yaml - id: debug-statements - id: end-of-file-fixer - id: mixed-line-ending args: [ "--fix=lf" ] - id: trailing-whitespace - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.4 hooks: - id: ruff args: [--fix, --show-fixes] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.10.0 hooks: - id: mypy additional_dependencies: - pytest - trio >= 0.23 - packaging - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: rst-backticks - id: rst-directive-colons - id: rst-inline-touching-normal anyio-4.4.0/.readthedocs.yml000066400000000000000000000003341462472665400157440ustar00rootroot00000000000000version: 2 build: os: ubuntu-22.04 tools: python: "3.11" sphinx: configuration: docs/conf.py fail_on_warning: true python: install: - method: pip path: . extra_requirements: [doc] anyio-4.4.0/LICENSE000066400000000000000000000020711462472665400136630ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2018 Alex Grönholm Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. anyio-4.4.0/README.rst000066400000000000000000000046341462472665400143540ustar00rootroot00000000000000.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg :target: https://github.com/agronholm/anyio/actions/workflows/test.yml :alt: Build Status .. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master :target: https://coveralls.io/github/agronholm/anyio?branch=master :alt: Code Coverage .. image:: https://readthedocs.org/projects/anyio/badge/?version=latest :target: https://anyio.readthedocs.io/en/latest/?badge=latest :alt: Documentation .. image:: https://badges.gitter.im/gitterHQ/gitter.svg :target: https://gitter.im/python-trio/AnyIO :alt: Gitter chat AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or trio_. It implements trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony with the native SC of trio itself. Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full refactoring necessary. It will blend in with the native libraries of your chosen backend. Documentation ------------- View full documentation at: https://anyio.readthedocs.io/ Features -------- AnyIO offers the following functionality: * Task groups (nurseries_ in trio terminology) * High-level networking (TCP, UDP and UNIX sockets) * `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python 3.8) * async/await style UDP sockets (unlike asyncio where you still have to use Transports and Protocols) * A versatile API for byte streams and object streams * Inter-task synchronization and communication (locks, conditions, events, semaphores, object streams) * Worker threads * Subprocesses * Asynchronous file I/O (using worker threads) * Signal handling AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures. It even works with the popular Hypothesis_ library. .. _asyncio: https://docs.python.org/3/library/asyncio.html .. _trio: https://github.com/python-trio/trio .. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency .. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning .. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs .. _pytest: https://docs.pytest.org/en/latest/ .. _Hypothesis: https://hypothesis.works/ anyio-4.4.0/docs/000077500000000000000000000000001462472665400136065ustar00rootroot00000000000000anyio-4.4.0/docs/api.rst000066400000000000000000000131331462472665400151120ustar00rootroot00000000000000API reference ============= Event loop ---------- .. autofunction:: anyio.run .. autofunction:: anyio.get_all_backends .. autofunction:: anyio.get_cancelled_exc_class .. autofunction:: anyio.sleep .. autofunction:: anyio.sleep_forever .. autofunction:: anyio.sleep_until .. autofunction:: anyio.current_time Asynchronous resources ---------------------- .. autofunction:: anyio.aclose_forcefully .. autoclass:: anyio.abc.AsyncResource Typed attributes ---------------- .. autofunction:: anyio.typed_attribute .. autoclass:: anyio.TypedAttributeSet .. autoclass:: anyio.TypedAttributeProvider Timeouts and cancellation ------------------------- .. autofunction:: anyio.move_on_after .. autofunction:: anyio.fail_after .. autofunction:: anyio.current_effective_deadline .. autoclass:: anyio.CancelScope Task groups ----------- .. autofunction:: anyio.create_task_group .. autoclass:: anyio.abc.TaskGroup .. autoclass:: anyio.abc.TaskStatus Running code in worker threads ------------------------------ .. autofunction:: anyio.to_thread.run_sync .. autofunction:: anyio.to_thread.current_default_thread_limiter Running code in worker processes -------------------------------- .. autofunction:: anyio.to_process.run_sync .. autofunction:: anyio.to_process.current_default_process_limiter Running asynchronous code from other threads -------------------------------------------- .. autofunction:: anyio.from_thread.run .. autofunction:: anyio.from_thread.run_sync .. autofunction:: anyio.from_thread.check_cancelled .. autofunction:: anyio.from_thread.start_blocking_portal .. autoclass:: anyio.from_thread.BlockingPortal .. autoclass:: anyio.from_thread.BlockingPortalProvider Async file I/O -------------- .. autofunction:: anyio.open_file .. autofunction:: anyio.wrap_file .. autoclass:: anyio.AsyncFile .. autoclass:: anyio.Path Streams and stream wrappers --------------------------- .. autofunction:: anyio.create_memory_object_stream .. autoclass:: anyio.abc.UnreliableObjectReceiveStream() .. autoclass:: anyio.abc.UnreliableObjectSendStream() .. autoclass:: anyio.abc.UnreliableObjectStream() .. autoclass:: anyio.abc.ObjectReceiveStream() .. autoclass:: anyio.abc.ObjectSendStream() .. autoclass:: anyio.abc.ObjectStream() .. autoclass:: anyio.abc.ByteReceiveStream .. autoclass:: anyio.abc.ByteSendStream .. autoclass:: anyio.abc.ByteStream .. autoclass:: anyio.abc.Listener .. autodata:: anyio.abc.AnyUnreliableByteReceiveStream .. autodata:: anyio.abc.AnyUnreliableByteSendStream .. autodata:: anyio.abc.AnyUnreliableByteStream .. autodata:: anyio.abc.AnyByteReceiveStream .. autodata:: anyio.abc.AnyByteSendStream .. autodata:: anyio.abc.AnyByteStream .. autoclass:: anyio.streams.buffered.BufferedByteReceiveStream .. autoclass:: anyio.streams.file.FileStreamAttribute .. autoclass:: anyio.streams.file.FileReadStream .. autoclass:: anyio.streams.file.FileWriteStream .. autoclass:: anyio.streams.memory.MemoryObjectReceiveStream .. autoclass:: anyio.streams.memory.MemoryObjectSendStream .. autoclass:: anyio.streams.memory.MemoryObjectStreamStatistics .. autoclass:: anyio.streams.stapled.MultiListener .. autoclass:: anyio.streams.stapled.StapledByteStream .. autoclass:: anyio.streams.stapled.StapledObjectStream .. autoclass:: anyio.streams.text.TextReceiveStream .. autoclass:: anyio.streams.text.TextSendStream .. autoclass:: anyio.streams.text.TextStream .. autoclass:: anyio.streams.tls.TLSAttribute .. autoclass:: anyio.streams.tls.TLSStream .. autoclass:: anyio.streams.tls.TLSListener Sockets and networking ---------------------- .. autofunction:: anyio.connect_tcp .. autofunction:: anyio.connect_unix .. autofunction:: anyio.create_tcp_listener .. autofunction:: anyio.create_unix_listener .. autofunction:: anyio.create_udp_socket .. autofunction:: anyio.create_connected_udp_socket .. autofunction:: anyio.getaddrinfo .. autofunction:: anyio.getnameinfo .. autofunction:: anyio.wait_socket_readable .. autofunction:: anyio.wait_socket_writable .. autoclass:: anyio.abc.SocketAttribute .. autoclass:: anyio.abc.SocketStream() .. autoclass:: anyio.abc.SocketListener() .. autoclass:: anyio.abc.UDPSocket() .. autoclass:: anyio.abc.ConnectedUDPSocket() .. autoclass:: anyio.abc.UNIXSocketStream() Subprocesses ------------ .. autofunction:: anyio.run_process .. autofunction:: anyio.open_process .. autoclass:: anyio.abc.Process Synchronization --------------- .. autoclass:: anyio.Event .. autoclass:: anyio.Lock .. autoclass:: anyio.Condition .. autoclass:: anyio.Semaphore .. autoclass:: anyio.CapacityLimiter .. autoclass:: anyio.ResourceGuard .. autoclass:: anyio.LockStatistics .. autoclass:: anyio.EventStatistics .. autoclass:: anyio.ConditionStatistics .. autoclass:: anyio.CapacityLimiterStatistics .. autoclass:: anyio.SemaphoreStatistics Operating system signals ------------------------ .. autofunction:: anyio.open_signal_receiver Low level operations -------------------- .. autofunction:: anyio.lowlevel.checkpoint .. autofunction:: anyio.lowlevel.checkpoint_if_cancelled .. autofunction:: anyio.lowlevel.cancel_shielded_checkpoint .. autoclass:: anyio.lowlevel.RunVar Testing and debugging --------------------- .. autoclass:: anyio.TaskInfo .. autofunction:: anyio.get_current_task .. autofunction:: anyio.get_running_tasks .. autofunction:: anyio.wait_all_tasks_blocked Exceptions ---------- .. autoexception:: anyio.BrokenResourceError .. autoexception:: anyio.BusyResourceError .. autoexception:: anyio.ClosedResourceError .. autoexception:: anyio.DelimiterNotFound .. autoexception:: anyio.EndOfStream .. autoexception:: anyio.IncompleteRead .. autoexception:: anyio.TypedAttributeLookupError .. autoexception:: anyio.WouldBlock anyio-4.4.0/docs/basics.rst000066400000000000000000000054461462472665400156150ustar00rootroot00000000000000The basics ========== .. py:currentmodule:: anyio AnyIO requires Python 3.8 or later to run. It is recommended that you set up a virtualenv_ when developing or playing around with AnyIO. Installation ------------ To install AnyIO, run: .. code-block:: bash pip install anyio To install a supported version of Trio_, you can install it as an extra like this: .. code-block:: bash pip install anyio[trio] Running async programs ---------------------- The simplest possible AnyIO program looks like this:: from anyio import run async def main(): print('Hello, world!') run(main) This will run the program above on the default backend (asyncio). To run it on another supported backend, say Trio_, you can use the ``backend`` argument, like so:: run(main, backend='trio') But AnyIO code is not required to be run via :func:`run`. You can just as well use the native ``run()`` function of the backend library:: import sniffio import trio from anyio import sleep async def main(): print('Hello') await sleep(1) print("I'm running on", sniffio.current_async_library()) trio.run(main) .. versionchanged:: 4.0.0 On the ``asyncio`` backend, ``anyio.run()`` now uses a back-ported version of :class:`asyncio.Runner` on Pythons older than 3.11. .. _backend options: Backend specific options ------------------------ **Asyncio**: * options covered in the documentation of :class:`asyncio.Runner` * ``use_uvloop`` (``bool``, default=False): Use the faster uvloop_ event loop implementation, if available (this is a shorthand for passing ``loop_factory=uvloop.new_event_loop``, and is ignored if ``loop_factory`` is passed a value other than ``None``) **Trio**: options covered in the `official documentation `_ .. versionchanged:: 3.2.0 The default value of ``use_uvloop`` was changed to ``False``. .. versionchanged:: 4.0.0 The ``policy`` option was replaced with ``loop_factory``. .. _uvloop: https://pypi.org/project/uvloop/ Using native async libraries ---------------------------- AnyIO lets you mix and match code written for AnyIO and code written for the asynchronous framework of your choice. There are a few rules to keep in mind however: * You can only use "native" libraries for the backend you're running, so you cannot, for example, use a library written for Trio_ together with a library written for asyncio. * Tasks spawned by these "native" libraries on backends other than Trio_ are not subject to the cancellation rules enforced by AnyIO * Threads spawned outside of AnyIO cannot use :func:`.from_thread.run` to call asynchronous code .. _virtualenv: https://docs.python-guide.org/dev/virtualenvs/ .. _Trio: https://github.com/python-trio/trio anyio-4.4.0/docs/cancellation.rst000066400000000000000000000205401462472665400167750ustar00rootroot00000000000000Cancellation and timeouts ========================= .. py:currentmodule:: anyio The ability to cancel tasks is the foremost advantage of the asynchronous programming model. Threads, on the other hand, cannot be forcibly killed and shutting them down will require perfect cooperation from the code running in them. Cancellation in AnyIO follows the model established by the Trio_ framework. This means that cancellation of tasks is done via so called *cancel scopes*. Cancel scopes are used as context managers and can be nested. Cancelling a cancel scope cancels all cancel scopes nested within it. If a task is waiting on something, it is cancelled immediately. If the task is just starting, it will run until it first tries to run an operation requiring waiting, such as :func:`~sleep`. A task group contains its own cancel scope. The entire task group can be cancelled by cancelling this scope. .. _Trio: https://trio.readthedocs.io/en/latest/reference-core.html #cancellation-and-timeouts Timeouts -------- Networked operations can often take a long time, and you usually want to set up some kind of a timeout to ensure that your application doesn't stall forever. There are two principal ways to do this: :func:`~move_on_after` and :func:`~fail_after`. Both are used as synchronous context managers. The difference between these two is that the former simply exits the context block prematurely on a timeout, while the other raises a :exc:`TimeoutError`. Both methods create a new cancel scope, and you can check the deadline by accessing the :attr:`~.CancelScope.deadline` attribute. Note, however, that an outer cancel scope may have an earlier deadline than your current cancel scope. To check the actual deadline, you can use the :func:`~current_effective_deadline` function. Here's how you typically use timeouts:: from anyio import create_task_group, move_on_after, sleep, run async def main(): async with create_task_group() as tg: with move_on_after(1) as scope: print('Starting sleep') await sleep(2) print('This should never be printed') # The cancelled_caught property will be True if timeout was reached print('Exited cancel scope, cancelled =', scope.cancelled_caught) run(main) .. note:: It's recommended not to directly cancel a scope from :func:`~fail_after`, as that may currently result in :exc:`TimeoutError` being erroneously raised if exiting the scope is delayed long enough for the deadline to be exceeded. Shielding --------- There are cases where you want to shield your task from cancellation, at least temporarily. The most important such use case is performing shutdown procedures on asynchronous resources. To accomplish this, open a new cancel scope with the ``shield=True`` argument:: from anyio import CancelScope, create_task_group, sleep, run async def external_task(): print('Started sleeping in the external task') await sleep(1) print('This line should never be seen') async def main(): async with create_task_group() as tg: with CancelScope(shield=True) as scope: tg.start_soon(external_task) tg.cancel_scope.cancel() print('Started sleeping in the host task') await sleep(1) print('Finished sleeping in the host task') run(main) The shielded block will be exempt from cancellation except when the shielded block itself is being cancelled. Shielding a cancel scope is often best combined with :func:`~move_on_after` or :func:`~fail_after`, both of which also accept ``shield=True``. Finalization ------------ Sometimes you may want to perform cleanup operations in response to the failure of the operation:: async def do_something(): try: await run_async_stuff() except BaseException: # (perform cleanup) raise In some specific cases, you might only want to catch the cancellation exception. This is tricky because each async framework has its own exception class for that and AnyIO cannot control which exception is raised in the task when it's cancelled. To work around that, AnyIO provides a way to retrieve the exception class specific to the currently running async framework, using:func:`~get_cancelled_exc_class`:: from anyio import get_cancelled_exc_class async def do_something(): try: await run_async_stuff() except get_cancelled_exc_class(): # (perform cleanup) raise .. warning:: Always reraise the cancellation exception if you catch it. Failing to do so may cause undefined behavior in your application. If you need to use ``await`` during finalization, you need to enclose it in a shielded cancel scope, or the operation will be cancelled immediately since it's in an already cancelled scope:: async def do_something(): try: await run_async_stuff() except get_cancelled_exc_class(): with CancelScope(shield=True): await some_cleanup_function() raise Avoiding cancel scope stack corruption -------------------------------------- When using cancel scopes, it is important that they are entered and exited in LIFO (last in, first out) order within each task. This is usually not an issue since cancel scopes are normally used as context managers. However, in certain situations, cancel scope stack corruption might still occur: * Manually calling ``CancelScope.__enter__()`` and ``CancelScope.__exit__()``, usually from another context manager class, in the wrong order * Using cancel scopes with ``[Async]ExitStack`` in a manner that couldn't be achieved by nesting them as context managers * Using the low level coroutine protocol to execute parts of the coroutine function in different cancel scopes * Yielding in an async generator while enclosed in a cancel scope Remember that task groups contain their own cancel scopes so the same list of risky situations applies to them too. As an example, the following code is highly dubious:: # Bad! async def some_generator(): async with create_task_group() as tg: tg.start_soon(foo) yield The problem with this code is that it violates structural concurrency: what happens if the spawned task raises an exception? The host task would be cancelled as a result, but the host task might be long gone by the time that happens. Even if it weren't, any enclosing ``try...except`` in the generator would not be triggered. Unfortunately there is currently no way to automatically detect this condition in AnyIO, so in practice you may simply experience some weird behavior in your application as a consequence of running code like above. Depending on how they are used, this pattern is, however, *usually* safe to use in asynchronous context managers, so long as you make sure that the same host task keeps running throughout the entire enclosed code block:: # Okay in most cases! @async_context_manager async def some_context_manager(): async with create_task_group() as tg: tg.start_soon(foo) yield Prior to AnyIO 3.6, this usage pattern was also invalid in pytest's asynchronous generator fixtures. Starting from 3.6, however, each async generator fixture is run from start to end in the same task, making it possible to have task groups or cancel scopes safely straddle the ``yield``. When you're implementing the async context manager protocol manually and your async context manager needs to use other context managers, you may find it necessary to call their ``__aenter__()`` and ``__aexit__()`` directly. In such cases, it is absolutely vital to ensure that their ``__aexit__()`` methods are called in the exact reverse order of the ``__aenter__()`` calls. To this end, you may find the :class:`~contextlib.AsyncExitStack` class very useful:: from contextlib import AsyncExitStack from anyio import create_task_group class MyAsyncContextManager: async def __aenter__(self): self._exitstack = AsyncExitStack() await self._exitstack.__aenter__() self._task_group = await self._exitstack.enter_async_context( create_task_group() ) async def __aexit__(self, exc_type, exc_val, exc_tb): return await self._exitstack.__aexit__(exc_type, exc_val, exc_tb) anyio-4.4.0/docs/conf.py000066400000000000000000000014741462472665400151130ustar00rootroot00000000000000#!/usr/bin/env python3 from __future__ import annotations from importlib.metadata import version as get_version from packaging.version import parse extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx_autodoc_typehints", ] templates_path = ["_templates"] source_suffix = ".rst" master_doc = "index" project = "AnyIO" author = "Alex Grönholm" copyright = "2018, " + author v = parse(get_version("anyio")) version = v.base_version release = v.public language = "en" exclude_patterns = ["_build"] pygments_style = "sphinx" autodoc_default_options = {"members": True, "show-inheritance": True} autodoc_mock_imports = ["_typeshed"] todo_include_todos = False html_theme = "sphinx_rtd_theme" htmlhelp_basename = "anyiodoc" intersphinx_mapping = {"python": ("https://docs.python.org/3/", None)} anyio-4.4.0/docs/contributing.rst000066400000000000000000000045121462472665400170510ustar00rootroot00000000000000Contributing to AnyIO ===================== If you wish to contribute a fix or feature to AnyIO, please follow the following guidelines. When you make a pull request against the main AnyIO codebase, Github runs the AnyIO test suite against your modified code. Before making a pull request, you should ensure that the modified code passes tests locally. To that end, the use of tox_ is recommended. The default tox run first runs ``pre-commit`` and then the actual test suite. To run the checks on all environments in parallel, invoke tox with ``tox -p``. To build the documentation, run ``tox -e docs`` which will generate a directory named ``build`` in which you may view the formatted HTML documentation. AnyIO uses pre-commit_ to perform several code style/quality checks. It is recommended to activate pre-commit_ on your local clone of the repository (using ``pre-commit install``) to ensure that your changes will pass the same checks on GitHub. .. _tox: https://tox.readthedocs.io/en/latest/install.html .. _pre-commit: https://pre-commit.com/#installation Making a pull request on Github ------------------------------- To get your changes merged to the main codebase, you need a Github account. #. Fork the repository (if you don't have your own fork of it yet) by navigating to the `main AnyIO repository`_ and clicking on "Fork" near the top right corner. #. Clone the forked repository to your local machine with ``git clone git@github.com/yourusername/anyio``. #. Create a branch for your pull request, like ``git checkout -b myfixname`` #. Make the desired changes to the code base. #. Commit your changes locally. If your changes close an existing issue, add the text ``Fixes XXX.`` or ``Closes XXX.`` to the commit message (where XXX is the issue number). #. Push the changeset(s) to your forked repository (``git push``) #. Navigate to Pull requests page on the original repository (not your fork) and click "New pull request" #. Click on the text "compare across forks". #. Select your own fork as the head repository and then select the correct branch name. #. Click on "Create pull request". If you have trouble, consult the `pull request making guide`_ on opensource.com. .. _main AnyIO repository: https://github.com/agronholm/anyio .. _pull request making guide: https://opensource.com/article/19/7/create-pull-request-github anyio-4.4.0/docs/faq.rst000066400000000000000000000025631462472665400151150ustar00rootroot00000000000000Frequently Asked Questions ========================== Why is Curio not supported as a backend? ---------------------------------------- Curio_ was supported in AnyIO before v3.0. Support for it was dropped for two reasons: #. Its interface allowed only coroutine functions to access the Curio_ kernel. This forced AnyIO to follow suit in its own API design, making it difficult to adapt existing applications that relied on synchronous callbacks to use AnyIO. It also interfered with the goal of matching Trio's API in functions with the same purpose (e.g. ``Event.set()``). #. The maintainer specifically requested Curio_ support to be removed from AnyIO (`issue 185 `_). .. _Curio: https://github.com/dabeaz/curio Why is Twisted not supported as a backend? ------------------------------------------ The minimum requirement to support Twisted_ would be for sniffio_ to be able to detect a running Twisted event loop (and be able to tell when Twisted_ is being run on top of its asyncio reactor). This is not currently supported in sniffio_, so AnyIO cannot support Twisted either. There is a Twisted `issue `_ that you can follow if you're interested in Twisted support in AnyIO. .. _Twisted: https://twistedmatrix.com/trac/ .. _sniffio: https://github.com/python-trio/sniffio anyio-4.4.0/docs/fileio.rst000066400000000000000000000050151462472665400156100ustar00rootroot00000000000000Asynchronous file I/O support ============================= .. py:currentmodule:: anyio AnyIO provides asynchronous wrappers for blocking file operations. These wrappers run blocking operations in worker threads. Example:: from anyio import open_file, run async def main(): async with await open_file('/some/path/somewhere') as f: contents = await f.read() print(contents) run(main) The wrappers also support asynchronous iteration of the file line by line, just as the standard file objects support synchronous iteration:: from anyio import open_file, run async def main(): async with await open_file('/some/path/somewhere') as f: async for line in f: print(line, end='') run(main) To wrap an existing open file object as an asynchronous file, you can use :func:`.wrap_file`:: from anyio import wrap_file, run async def main(): with open('/some/path/somewhere') as f: async for line in wrap_file(f): print(line, end='') run(main) .. note:: Closing the wrapper also closes the underlying synchronous file object. .. seealso:: :ref:`FileStreams` Asynchronous path operations ---------------------------- AnyIO provides an asynchronous version of the :class:`pathlib.Path` class. It differs with the original in a number of ways: * Operations that perform disk I/O (like :meth:`~pathlib.Path.read_bytes`) are run in a worker thread and thus require an ``await`` * Methods like :meth:`~pathlib.Path.glob` return an asynchronous iterator that yields asynchronous :class:`~.Path` objects * Properties and methods that normally return :class:`pathlib.Path` objects return :class:`~.Path` objects instead * Methods and properties from the Python 3.10 API are available on all versions * Use as a context manager is not supported, as it is deprecated in pathlib For example, to create a file with binary content:: from anyio import Path, run async def main(): path = Path('/foo/bar') await path.write_bytes(b'hello, world') run(main) Asynchronously iterating a directory contents can be done as follows:: from anyio import Path, run async def main(): # Print the contents of every file (assumed to be text) in the directory /foo/bar dir_path = Path('/foo/bar') async for path in dir_path.iterdir(): if await path.is_file(): print(await path.read_text()) print('---------------------') run(main) anyio-4.4.0/docs/index.rst000066400000000000000000000004761462472665400154560ustar00rootroot00000000000000AnyIO ===== .. include:: ../README.rst The manual ---------- .. toctree:: :maxdepth: 2 basics tasks cancellation synchronization streams typedattrs networking threads subprocesses fileio signals testing api migration faq support contributing versionhistory anyio-4.4.0/docs/migration.rst000066400000000000000000000247001462472665400163340ustar00rootroot00000000000000Migrating from AnyIO 3 to AnyIO 4 ================================= .. py:currentmodule:: anyio The non-standard exception group class was removed -------------------------------------------------- AnyIO 3 had its own ``ExceptionGroup`` class which predated the :pep:`654` exception group classes. This class has now been removed in favor of the built-in :exc:`BaseExceptionGroup` and :exc:`ExceptionGroup` classes. If your code was either raising the old ``ExceptionGroup`` exception or catching it, you need to make the switch to these standard classes. Otherwise you can ignore this part. If you're targeting Python releases older than 3.11, you need to use the exceptiongroup_ backport and import one of those classes from ``exceptiongroup``. The only difference between :exc:`BaseExceptionGroup` and :exc:`ExceptionGroup` is that the latter can only contain exceptions derived from :exc:`Exception`, and likewise can be caught with ``except Exception:``. Task groups now wrap single exceptions in groups ------------------------------------------------ The most prominent backwards incompatible change in AnyIO 4 was that task groups now always raise exception groups when either the host task or any child tasks raise an exception (other than a cancellation exception). Previously, an exception group was only raised when more than one exception needed to be raised from the task group. The practical consequence is that if your code previously expected to catch a specific kind of exception falling out of a task group, you now need to either switch to the ``except*`` syntax (if you're fortunate enough to work solely with Python 3.11 or later), or use the ``catch()`` context manager from the exceptiongroup_ backport. So, if you had code like this:: try: await function_using_a_taskgroup() except ValueError as exc: ... The Python 3.11+ equivalent would look almost the same:: try: await function_using_a_taskgroup() except* ValueError as excgrp: # Note: excgrp is an ExceptionGroup now! ... If you need to stay compatible with older Python releases, you need to use the backport:: from exceptiongroup import ExceptionGroup, catch def handle_value_errors(excgrp: ExceptionGroup) -> None: ... with catch({ValueError: handle_value_errors}): await function_using_a_taskgroup() This difference often comes up in test suites too. For example, if you had this before in a pytest-based test suite:: with pytest.raises(ValueError): await function_using_a_taskgroup() You now need to change it to:: from exceptiongroup import ExceptionGroup with pytest.raises(ExceptionGroup) as exc: await function_using_a_taskgroup() assert len(exc.value.exceptions) == 1 assert isinstance(exc.value.exceptions[0], ValueError) If you need to stay compatible with both AnyIO 3 and 4, you can use the following compatibility code to "collapse" single-exception groups by unwrapping them:: import sys from contextlib import contextmanager from typing import Generator has_exceptiongroups = True if sys.version_info < (3, 11): try: from exceptiongroup import BaseExceptionGroup except ImportError: has_exceptiongroups = False @contextmanager def collapse_excgroups() -> Generator[None, None, None]: try: yield except BaseException as exc: if has_exceptiongroups: while isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1: exc = exc.exceptions[0] raise exc Syntax for type annotated memory object streams has changed ----------------------------------------------------------- Where previously, creating type annotated memory object streams worked by passing the desired type as the second argument:: send, receive = create_memory_object_stream(100, int) In 4.0, :class:`create_memory_object_stream() ` is a class masquerading as a function, so you need to parametrize it:: send, receive = create_memory_object_stream[int](100) If you didn't parametrize your memory object streams before, then you don't need to make any changes in this regard. Event loop factories instead of event loop policies ---------------------------------------------------- If you're using a custom asyncio event loop policy with :func:`run`, you need to switch to passing an *event loop factory*, that is, a callable that returns a new event loop. Using uvloop_ as an example, code like the following:: anyio.run(main, backend_options={"event_loop_policy": uvloop.EventLoopPolicy()}) should be converted into:: anyio.run(main, backend_options={"loop_factory": uvloop.new_event_loop}) Make sure not to actually call the factory function! .. _exceptiongroup: https://pypi.org/project/exceptiongroup/ .. _uvloop: https://github.com/MagicStack/uvloop Migrating from AnyIO 2 to AnyIO 3 ================================= AnyIO 3 changed some functions and methods in a way that needs some adaptation in your code. All deprecated functions and methods will be removed in AnyIO 4. Asynchronous functions converted to synchronous ----------------------------------------------- AnyIO 3 changed several previously asynchronous functions and methods into regular ones for two reasons: #. to better serve use cases where synchronous callbacks are used by third party libraries #. to better match the API of Trio_ The following functions and methods were changed: * :func:`current_time` * :func:`current_effective_deadline` * :meth:`CancelScope.cancel() <.CancelScope.cancel>` * :meth:`CapacityLimiter.acquire_nowait` * :meth:`CapacityLimiter.acquire_on_behalf_of_nowait` * :meth:`Condition.release` * :meth:`Event.set` * :func:`get_current_task` * :func:`get_running_tasks` * :meth:`Lock.release` * :meth:`MemoryObjectReceiveStream.receive_nowait() <.streams.memory.MemoryObjectReceiveStream.receive_nowait>` * :meth:`MemoryObjectSendStream.send_nowait() <.streams.memory.MemoryObjectSendStream.send_nowait>` * :func:`open_signal_receiver` * :meth:`Semaphore.release` When migrating to AnyIO 3, simply remove the ``await`` from each call to these. .. note:: For backwards compatibility reasons, :func:`current_time`, :func:`current_effective_deadline` and :func:`get_running_tasks` return objects which are awaitable versions of their original types (:class:`float` and :class:`list`, respectively). These awaitable versions are subclasses of the original types so they should behave as their originals, but if you absolutely need the pristine original types, you can either use ``maybe_async`` or ``float()`` / ``list()`` on the returned value as appropriate. The following async context managers changed to regular context managers: * :func:`fail_after` * :func:`move_on_after` * ``open_cancel_scope()`` (now just ``CancelScope()``) When migrating, just change ``async with`` into a plain ``with``. With the exception of :meth:`MemoryObjectReceiveStream.receive_nowait() <.streams.memory.MemoryObjectReceiveStream.receive_nowait>`, all of them can still be used like before – they will raise :exc:`DeprecationWarning` when used this way on AnyIO 3, however. If you're writing a library that needs to be compatible with both major releases, you will need to use the compatibility functions added in AnyIO 2.2: ``maybe_async()`` and ``maybe_async_cm()``. These will let you safely use functions/methods and context managers (respectively) regardless of which major release is currently installed. Example 1 – setting an event:: from anyio.abc import Event from anyio import maybe_async async def foo(event: Event): await maybe_async(event.set()) ... Example 2 – opening a cancel scope:: from anyio import CancelScope, maybe_async_cm async def foo(): async with maybe_async_cm(CancelScope()) as scope: ... .. _Trio: https://github.com/python-trio/trio Starting tasks -------------- The ``TaskGroup.spawn()`` coroutine method has been deprecated in favor of the synchronous method :meth:`.TaskGroup.start_soon` (which mirrors ``start_soon()`` in Trio's nurseries). If you're fully migrating to AnyIO 3, simply switch to calling the new method (and remove the ``await``). If your code needs to work with both AnyIO 2 and 3, you can keep using ``TaskGroup.spawn()`` (until AnyIO 4) and suppress the deprecation warning:: import warnings async def foo(): async with create_task_group() as tg: with warnings.catch_warnings(): await tg.spawn(otherfunc) Blocking portal changes ----------------------- AnyIO now **requires** :func:`.from_thread.start_blocking_portal` to be used as a context manager:: from anyio import sleep from anyio.from_thread import start_blocking_portal with start_blocking_portal() as portal: portal.call(sleep, 1) As with ``TaskGroup.spawn()``, the ``BlockingPortal.spawn_task()`` method has also been renamed to :meth:`~from_thread.BlockingPortal.start_task_soon`, so as to be consistent with task groups. The ``create_blocking_portal()`` factory function was also deprecated in favor of instantiating :class:`~from_thread.BlockingPortal` directly. For code requiring cross compatibility, catching the deprecation warning (as above) should work. Synchronization primitives -------------------------- Synchronization primitive factories (``create_event()`` etc.) were deprecated in favor of instantiating the classes directly. So convert code like this:: from anyio import create_event async def main(): event = create_event() into this:: from anyio import Event async def main(): event = Event() or, if you need to work with both AnyIO 2 and 3:: try: from anyio import Event create_event = Event except ImportError: from anyio import create_event from anyio.abc import Event async def foo() -> Event: return create_event() Threading functions moved ------------------------- Threading functions were restructured to submodules, following the example of Trio: * ``current_default_worker_thread_limiter`` → :func:`.to_thread.current_default_thread_limiter` (NOTE: the function was renamed too!) * ``run_sync_in_worker_thread()`` → :func:`.to_thread.run_sync` * ``run_async_from_thread()`` → :func:`.from_thread.run` * ``run_sync_from_thread()`` → :func:`.from_thread.run_sync` The old versions are still in place but emit deprecation warnings when called. anyio-4.4.0/docs/networking.rst000066400000000000000000000167651462472665400165460ustar00rootroot00000000000000Using sockets and streams ========================= .. py:currentmodule:: anyio Networking capabilities are arguably the most important part of any asynchronous library. AnyIO contains its own high level implementation of networking on top of low level primitives offered by each of its supported backends. Currently AnyIO offers the following networking functionality: * TCP sockets (client + server) * UNIX domain sockets (client + server) * UDP sockets * UNIX datagram sockets More exotic forms of networking such as raw sockets and SCTP are currently not supported. .. warning:: Unlike the standard BSD sockets interface and most other networking libraries, AnyIO (from 2.0 onwards) signals the end of any stream by raising the :exc:`~EndOfStream` exception instead of returning an empty bytes object. Working with TCP sockets ------------------------ TCP (Transmission Control Protocol) is the most commonly used protocol on the Internet. It allows one to connect to a port on a remote host and send and receive data in a reliable manner. To connect to a listening TCP socket somewhere, you can use :func:`~connect_tcp`:: from anyio import connect_tcp, run async def main(): async with await connect_tcp('hostname', 1234) as client: await client.send(b'Client\n') response = await client.receive() print(response) run(main) As a convenience, you can also use :func:`~connect_tcp` to establish a TLS session with the peer after connection, by passing ``tls=True`` or by passing a nonempty value for either ``ssl_context`` or ``tls_hostname``. To receive incoming TCP connections, you first create a TCP listener with :func:`create_tcp_listener` and call :meth:`~.abc.Listener.serve` on it:: from anyio import create_tcp_listener, run async def handle(client): async with client: name = await client.receive(1024) await client.send(b'Hello, %s\n' % name) async def main(): listener = await create_tcp_listener(local_port=1234) await listener.serve(handle) run(main) See the section on :ref:`TLS` for more information. Working with UNIX sockets ------------------------- UNIX domain sockets are a form of interprocess communication on UNIX-like operating systems. They cannot be used to connect to remote hosts and do not work on Windows. The API for UNIX domain sockets is much like the one for TCP sockets, except that instead of host/port combinations, you use file system paths. This is what the client from the TCP example looks like when converted to use UNIX sockets:: from anyio import connect_unix, run async def main(): async with await connect_unix('/tmp/mysock') as client: await client.send(b'Client\n') response = await client.receive(1024) print(response) run(main) And the listener:: from anyio import create_unix_listener, run async def handle(client): async with client: name = await client.receive(1024) await client.send(b'Hello, %s\n' % name) async def main(): listener = await create_unix_listener('/tmp/mysock') await listener.serve(handle) run(main) .. note:: The UNIX socket listener does not remove the socket it creates, so you may need to delete them manually. Sending and receiving file descriptors ++++++++++++++++++++++++++++++++++++++ UNIX sockets can be used to pass open file descriptors (sockets and files) to another process. The receiving end can then use either :func:`os.fdopen` or :class:`socket.socket` to get a usable file or socket object, respectively. The following is an example where a client connects to a UNIX socket server and receives the descriptor of a file opened on the server, reads the contents of the file and then prints them on standard output. Client:: import os from anyio import connect_unix, run async def main(): async with await connect_unix('/tmp/mysock') as client: _, fds = await client.receive_fds(0, 1) with os.fdopen(fds[0]) as file: print(file.read()) run(main) Server:: from pathlib import Path from anyio import create_unix_listener, run async def handle(client): async with client: with path.open('r') as file: await client.send_fds(b'this message is ignored', [file]) async def main(): listener = await create_unix_listener('/tmp/mysock') await listener.serve(handle) path = Path('/tmp/examplefile') path.write_text('Test file') run(main) Working with UDP sockets ------------------------ UDP (User Datagram Protocol) is a way of sending packets over the network without features like connections, retries or error correction. For example, if you wanted to create a UDP "hello" service that just reads a packet and then sends a packet to the sender with the contents prepended with "Hello, ", you would do this:: import socket from anyio import create_udp_socket, run async def main(): async with await create_udp_socket( family=socket.AF_INET, local_port=1234 ) as udp: async for packet, (host, port) in udp: await udp.sendto(b'Hello, ' + packet, host, port) run(main) .. note:: If you are testing on your local machine or don't know which family socket to use, it is a good idea to replace ``family=socket.AF_INET`` by ``local_host='localhost'`` in the previous example. If your use case involves sending lots of packets to a single destination, you can still "connect" your UDP socket to a specific host and port to avoid having to pass the address and port every time you send data to the peer:: from anyio import create_connected_udp_socket, run async def main(): async with await create_connected_udp_socket( remote_host='hostname', remote_port=1234) as udp: await udp.send(b'Hi there!\n') run(main) Working with UNIX datagram sockets ---------------------------------- UNIX datagram sockets are a subset of UNIX domain sockets, with the difference being that while UNIX sockets implement reliable communication of a continuous byte stream (similarly to TCP), UNIX datagram sockets implement communication of data packets (similarly to UDP). The API for UNIX datagram sockets is modeled after the one for UDP sockets, except that instead of host/port combinations, you use file system paths - here is the UDP "hello" service example written with UNIX datagram sockets:: from anyio import create_unix_datagram_socket, run async def main(): async with await create_unix_datagram_socket( local_path='/tmp/mysock' ) as unix_dg: async for packet, path in unix_dg: await unix_dg.sendto(b'Hello, ' + packet, path) run(main) .. note:: If ``local_path`` is not set, the UNIX datagram socket will be bound on an unnamed address, and will generally not be able to receive datagrams from other UNIX datagram sockets. Similarly to UDP sockets, if your case involves sending lots of packets to a single destination, you can "connect" your UNIX datagram socket to a specific path to avoid having to pass the path every time you send data to the peer:: from anyio import create_connected_unix_datagram_socket, run async def main(): async with await create_connected_unix_datagram_socket( remote_path='/dev/log' ) as unix_dg: await unix_dg.send(b'Hi there!\n') run(main) anyio-4.4.0/docs/signals.rst000066400000000000000000000050151462472665400160010ustar00rootroot00000000000000Receiving operating system signals ================================== .. py:currentmodule:: anyio You may occasionally find it useful to receive signals sent to your application in a meaningful way. For example, when you receive a ``signal.SIGTERM`` signal, your application is expected to shut down gracefully. Likewise, ``SIGHUP`` is often used as a means to ask the application to reload its configuration. AnyIO provides a simple mechanism for you to receive the signals you're interested in:: import signal from anyio import open_signal_receiver, run async def main(): with open_signal_receiver(signal.SIGTERM, signal.SIGHUP) as signals: async for signum in signals: if signum == signal.SIGTERM: return elif signum == signal.SIGHUP: print('Reloading configuration') run(main) .. note:: Signal handlers can only be installed in the main thread, so they will not work when the event loop is being run through :class:`~.from_thread.BlockingPortal`, for instance. .. note:: Windows does not natively support signals so do not rely on this in a cross platform application. Handling KeyboardInterrupt and SystemExit ----------------------------------------- By default, different backends handle the Ctrl+C (or Ctrl+Break on Windows) key combination and external termination (:exc:`KeyboardInterrupt` and :exc:`SystemExit`, respectively) differently: Trio raises the relevant exception inside the application while asyncio shuts down all the tasks and exits. If you need to do your own cleanup in these situations, you will need to install a signal handler:: import signal from anyio import open_signal_receiver, create_task_group, run from anyio.abc import CancelScope async def signal_handler(scope: CancelScope): with open_signal_receiver(signal.SIGINT, signal.SIGTERM) as signals: async for signum in signals: if signum == signal.SIGINT: print('Ctrl+C pressed!') else: print('Terminated!') scope.cancel() return async def main(): async with create_task_group() as tg: tg.start_soon(signal_handler, tg.cancel_scope) ... # proceed with starting the actual application logic run(main) .. note:: Windows does not support the :data:`~signal.SIGTERM` signal so if you need a mechanism for graceful shutdown on Windows, you will have to find another way. anyio-4.4.0/docs/streams.rst000066400000000000000000000306461462472665400160270ustar00rootroot00000000000000Streams ======= .. py:currentmodule:: anyio A "stream" in AnyIO is a simple interface for transporting information from one place to another. It can mean either in-process communication or sending data over a network. AnyIO divides streams into two categories: byte streams and object streams. Byte streams ("Streams" in Trio lingo) are objects that receive and/or send chunks of bytes. They are modelled after the limitations of the stream sockets, meaning the boundaries are not respected. In practice this means that if, for example, you call ``.send(b'hello ')`` and then ``.send(b'world')``, the other end will receive the data chunked in any arbitrary way, like (``b'hello'`` and ``b' world'``), ``b'hello world'`` or (``b'hel'``, ``b'lo wo'``, ``b'rld'``). Object streams ("Channels" in Trio lingo), on the other hand, deal with Python objects. The most commonly used implementation of these is the memory object stream. The exact semantics of object streams vary a lot by implementation. Many stream implementations wrap other streams. Of these, some can wrap any bytes-oriented streams, meaning ``ObjectStream[bytes]`` and ``ByteStream``. This enables many interesting use cases. .. _memory object streams: Memory object streams --------------------- Memory object streams are intended for implementing a producer-consumer pattern with multiple tasks. Using :func:`~create_memory_object_stream`, you get a pair of object streams: one for sending, one for receiving. They essentially work like queues, but with support for closing and asynchronous iteration. By default, memory object streams are created with a buffer size of 0. This means that :meth:`~.streams.memory.MemoryObjectSendStream.send` will block until there's another task that calls :meth:`~.streams.memory.MemoryObjectReceiveStream.receive`. You can set the buffer size to a value of your choosing when creating the stream. It is also possible to have an unbounded buffer by passing :data:`math.inf` as the buffer size but this is not recommended. Memory object streams can be cloned by calling the ``clone()`` method. Each clone can be closed separately, but each end of the stream is only considered closed once all of its clones have been closed. For example, if you have two clones of the receive stream, the send stream will start raising :exc:`~BrokenResourceError` only when both receive streams have been closed. Multiple tasks can send and receive on the same memory object stream (or its clones) but each sent item is only ever delivered to a single recipient. The receive ends of memory object streams can be iterated using the async iteration protocol. The loop exits when all clones of the send stream have been closed. Example:: from anyio import create_task_group, create_memory_object_stream, run from anyio.streams.memory import MemoryObjectReceiveStream async def process_items(receive_stream: MemoryObjectReceiveStream[str]) -> None: async with receive_stream: async for item in receive_stream: print('received', item) async def main(): # The [str] specifies the type of the objects being passed through the # memory object stream. This is a bit of trick, as create_memory_object_stream # is actually a class masquerading as a function. send_stream, receive_stream = create_memory_object_stream[str]() async with create_task_group() as tg: tg.start_soon(process_items, receive_stream) async with send_stream: for num in range(10): await send_stream.send(f'number {num}') run(main) In contrast to other AnyIO streams (but in line with Trio's Channels), memory object streams can be closed synchronously, using either the ``close()`` method or by using the stream as a context manager:: from anyio.streams.memory import MemoryObjectSendStream def synchronous_callback(send_stream: MemoryObjectSendStream[str]) -> None: with send_stream: send_stream.send_nowait('hello') Stapled streams --------------- A stapled stream combines any mutually compatible receive and send stream together, forming a single bidirectional stream. It comes in two variants: * :class:`~.streams.stapled.StapledByteStream` (combines a :class:`~.abc.ByteReceiveStream` with a :class:`~.abc.ByteSendStream`) * :class:`~.streams.stapled.StapledObjectStream` (combines an :class:`~.abc.ObjectReceiveStream` with a compatible :class:`~.abc.ObjectSendStream`) Buffered byte streams --------------------- A buffered byte stream wraps an existing bytes-oriented receive stream and provides certain amenities that require buffering, such as receiving an exact number of bytes, or receiving until the given delimiter is found. Example:: from anyio import run, create_memory_object_stream from anyio.streams.buffered import BufferedByteReceiveStream async def main(): send, receive = create_memory_object_stream[bytes](4) buffered = BufferedByteReceiveStream(receive) for part in b'hel', b'lo, ', b'wo', b'rld!': await send.send(part) result = await buffered.receive_exactly(8) print(repr(result)) result = await buffered.receive_until(b'!', 10) print(repr(result)) run(main) The above script gives the following output:: b'hello, w' b'orld' Text streams ------------ Text streams wrap existing receive/send streams and encode/decode strings to bytes and vice versa. Example:: from anyio import run, create_memory_object_stream from anyio.streams.text import TextReceiveStream, TextSendStream async def main(): bytes_send, bytes_receive = create_memory_object_stream[bytes](1) text_send = TextSendStream(bytes_send) await text_send.send('åäö') result = await bytes_receive.receive() print(repr(result)) text_receive = TextReceiveStream(bytes_receive) await bytes_send.send(result) result = await text_receive.receive() print(repr(result)) run(main) The above script gives the following output:: b'\xc3\xa5\xc3\xa4\xc3\xb6' 'åäö' .. _FileStreams: File streams ------------ File streams read from or write to files on the file system. They can be useful for substituting a file for another source of data, or writing output to a file for logging or debugging purposes. Example:: from anyio import run from anyio.streams.file import FileReadStream, FileWriteStream async def main(): path = '/tmp/testfile' async with await FileWriteStream.from_path(path) as stream: await stream.send(b'Hello, World!') async with await FileReadStream.from_path(path) as stream: async for chunk in stream: print(chunk.decode(), end='') print() run(main) .. versionadded:: 3.0 .. _TLS: TLS streams ----------- TLS (Transport Layer Security), the successor to SSL (Secure Sockets Layer), is the supported way of providing authenticity and confidentiality for TCP streams in AnyIO. TLS is typically established right after the connection has been made. The handshake involves the following steps: * Sending the certificate to the peer (usually just by the server) * Checking the peer certificate(s) against trusted CA certificates * Checking that the peer host name matches the certificate Obtaining a server certificate ****************************** There are three principal ways you can get an X.509 certificate for your server: #. Create a self signed certificate #. Use certbot_ or a similar software to automatically obtain certificates from `Let's Encrypt`_ #. Buy one from a certificate vendor The first option is probably the easiest, but this requires that any client connecting to your server adds the self signed certificate to their list of trusted certificates. This is of course impractical outside of local development and is strongly discouraged in production use. The second option is nowadays the recommended method, as long as you have an environment where running certbot_ or similar software can automatically replace the certificate with a newer one when necessary, and that you don't need any extra features like class 2 validation. The third option may be your only valid choice when you have special requirements for the certificate that only a certificate vendor can fulfill, or that automatically renewing the certificates is not possible or practical in your environment. .. _certbot: https://certbot.eff.org/ .. _Let's Encrypt: https://letsencrypt.org/ Using self signed certificates ****************************** To create a self signed certificate for ``localhost``, you can use the openssl_ command line tool: .. code-block:: bash openssl req -x509 -newkey rsa:2048 -subj '/CN=localhost' -keyout key.pem -out cert.pem -nodes -days 365 This creates a (2048 bit) private RSA key (``key.pem``) and a certificate (``cert.pem``) matching the host name "localhost". The certificate will be valid for one year with these settings. To set up a server using this key-certificate pair:: import ssl from anyio import create_tcp_listener, run from anyio.streams.tls import TLSListener async def handle(client): async with client: name = await client.receive() await client.send(b'Hello, %s\n' % name) async def main(): # Create a context for the purpose of authenticating clients context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) # Load the server certificate and private key context.load_cert_chain(certfile='cert.pem', keyfile='key.pem') # Create the listener and start serving connections listener = TLSListener(await create_tcp_listener(local_port=1234), context) await listener.serve(handle) run(main) Connecting to this server can then be done as follows:: import ssl from anyio import connect_tcp, run async def main(): # These two steps are only required for certificates that are not trusted by the # installed CA certificates on your machine, so you can skip this part if you # use Let's Encrypt or a commercial certificate vendor context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) context.load_verify_locations(cafile='cert.pem') async with await connect_tcp('localhost', 1234, ssl_context=context) as client: await client.send(b'Client\n') response = await client.receive() print(response) run(main) .. _openssl: https://www.openssl.org/ Creating self-signed certificates on the fly ******************************************** When testing your TLS enabled service, it would be convenient to generate the certificates on the fly. To this end, you can use the trustme_ library:: import ssl import pytest import trustme @pytest.fixture(scope='session') def ca(): return trustme.CA() @pytest.fixture(scope='session') def server_context(ca): server_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ca.issue_cert('localhost').configure_cert(server_context) return server_context @pytest.fixture(scope='session') def client_context(ca): client_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) ca.configure_trust(client_context) return client_context You can then pass the server and client contexts from the above fixtures to :class:`~.streams.tls.TLSListener`, :meth:`~.streams.tls.TLSStream.wrap` or whatever you use on either side. .. _trustme: https://pypi.org/project/trustme/ Dealing with ragged EOFs ************************ According to the `TLS standard`_, encrypted connections should end with a closing handshake. This practice prevents so-called `truncation attacks`_. However, broadly available implementations for protocols such as HTTP, widely ignore this requirement because the protocol level closing signal would make the shutdown handshake redundant. AnyIO follows the standard by default (unlike the Python standard library's :mod:`ssl` module). The practical implication of this is that if you're implementing a protocol that is expected to skip the TLS closing handshake, you need to pass the ``standard_compatible=False`` option to :meth:`~.streams.tls.TLSStream.wrap` or :class:`~.streams.tls.TLSListener`. .. _TLS standard: https://tools.ietf.org/html/draft-ietf-tls-tls13-28 .. _truncation attacks: https://en.wikipedia.org/wiki/Transport_Layer_Security #Attacks_against_TLS/SSL anyio-4.4.0/docs/subprocesses.rst000066400000000000000000000073651462472665400170730ustar00rootroot00000000000000Using subprocesses ================== .. py:currentmodule:: anyio AnyIO allows you to run arbitrary executables in subprocesses, either as a one-shot call or by opening a process handle for you that gives you more control over the subprocess. You can either give the command as a string, in which case it is passed to your default shell (equivalent to ``shell=True`` in :func:`subprocess.run`), or as a sequence of strings (``shell=False``) in which case the executable is the first item in the sequence and the rest are arguments passed to it. Running one-shot commands ------------------------- To run an external command with one call, use :func:`~run_process`:: from anyio import run_process, run async def main(): result = await run_process('ps') print(result.stdout.decode()) run(main) The snippet above runs the ``ps`` command within a shell. To run it directly:: from anyio import run_process, run async def main(): result = await run_process(['ps']) print(result.stdout.decode()) run(main) Working with processes ---------------------- When you have more complex requirements for your interaction with subprocesses, you can launch one with :func:`~open_process`:: from anyio import open_process, run from anyio.streams.text import TextReceiveStream async def main(): async with await open_process(['ps']) as process: async for text in TextReceiveStream(process.stdout): print(text) run(main) See the API documentation of :class:`~.abc.Process` for more information. .. _RunInProcess: Running functions in worker processes ------------------------------------- When you need to run CPU intensive code, worker processes are better than threads because current implementations of Python cannot run Python code in multiple threads at once. Exceptions to this rule are: #. Blocking I/O operations #. C extension code that explicitly releases the Global Interpreter Lock If the code you wish to run does not belong in this category, it's best to use worker processes instead in order to take advantage of multiple CPU cores. This is done by using :func:`.to_process.run_sync`:: import time from anyio import run, to_process def cpu_intensive_function(arg1, arg2): time.sleep(1) return arg1 + arg2 async def main(): result = await to_process.run_sync(cpu_intensive_function, 'Hello, ', 'world!') print(result) # This check is important when the application uses run_sync_in_process() if __name__ == '__main__': run(main) Technical details ***************** There are some limitations regarding the arguments and return values passed: * the arguments must be pickleable (using the highest available protocol) * the return value must be pickleable (using the highest available protocol) * the target callable must be importable (lambdas and inner functions won't work) Other considerations: * Even ``cancellable=False`` runs can be cancelled before the request has been sent to the worker process * If a cancellable call is cancelled during execution on the worker process, the worker process will be killed * The worker process imports the parent's ``__main__`` module, so guarding for any import time side effects using ``if __name__ == '__main__':`` is required to avoid infinite recursion * ``sys.stdin`` and ``sys.stdout``, ``sys.stderr`` are redirected to ``/dev/null`` so :func:`print` and :func:`input` won't work * Worker processes terminate after 5 minutes of inactivity, or when the event loop is finished * On asyncio, either :func:`asyncio.run` or :func:`anyio.run` must be used for proper cleanup to happen * Multiprocessing-style synchronization primitives are currently not available anyio-4.4.0/docs/support.rst000066400000000000000000000013421462472665400160540ustar00rootroot00000000000000Getting help ============ If you are having trouble with AnyIO, make sure you've first checked the :doc:`FAQ ` to see if your question is answered there. If not, you have a couple ways for getting support: * Post a question on `Stack Overflow`_ and use the ``anyio`` tag * Join the `python-trio/AnyIO`_ room on Gitter .. _Stack Overflow: https://stackoverflow.com/ .. _python-trio/AnyIO: https://gitter.im/python-trio/AnyIO Reporting bugs ============== If you're fairly certain that you have discovered a bug, you can `file an issue`_ on Github. If you feel unsure, come talk to us first! The issue tracker is **not** the proper venue for asking support questions. .. _file an issue: https://github.com/agronholm/anyio/issues anyio-4.4.0/docs/synchronization.rst000066400000000000000000000126421462472665400176060ustar00rootroot00000000000000Using synchronization primitives ================================ .. py:currentmodule:: anyio Synchronization primitives are objects that are used by tasks to communicate and coordinate with each other. They are useful for things like distributing workload, notifying other tasks and guarding access to shared resources. .. note:: AnyIO primitives are not thread-safe, therefore they should not be used directly from worker threads. Use :func:`~from_thread.run_sync` for that. Events ------ Events are used to notify tasks that something they've been waiting to happen has happened. An event object can have multiple listeners and they are all notified when the event is triggered. Example:: from anyio import Event, create_task_group, run async def notify(event): event.set() async def main(): event = Event() async with create_task_group() as tg: tg.start_soon(notify, event) await event.wait() print('Received notification!') run(main) .. note:: Unlike standard library Events, AnyIO events cannot be reused, and must be replaced instead. This practice prevents a class of race conditions, and matches the semantics of the Trio library. Semaphores ---------- Semaphores are used for limiting access to a shared resource. A semaphore starts with a maximum value, which is decremented each time the semaphore is acquired by a task and incremented when it is released. If the value drops to zero, any attempt to acquire the semaphore will block until another task frees it. Example:: from anyio import Semaphore, create_task_group, sleep, run async def use_resource(tasknum, semaphore): async with semaphore: print('Task number', tasknum, 'is now working with the shared resource') await sleep(1) async def main(): semaphore = Semaphore(2) async with create_task_group() as tg: for num in range(10): tg.start_soon(use_resource, num, semaphore) run(main) Locks ----- Locks are used to guard shared resources to ensure sole access to a single task at once. They function much like semaphores with a maximum value of 1, except that only the task that acquired the lock is allowed to release it. Example:: from anyio import Lock, create_task_group, sleep, run async def use_resource(tasknum, lock): async with lock: print('Task number', tasknum, 'is now working with the shared resource') await sleep(1) async def main(): lock = Lock() async with create_task_group() as tg: for num in range(4): tg.start_soon(use_resource, num, lock) run(main) Conditions ---------- A condition is basically a combination of an event and a lock. It first acquires a lock and then waits for a notification from the event. Once the condition receives a notification, it releases the lock. The notifying task can also choose to wake up more than one listener at once, or even all of them. Like :class:`Lock`, :class:`Condition` also requires that the task which locked it also the one to release it. Example:: from anyio import Condition, create_task_group, sleep, run async def listen(tasknum, condition): async with condition: await condition.wait() print('Woke up task number', tasknum) async def main(): condition = Condition() async with create_task_group() as tg: for tasknum in range(6): tg.start_soon(listen, tasknum, condition) await sleep(1) async with condition: condition.notify(1) await sleep(1) async with condition: condition.notify(2) await sleep(1) async with condition: condition.notify_all() run(main) Capacity limiters ----------------- Capacity limiters are like semaphores except that a single borrower (the current task by default) can only hold a single token at a time. It is also possible to borrow a token on behalf of any arbitrary object, so long as that object is hashable. Example:: from anyio import CapacityLimiter, create_task_group, sleep, run async def use_resource(tasknum, limiter): async with limiter: print('Task number', tasknum, 'is now working with the shared resource') await sleep(1) async def main(): limiter = CapacityLimiter(2) async with create_task_group() as tg: for num in range(10): tg.start_soon(use_resource, num, limiter) run(main) You can adjust the total number of tokens by setting a different value on the limiter's ``total_tokens`` property. Resource guards --------------- Some resources, such as sockets, are very sensitive about concurrent use and should not allow even attempts to be used concurrently. For such cases, :class:`ResourceGuard` is the appropriate solution:: class Resource: def __init__(self): self._guard = ResourceGuard() async def do_something() -> None: with self._guard: ... Now, if another task tries calling the ``do_something()`` method on the same ``Resource`` instance before the first call has finished, that will raise a :exc:`BusyResourceError`. Queues ------ In place of queues, AnyIO offers a more powerful construct: :ref:`memory object streams `. anyio-4.4.0/docs/tasks.rst000066400000000000000000000160211462472665400154650ustar00rootroot00000000000000Creating and managing tasks =========================== .. py:currentmodule:: anyio A *task* is a unit of execution that lets you do many things concurrently that need waiting on. This works so that while you can have any number of tasks, the asynchronous event loop can only run one of them at a time. When the task encounters an ``await`` statement that requires the task to sleep until something happens, the event loop is then free to work on another task. When the thing the first task was waiting is complete, the event loop will resume the execution of that task on the first opportunity it gets. Task handling in AnyIO loosely follows the Trio_ model. Tasks can be created (*spawned*) using *task groups*. A task group is an asynchronous context manager that makes sure that all its child tasks are finished one way or another after the context block is exited. If a child task, or the code in the enclosed context block raises an exception, all child tasks are cancelled. Otherwise the context manager just waits until all child tasks have exited before proceeding. Here's a demonstration:: from anyio import sleep, create_task_group, run async def sometask(num: int) -> None: print('Task', num, 'running') await sleep(1) print('Task', num, 'finished') async def main() -> None: async with create_task_group() as tg: for num in range(5): tg.start_soon(sometask, num) print('All tasks finished!') run(main) .. _Trio: https://trio.readthedocs.io/en/latest/reference-core.html #tasks-let-you-do-multiple-things-at-once Starting and initializing tasks ------------------------------- Sometimes it is very useful to be able to wait until a task has successfully initialized itself. For example, when starting network services, you can have your task start the listener and then signal the caller that initialization is done. That way, the caller can now start another task that depends on that service being up and running. Also, if the socket bind fails or something else goes wrong during initialization, the exception will be propagated to the caller which can then catch and handle it. This can be done with :meth:`TaskGroup.start() <.abc.TaskGroup.start>`:: from anyio import ( TASK_STATUS_IGNORED, create_task_group, connect_tcp, create_tcp_listener, run, ) from anyio.abc import TaskStatus async def handler(stream): ... async def start_some_service( port: int, *, task_status: TaskStatus[None] = TASK_STATUS_IGNORED ): async with await create_tcp_listener( local_host="127.0.0.1", local_port=port ) as listener: task_status.started() await listener.serve(handler) async def main(): async with create_task_group() as tg: await tg.start(start_some_service, 5000) async with await connect_tcp("127.0.0.1", 5000) as stream: ... run(main) The target coroutine function **must** call ``task_status.started()`` because the task that is calling with :meth:`TaskGroup.start() <.abc.TaskGroup.start>` will be blocked until then. If the spawned task never calls it, then the :meth:`TaskGroup.start() <.abc.TaskGroup.start>` call will raise a ``RuntimeError``. .. note:: Unlike :meth:`~.abc.TaskGroup.start_soon`, :meth:`~.abc.TaskGroup.start` needs an ``await``. Handling multiple errors in a task group ---------------------------------------- It is possible for more than one task to raise an exception in a task group. This can happen when a task reacts to cancellation by entering either an exception handler block or a ``finally:`` block and raises an exception there. This raises the question: which exception is propagated from the task group context manager? The answer is "both". In practice this means that a special exception, :exc:`ExceptionGroup` (or :exc:`BaseExceptionGroup`) is raised which contains both exception objects. To catch such exceptions potentially nested in groups, special measures are required. On Python 3.11 and later, you can use the ``except*`` syntax to catch multiple exceptions:: from anyio import create_task_group try: async with create_task_group() as tg: tg.start_soon(some_task) tg.start_soon(another_task) except* ValueError as excgroup: for exc in excgroup.exceptions: ... # handle each ValueError except* KeyError as excgroup: for exc in excgroup.exceptions: ... # handle each KeyError If compatibility with older Python versions is required, you can use the ``catch()`` function from the exceptiongroup_ package:: from anyio import create_task_group from exceptiongroup import catch def handle_valueerror(excgroup: ExceptionGroup) -> None: for exc in excgroup.exceptions: ... # handle each ValueError def handle_keyerror(excgroup: ExceptionGroup) -> None: for exc in excgroup.exceptions: ... # handle each KeyError with catch({ ValueError: handle_valueerror, KeyError: handle_keyerror }): async with create_task_group() as tg: tg.start_soon(some_task) tg.start_soon(another_task) If you need to set local variables in the handlers, declare them as ``nonlocal``:: def handle_valueerror(exc): nonlocal somevariable somevariable = 'whatever' .. _exceptiongroup: https://pypi.org/project/exceptiongroup/ Context propagation ------------------- Whenever a new task is spawned, `context`_ will be copied to the new task. It is important to note *which* context will be copied to the newly spawned task. It is not the context of the task group's host task that will be copied, but the context of the task that calls :meth:`TaskGroup.start() <.abc.TaskGroup.start>` or :meth:`TaskGroup.start_soon() <.abc.TaskGroup.start_soon>`. .. _context: https://docs.python.org/3/library/contextvars.html Differences with asyncio.TaskGroup ---------------------------------- The :class:`asyncio.TaskGroup` class, added in Python 3.11, is very similar in design to the AnyIO :class:`~.abc.TaskGroup` class. The asyncio counterpart has some important differences in its semantics, however: * The task group itself is instantiated directly, rather than using a factory function * Tasks are spawned solely through :meth:`~asyncio.TaskGroup.create_task`; there is no ``start()`` or ``start_soon()`` method * The :meth:`~asyncio.TaskGroup.create_task` method returns a task object which can be awaited on (or cancelled) * Tasks spawned via :meth:`~asyncio.TaskGroup.create_task` can only be cancelled individually (there is no ``cancel()`` method or similar in the task group) * When a task spawned via :meth:`~asyncio.TaskGroup.create_task` is cancelled before its coroutine has started running, it will not get a chance to handle the cancellation exception * :class:`asyncio.TaskGroup` does not allow starting new tasks after an exception in one of the tasks has triggered a shutdown of the task group anyio-4.4.0/docs/testing.rst000066400000000000000000000144421462472665400160220ustar00rootroot00000000000000Testing with AnyIO ================== AnyIO provides built-in support for testing your library or application in the form of a pytest_ plugin. .. _pytest: https://docs.pytest.org/en/latest/ Creating asynchronous tests --------------------------- Pytest does not natively support running asynchronous test functions, so they have to be marked for the AnyIO pytest plugin to pick them up. This can be done in one of two ways: #. Using the ``pytest.mark.anyio`` marker #. Using the ``anyio_backend`` fixture, either directly or via another fixture The simplest way is thus the following:: import pytest # This is the same as using the @pytest.mark.anyio on all test functions in the module pytestmark = pytest.mark.anyio async def test_something(): ... Marking modules, classes or functions with this marker has the same effect as applying the ``pytest.mark.usefixtures('anyio_backend')`` on them. Thus, you can also require the fixture directly in your tests and fixtures:: import pytest async def test_something(anyio_backend): ... Specifying the backends to run on --------------------------------- The ``anyio_backend`` fixture determines the backends and their options that tests and fixtures are run with. The AnyIO pytest plugin comes with a function scoped fixture with this name which runs everything on all supported backends. If you change the backends/options for the entire project, then put something like this in your top level ``conftest.py``:: @pytest.fixture def anyio_backend(): return 'asyncio' If you want to specify different options for the selected backend, you can do so by passing a tuple of (backend name, options dict):: @pytest.fixture(params=[ pytest.param(('asyncio', {'use_uvloop': True}), id='asyncio+uvloop'), pytest.param(('asyncio', {'use_uvloop': False}), id='asyncio'), pytest.param(('trio', {'restrict_keyboard_interrupt_to_checkpoints': True}), id='trio') ]) def anyio_backend(request): return request.param If you need to run a single test on a specific backend, you can use ``@pytest.mark.parametrize`` (remember to add the ``anyio_backend`` parameter to the actual test function, or pytest will complain):: @pytest.mark.parametrize('anyio_backend', ['asyncio']) async def test_on_asyncio_only(anyio_backend): ... Because the ``anyio_backend`` fixture can return either a string or a tuple, there are two additional function-scoped fixtures (which themselves depend on the ``anyio_backend`` fixture) provided for your convenience: * ``anyio_backend_name``: the name of the backend (e.g. ``asyncio``) * ``anyio_backend_options``: the dictionary of option keywords used to run the backend Asynchronous fixtures --------------------- The plugin also supports coroutine functions as fixtures, for the purpose of setting up and tearing down asynchronous services used for tests. There are two ways to get the AnyIO pytest plugin to run your asynchronous fixtures: #. Use them in AnyIO enabled tests (see the first section) #. Use the ``anyio_backend`` fixture (or any other fixture using it) in the fixture itself The simplest way is using the first option:: import pytest pytestmark = pytest.mark.anyio @pytest.fixture async def server(): server = await setup_server() yield server await server.shutdown() async def test_server(server): result = await server.do_something() assert result == 'foo' For ``autouse=True`` fixtures, you may need to use the other approach:: @pytest.fixture(autouse=True) async def server(anyio_backend): server = await setup_server() yield await server.shutdown() async def test_server(): result = await client.do_something_on_the_server() assert result == 'foo' Using async fixtures with higher scopes --------------------------------------- For async fixtures with scopes other than ``function``, you will need to define your own ``anyio_backend`` fixture because the default ``anyio_backend`` fixture is function scoped:: @pytest.fixture(scope='module') def anyio_backend(): return 'asyncio' @pytest.fixture(scope='module') async def server(anyio_backend): server = await setup_server() yield await server.shutdown() Technical details ----------------- The fixtures and tests are run by a "test runner", implemented separately for each backend. The test runner keeps an event loop open during the request, making it possible for code in fixtures to communicate with the code in the tests (and each other). The test runner is created when the first matching async test or fixture is about to be run, and shut down when that same fixture is being torn down or the test has finished running. As such, if no higher-order (scoped ``class`` or higher) async fixtures are used, a separate test runner is created for each matching test. Conversely, if even one async fixture, scoped higher than ``function``, is shared across all tests, only one test runner will be created during the test session. Context variable propagation ++++++++++++++++++++++++++++ The asynchronous test runner runs all async fixtures and tests in the same task, so context variables set in async fixtures or tests, within an async test runner, will affect other async fixtures and tests within the same runner. However, these context variables are **not** carried over to synchronous tests and fixtures, or to other async test runners. Comparison with other async test runners ++++++++++++++++++++++++++++++++++++++++ The ``pytest-asyncio`` library only works with asyncio code. Like the AnyIO pytest plugin, it can be made to support higher order fixtures (by specifying a higher order ``event_loop`` fixture). However, it runs the setup and teardown phases of each async fixture in a new async task per operation, making context variable propagation impossible and preventing task groups and cancel scopes from functioning properly. The ``pytest-trio`` library, made for testing Trio projects, works only with Trio code. Additionally, it only supports function scoped async fixtures. Another significant difference with the AnyIO pytest plugin is that attempts to run the setup and teardown for async fixtures concurrently when their dependency graphs allow that. anyio-4.4.0/docs/threads.rst000066400000000000000000000204741462472665400160010ustar00rootroot00000000000000Working with threads ==================== .. py:currentmodule:: anyio Practical asynchronous applications occasionally need to run network, file or computationally expensive operations. Such operations would normally block the asynchronous event loop, leading to performance issues. The solution is to run such code in *worker threads*. Using worker threads lets the event loop continue running other tasks while the worker thread runs the blocking call. Running a function in a worker thread ------------------------------------- To run a (synchronous) callable in a worker thread:: import time from anyio import to_thread, run async def main(): await to_thread.run_sync(time.sleep, 5) run(main) By default, tasks are shielded from cancellation while they are waiting for a worker thread to finish. You can pass the ``cancellable=True`` parameter to allow such tasks to be cancelled. Note, however, that the thread will still continue running – only its outcome will be ignored. .. seealso:: :ref:`RunInProcess` Calling asynchronous code from a worker thread ---------------------------------------------- If you need to call a coroutine function from a worker thread, you can do this:: from anyio import from_thread, sleep, to_thread, run def blocking_function(): from_thread.run(sleep, 5) async def main(): await to_thread.run_sync(blocking_function) run(main) .. note:: The worker thread must have been spawned using :func:`~to_thread.run_sync` for this to work. Calling synchronous code from a worker thread --------------------------------------------- Occasionally you may need to call synchronous code in the event loop thread from a worker thread. Common cases include setting asynchronous events or sending data to a memory object stream. Because these methods aren't thread safe, you need to arrange them to be called inside the event loop thread using :func:`~from_thread.run_sync`:: import time from anyio import Event, from_thread, to_thread, run def worker(event): time.sleep(1) from_thread.run_sync(event.set) async def main(): event = Event() await to_thread.run_sync(worker, event) await event.wait() run(main) Calling asynchronous code from an external thread ------------------------------------------------- If you need to run async code from a thread that is not a worker thread spawned by the event loop, you need a *blocking portal*. This needs to be obtained from within the event loop thread. One way to do this is to start a new event loop with a portal, using :class:`~from_thread.start_blocking_portal` (which takes mostly the same arguments as :func:`~run`:: from anyio.from_thread import start_blocking_portal with start_blocking_portal(backend='trio') as portal: portal.call(...) If you already have an event loop running and wish to grant access to external threads, you can create a :class:`~.BlockingPortal` directly:: from anyio import run from anyio.from_thread import BlockingPortal async def main(): async with BlockingPortal() as portal: # ...hand off the portal to external threads... await portal.sleep_until_stopped() run(main) Spawning tasks from worker threads ---------------------------------- When you need to spawn a task to be run in the background, you can do so using :meth:`~.BlockingPortal.start_task_soon`:: from concurrent.futures import as_completed from anyio import sleep from anyio.from_thread import start_blocking_portal async def long_running_task(index): await sleep(1) print(f'Task {index} running...') await sleep(index) return f'Task {index} return value' with start_blocking_portal() as portal: futures = [portal.start_task_soon(long_running_task, i) for i in range(1, 5)] for future in as_completed(futures): print(future.result()) Cancelling tasks spawned this way can be done by cancelling the returned :class:`~concurrent.futures.Future`. Blocking portals also have a method similar to :meth:`TaskGroup.start() <.abc.TaskGroup.start>`: :meth:`~.BlockingPortal.start_task` which, like its counterpart, waits for the callable to signal readiness by calling ``task_status.started()``:: from anyio import sleep, TASK_STATUS_IGNORED from anyio.from_thread import start_blocking_portal async def service_task(*, task_status=TASK_STATUS_IGNORED): task_status.started('STARTED') await sleep(1) return 'DONE' with start_blocking_portal() as portal: future, start_value = portal.start_task(service_task) print('Task has started with value', start_value) return_value = future.result() print('Task has finished with return value', return_value) Using asynchronous context managers from worker threads ------------------------------------------------------- You can use :meth:`~.BlockingPortal.wrap_async_context_manager` to wrap an asynchronous context managers as a synchronous one:: from anyio.from_thread import start_blocking_portal class AsyncContextManager: async def __aenter__(self): print('entering') async def __aexit__(self, exc_type, exc_val, exc_tb): print('exiting with', exc_type) async_cm = AsyncContextManager() with start_blocking_portal() as portal, portal.wrap_async_context_manager(async_cm): print('inside the context manager block') .. note:: You cannot use wrapped async context managers in synchronous callbacks inside the event loop thread. Context propagation ------------------- When running functions in worker threads, the current context is copied to the worker thread. Therefore any context variables available on the task will also be available to the code running on the thread. As always with context variables, any changes made to them will not propagate back to the calling asynchronous task. When calling asynchronous code from worker threads, context is again copied to the task that calls the target function in the event loop thread. Adjusting the default maximum worker thread count ------------------------------------------------- The default AnyIO worker thread limiter has a value of **40**, meaning that any calls to :func:`.to_thread.run_sync` without an explicit ``limiter`` argument will cause a maximum of 40 threads to be spawned. You can adjust this limit like this:: from anyio import to_thread async def foo(): # Set the maximum number of worker threads to 60 to_thread.current_default_thread_limiter().total_tokens = 60 .. note:: AnyIO's default thread pool limiter does not affect the default thread pool executor on :mod:`asyncio`. Reacting to cancellation in worker threads ------------------------------------------ While there is no mechanism in Python to cancel code running in a thread, AnyIO provides a mechanism that allows user code to voluntarily check if the host task's scope has been cancelled, and if it has, raise a cancellation exception. This can be done by simply calling :func:`from_thread.check_cancelled`:: from anyio import to_thread, from_thread def sync_function(): while True: from_thread.check_cancelled() print("Not cancelled yet") sleep(1) async def foo(): with move_on_after(3): await to_thread.run_sync(sync_function) Sharing a blocking portal on demand ----------------------------------- If you're building a synchronous API that needs to start a blocking portal on demand, you might need a more efficient solution than just starting a blocking portal for each call. To that end, you can use :class:`BlockingPortalProvider`:: from anyio.to_thread import BlockingPortalProvider class MyAPI: def __init__(self, async_obj) -> None: self._async_obj = async_obj self._portal_provider = BlockingPortalProvider() def do_stuff(self) -> None: with self._portal_provider as portal: portal.call(async_obj.do_async_stuff) Now, no matter how many threads call the ``do_stuff()`` method on a ``MyAPI`` instance at the same time, the same blocking portal will be used to handle the async calls inside. It's easy to see that this is much more efficient than having each call spawn its own blocking portal. anyio-4.4.0/docs/typedattrs.rst000066400000000000000000000053201462472665400165430ustar00rootroot00000000000000Using typed attributes ====================== .. py:currentmodule:: anyio On AnyIO, streams and listeners can be layered on top of each other to provide extra functionality. But when you want to look up information from one of the layers down below, you might have to traverse the entire chain to find what you're looking for, which is highly inconvenient. To address this, AnyIO has a system of *typed attributes* where you can look for a specific attribute by its unique key. If a stream or listener wrapper does not have the attribute you're looking for, it will look it up in the wrapped instance, and that wrapper can look in its wrapped instance and so on, until the attribute is either found or the end of the chain is reached. This also lets wrappers override attributes from the wrapped objects when necessary. A common use case is finding the IP address of the remote side of a TCP connection when the stream may be either :class:`~.abc.SocketStream` or :class:`~.streams.tls.TLSStream`:: from anyio import connect_tcp from anyio.abc import SocketAttribute async def connect(host, port, tls: bool): stream = await connect_tcp(host, port, tls=tls) print('Connected to', stream.extra(SocketAttribute.remote_address)) Each typed attribute provider class should document the set of attributes it provides on its own. Defining your own typed attributes ---------------------------------- By convention, typed attributes are stored together in a container class with other attributes of the same category:: from anyio import TypedAttributeSet, typed_attribute class MyTypedAttribute(TypedAttributeSet): string_valued_attribute: str = typed_attribute() some_float_attribute: float = typed_attribute() To provide values for these attributes, implement the :meth:`~.TypedAttributeProvider.extra_attributes` property in your class:: from collections.abc import Callable, Mapping from anyio import TypedAttributeProvider class MyAttributeProvider(TypedAttributeProvider): @property def extra_attributes() -> Mapping[Any, Callable[[], Any]]: return { MyTypedAttribute.string_valued_attribute: lambda: 'my attribute value', MyTypedAttribute.some_float_attribute: lambda: 6.492 } If your class inherits from another typed attribute provider, make sure you include its attributes in the return value:: class AnotherAttributeProvider(MyAttributeProvider): @property def extra_attributes() -> Mapping[Any, Callable[[], Any]]: return { **super().extra_attributes, MyTypedAttribute.string_valued_attribute: lambda: 'overridden attribute value' } anyio-4.4.0/docs/versionhistory.rst000066400000000000000000001334211462472665400174530ustar00rootroot00000000000000Version history =============== This library adheres to `Semantic Versioning 2.0 `_. **4.4.0** - Added the ``BlockingPortalProvider`` class to aid with constructing synchronous counterparts to asynchronous interfaces that would otherwise require multiple blocking portals - Added ``__slots__`` to ``AsyncResource`` so that child classes can use ``__slots__`` (`#733 `_; PR by Justin Su) - Added the ``TaskInfo.has_pending_cancellation()`` method - Fixed erroneous ``RuntimeError: called 'started' twice on the same task status`` when cancelling a task in a TaskGroup created with the ``start()`` method before the first checkpoint is reached after calling ``task_status.started()`` (`#706 `_; PR by Dominik Schwabe) - Fixed two bugs with ``TaskGroup.start()`` on asyncio: * Fixed erroneous ``RuntimeError: called 'started' twice on the same task status`` when cancelling a task in a TaskGroup created with the ``start()`` method before the first checkpoint is reached after calling ``task_status.started()`` (`#706 `_; PR by Dominik Schwabe) * Fixed the entire task group being cancelled if a ``TaskGroup.start()`` call gets cancelled (`#685 `_, `#710 `_) - Fixed a race condition that caused crashes when multiple event loops of the same backend were running in separate threads and simultaneously attempted to use AnyIO for their first time (`#425 `_; PR by David Jiricek and Ganden Schaffner) - Fixed cancellation delivery on asyncio incrementing the wrong cancel scope's cancellation counter when cascading a cancel operation to a child scope, thus failing to uncancel the host task (`#716 `_) - Fixed erroneous ``TypedAttributeLookupError`` if a typed attribute getter raises ``KeyError`` - Fixed the asyncio backend not respecting the ``PYTHONASYNCIODEBUG`` environment variable when setting the ``debug`` flag in ``anyio.run()`` - Fixed ``SocketStream.receive()`` not detecting EOF on asyncio if there is also data in the read buffer (`#701 `_) - Fixed ``MemoryObjectStream`` dropping an item if the item is delivered to a recipient that is waiting to receive an item but has a cancellation pending (`#728 `_) - Emit a ``ResourceWarning`` for ``MemoryObjectReceiveStream`` and ``MemoryObjectSendStream`` that were garbage collected without being closed (PR by Andrey Kazantcev) - Fixed ``MemoryObjectSendStream.send()`` not raising ``BrokenResourceError`` when the last corresponding ``MemoryObjectReceiveStream`` is closed while waiting to send a falsey item (`#731 `_; PR by Ganden Schaffner) **4.3.0** - Added support for the Python 3.12 ``walk_up`` keyword argument in ``anyio.Path.relative_to()`` (PR by Colin Taylor) - Fixed passing ``total_tokens`` to ``anyio.CapacityLimiter()`` as a keyword argument not working on the ``trio`` backend (`#515 `_) - Fixed ``Process.aclose()`` not performing the minimum level of necessary cleanup when cancelled. Previously: - Cancellation of ``Process.aclose()`` could leak an orphan process - Cancellation of ``run_process()`` could very briefly leak an orphan process. - Cancellation of ``Process.aclose()`` or ``run_process()`` on Trio could leave standard streams unclosed (PR by Ganden Schaffner) - Fixed ``Process.stdin.aclose()``, ``Process.stdout.aclose()``, and ``Process.stderr.aclose()`` not including a checkpoint on asyncio (PR by Ganden Schaffner) - Fixed documentation on how to provide your own typed attributes **4.2.0** - Add support for ``byte``-based paths in ``connect_unix``, ``create_unix_listeners``, ``create_unix_datagram_socket``, and ``create_connected_unix_datagram_socket``. (PR by Lura Skye) - Enabled the ``Event`` and ``CapacityLimiter`` classes to be instantiated outside an event loop thread - Broadly improved/fixed the type annotations. Among other things, many functions and methods that take variadic positional arguments now make use of PEP 646 ``TypeVarTuple`` to allow the positional arguments to be validated by static type checkers. These changes affected numerous methods and functions, including: * ``anyio.run()`` * ``TaskGroup.start_soon()`` * ``anyio.from_thread.run()`` * ``anyio.from_thread.run_sync()`` * ``anyio.to_thread.run_sync()`` * ``anyio.to_process.run_sync()`` * ``BlockingPortal.call()`` * ``BlockingPortal.start_task_soon()`` * ``BlockingPortal.start_task()`` (also resolves `#560 `_) - Fixed various type annotations of ``anyio.Path`` to match Typeshed: * ``anyio.Path.__lt__()`` * ``anyio.Path.__le__()`` * ``anyio.Path.__gt__()`` * ``anyio.Path.__ge__()`` * ``anyio.Path.__truediv__()`` * ``anyio.Path.__rtruediv__()`` * ``anyio.Path.hardlink_to()`` * ``anyio.Path.samefile()`` * ``anyio.Path.symlink_to()`` * ``anyio.Path.with_segments()`` (PR by Ganden Schaffner) - Fixed adjusting the total number of tokens in a ``CapacityLimiter`` on asyncio failing to wake up tasks waiting to acquire the limiter in certain edge cases (fixed with help from Egor Blagov) - Fixed ``loop_factory`` and ``use_uvloop`` options not being used on the asyncio backend (`#643 `_) - Fixed cancellation propagating on asyncio from a task group to child tasks if the task hosting the task group is in a shielded cancel scope (`#642 `_) **4.1.0** - Adapted to API changes made in Trio v0.23: - Call ``trio.to_thread.run_sync()`` using the ``abandon_on_cancel`` keyword argument instead of ``cancellable`` - Removed a checkpoint when exiting a task group - Renamed the ``cancellable`` argument in ``anyio.to_thread.run_sync()`` to ``abandon_on_cancel`` (and deprecated the old parameter name) - Bumped minimum version of Trio to v0.23 - Added support for voluntary thread cancellation via ``anyio.from_thread.check_cancelled()`` - Bumped minimum version of trio to v0.23 - Exposed the ``ResourceGuard`` class in the public API (`#627 `_) - Fixed ``RuntimeError: Runner is closed`` when running higher-scoped async generator fixtures in some cases (`#619 `_) - Fixed discrepancy between ``asyncio`` and ``trio`` where reraising a cancellation exception in an ``except*`` block would incorrectly bubble out of its cancel scope (`#634 `_) **4.0.0** - **BACKWARDS INCOMPATIBLE** Replaced AnyIO's own ``ExceptionGroup`` class with the PEP 654 ``BaseExceptionGroup`` and ``ExceptionGroup`` - **BACKWARDS INCOMPATIBLE** Changes to cancellation semantics: - Any exceptions raising out of a task groups are now nested inside an ``ExceptionGroup`` (or ``BaseExceptionGroup`` if one or more ``BaseException`` were included) - Fixed task group not raising a cancellation exception on asyncio at exit if no child tasks were spawned and an outer cancellation scope had been cancelled before - Ensured that exiting a ``TaskGroup`` always hits a yield point, regardless of whether there are running child tasks to be waited on - On asyncio, cancel scopes will defer cancelling tasks that are scheduled to resume with a finished future - On asyncio and Python 3.9/3.10, cancel scopes now only suppress cancellation exceptions if the cancel message matches the scope - Task groups on all backends now raise a single cancellation exception when an outer cancel scope is cancelled, and no exceptions other than cancellation exceptions are raised in the group - **BACKWARDS INCOMPATIBLE** Changes the pytest plugin to run all tests and fixtures in the same task, allowing fixtures to set context variables for tests and other fixtures - **BACKWARDS INCOMPATIBLE** Changed ``anyio.Path.relative_to()`` and ``anyio.Path.is_relative_to()`` to only accept one argument, as passing multiple arguments is deprecated as of Python 3.12 - **BACKWARDS INCOMPATIBLE** Dropped support for spawning tasks from old-style coroutine functions (``@asyncio.coroutine``) - **BACKWARDS INCOMPATIBLE** The ``policy`` option on the ``asyncio`` backend was changed to ``loop_factory`` to accommodate ``asyncio.Runner`` - Changed ``anyio.run()`` to use ``asyncio.Runner`` (or a back-ported version of it on Pythons older than 3.11) on the ``asyncio`` backend - Dropped support for Python 3.7 - Added support for Python 3.12 - Bumped minimum version of trio to v0.22 - Added the ``anyio.Path.is_junction()`` and ``anyio.Path.walk()`` methods - Added ``create_unix_datagram_socket`` and ``create_connected_unix_datagram_socket`` to create UNIX datagram sockets (PR by Jean Hominal) - Fixed ``from_thread.run`` and ``from_thread.run_sync`` not setting sniffio on asyncio. As a result: - Fixed ``from_thread.run_sync`` failing when used to call sniffio-dependent functions on asyncio - Fixed ``from_thread.run`` failing when used to call sniffio-dependent functions on asyncio from a thread running trio or curio - Fixed deadlock when using ``from_thread.start_blocking_portal(backend="asyncio")`` in a thread running trio or curio (PR by Ganden Schaffner) - Improved type annotations: - The ``item_type`` argument of ``create_memory_object_stream`` was deprecated. To indicate the item type handled by the stream, use ``create_memory_object_stream[T_Item]()`` instead. Type checking should no longer fail when annotating memory object streams with uninstantiable item types (PR by Ganden Schaffner) - Added the ``CancelScope.cancelled_caught`` property which tells users if the cancel scope suppressed a cancellation exception - Fixed ``fail_after()`` raising an unwarranted ``TimeoutError`` when the cancel scope was cancelled before reaching its deadline - Fixed ``MemoryObjectReceiveStream.receive()`` causing the receiving task on asyncio to remain in a cancelled state if the operation was cancelled after an item was queued to be received by the task (but before the task could actually receive the item) - Fixed ``TaskGroup.start()`` on asyncio not responding to cancellation from the outside - Fixed tasks started from ``BlockingPortal`` not notifying synchronous listeners (``concurrent.futures.wait()``) when they're cancelled - Removed unnecessary extra waiting cycle in ``Event.wait()`` on asyncio in the case where the event was not yet set - Fixed processes spawned by ``anyio.to_process()`` being "lost" as unusable to the process pool when processes that have idled over 5 minutes are pruned at part of the ``to_process.run_sync()`` call, leading to increased memory consumption (PR by Anael Gorfinkel) Changes since 4.0.0rc1: - Fixed the type annotation of ``TaskGroup.start_soon()`` to accept any awaitables (already in v3.7.0 but was missing from 4.0.0rc1) - Changed ``CancelScope`` to also consider the cancellation count (in addition to the cancel message) on asyncio to determine if a cancellation exception should be swallowed on scope exit, to combat issues where third party libraries catch the ``CancelledError`` and raise another, thus erasing the original cancel message - Worked around a `CPython bug `_ that caused ``TLSListener.handle_handshake_error()`` on asyncio to log ``"NoneType: None"`` instead of the error (PR by Ganden Schaffner) - Re-added the ``item_type`` argument to ``create_memory_object_stream()`` (but using it raises a deprecation warning and does nothing with regards to the static types of the returned streams) - Fixed processes spawned by ``anyio.to_process()`` being "lost" as unusable to the process pool when processes that have idled over 5 minutes are pruned at part of the ``to_process.run_sync()`` call, leading to increased memory consumption (PR by Anael Gorfinkel) **3.7.1** - Fixed sending large buffers via UNIX stream sockets on asyncio - Fixed several minor documentation issues (broken links to classes, missing classes or attributes) **3.7.0** - Dropped support for Python 3.6 - Improved type annotations: - Several functions and methods that were previously annotated as accepting ``Coroutine[Any, Any, Any]`` as the return type of the callable have been amended to accept ``Awaitable[Any]`` instead, to allow a slightly broader set of coroutine-like inputs, like ``async_generator_asend`` objects returned from the ``asend()`` method of async generators, and to match the ``trio`` annotations: - ``anyio.run()`` - ``anyio.from_thread.run()`` - ``TaskGroup.start_soon()`` - ``TaskGroup.start()`` - ``BlockingPortal.call()`` - ``BlockingPortal.start_task_soon()`` - ``BlockingPortal.start_task()`` Note that this change involved only changing the type annotations; run-time functionality was not altered. - The ``TaskStatus`` class is now a generic protocol, and should be parametrized to indicate the type of the value passed to ``task_status.started()`` - The ``Listener`` class is now covariant in its stream type - ``create_memory_object_stream()`` now allows passing only ``item_type`` - Object receive streams are now covariant and object send streams are correspondingly contravariant - Changed ``TLSAttribute.shared_ciphers`` to match the documented semantics of ``SSLSocket.shared_ciphers`` of always returning ``None`` for client-side streams - Fixed ``CapacityLimiter`` on the asyncio backend to order waiting tasks in the FIFO order (instead of LIFO) (PR by Conor Stevenson) - Fixed ``CancelScope.cancel()`` not working on asyncio if called before entering the scope - Fixed ``open_signal_receiver()`` inconsistently yielding integers instead of ``signal.Signals`` instances on the ``trio`` backend - Fixed ``to_thread.run_sync()`` hanging on asyncio if the target callable raises ``StopIteration`` - Fixed ``start_blocking_portal()`` raising an unwarranted ``RuntimeError: This portal is not running`` if a task raises an exception that causes the event loop to be closed - Fixed ``current_effective_deadline()`` not returning ``-inf`` on asyncio when the currently active cancel scope has been cancelled (PR by Ganden Schaffner) - Fixed the ``OP_IGNORE_UNEXPECTED_EOF`` flag in an SSL context created by default in ``TLSStream.wrap()`` being inadvertently set on Python 3.11.3 and 3.10.11 - Fixed ``CancelScope`` to properly handle asyncio task uncancellation on Python 3.11 (PR by Nikolay Bryskin) - Fixed ``OSError`` when trying to use ``create_tcp_listener()`` to bind to a link-local IPv6 address (and worked around related bugs in ``uvloop``) - Worked around a `PyPy bug `_ when using ``anyio.getaddrinfo()`` with for IPv6 link-local addresses containing interface names **3.6.2** - Pinned Trio to < 0.22 to avoid incompatibility with AnyIO's ``ExceptionGroup`` class causing ``AttributeError: 'NonBaseMultiError' object has no attribute '_exceptions'`` **3.6.1** - Fixed exception handler in the asyncio test runner not properly handling a context that does not contain the ``exception`` key **3.6.0** - Fixed ``TypeError`` in ``get_current_task()`` on asyncio when using a custom ``Task`` factory - Updated type annotations on ``run_process()`` and ``open_process()``: * ``command`` now accepts accepts bytes and sequences of bytes * ``stdin``, ``stdout`` and ``stderr`` now accept file-like objects (PR by John T. Wodder II) - Changed the pytest plugin to run both the setup and teardown phases of asynchronous generator fixtures within a single task to enable use cases such as cancel scopes and task groups where a context manager straddles the ``yield`` **3.5.0** - Added ``start_new_session`` keyword argument to ``run_process()`` and ``open_process()`` (PR by Jordan Speicher) - Fixed deadlock in synchronization primitives on asyncio which can happen if a task acquiring a primitive is hit with a native (not AnyIO) cancellation with just the right timing, leaving the next acquiring task waiting forever (`#398 `_) - Added workaround for bpo-46313_ to enable compatibility with OpenSSL 3.0 .. _bpo-46313: https://bugs.python.org/issue46313 **3.4.0** - Added context propagation to/from worker threads in ``to_thread.run_sync()``, ``from_thread.run()`` and ``from_thread.run_sync()`` (`#363 `_; partially based on a PR by Sebastián Ramírez) **NOTE**: Requires Python 3.7 to work properly on asyncio! - Fixed race condition in ``Lock`` and ``Semaphore`` classes when a task waiting on ``acquire()`` is cancelled while another task is waiting to acquire the same primitive (`#387 `_) - Fixed async context manager's ``__aexit__()`` method not being called in ``BlockingPortal.wrap_async_context_manager()`` if the host task is cancelled (`#381 `_; PR by Jonathan Slenders) - Fixed worker threads being marked as being event loop threads in sniffio - Fixed task parent ID not getting set to the correct value on asyncio - Enabled the test suite to run without IPv6 support, trio or pytest plugin autoloading **3.3.4** - Fixed ``BrokenResourceError`` instead of ``EndOfStream`` being raised in ``TLSStream`` when the peer abruptly closes the connection while ``TLSStream`` is receiving data with ``standard_compatible=False`` set **3.3.3** - Fixed UNIX socket listener not setting accepted sockets to non-blocking mode on asyncio - Changed unconnected UDP sockets to be always bound to a local port (on "any" interface) to avoid errors on asyncio + Windows **3.3.2** - Fixed cancellation problem on asyncio where level-triggered cancellation for **all** parent cancel scopes would not resume after exiting a shielded nested scope (`#370 `_) **3.3.1** - Added missing documentation for the ``ExceptionGroup.exceptions`` attribute - Changed the asyncio test runner not to use uvloop by default (to match the behavior of ``anyio.run()``) - Fixed ``RuntimeError`` on asyncio when a ``CancelledError`` is raised from a task spawned through a ``BlockingPortal`` (`#357 `_) - Fixed asyncio warning about a ``Future`` with an exception that was never retrieved which happened when a socket was already written to but the peer abruptly closed the connection **3.3.0** - Added asynchronous ``Path`` class - Added the ``wrap_file()`` function for wrapping existing files as asynchronous file objects - Relaxed the type of the ``path`` initializer argument to ``FileReadStream`` and ``FileWriteStream`` so they accept any path-like object (including the new asynchronous ``Path`` class) - Dropped unnecessary dependency on the ``async_generator`` library - Changed the generics in ``AsyncFile`` so that the methods correctly return either ``str`` or ``bytes`` based on the argument to ``open_file()`` - Fixed an asyncio bug where under certain circumstances, a stopping worker thread would still accept new assignments, leading to a hang **3.2.1** - Fixed idle thread pruning on asyncio sometimes causing an expired worker thread to be assigned a task **3.2.0** - Added Python 3.10 compatibility - Added the ability to close memory object streams synchronously (including support for use as a synchronous context manager) - Changed the default value of the ``use_uvloop`` asyncio backend option to ``False`` to prevent unsafe event loop policy changes in different threads - Fixed ``to_thread.run_sync()`` hanging on the second call on asyncio when used with ``loop.run_until_complete()`` - Fixed ``to_thread.run_sync()`` prematurely marking a worker thread inactive when a task await on the result is cancelled - Fixed ``ResourceWarning`` about an unclosed socket when UNIX socket connect fails on asyncio - Fixed the type annotation of ``open_signal_receiver()`` as a synchronous context manager - Fixed the type annotation of ``DeprecatedAwaitable(|List|Float).__await__`` to match the ``typing.Awaitable`` protocol **3.1.0** - Added ``env`` and ``cwd`` keyword arguments to ``run_process()`` and ``open_process``. - Added support for mutation of ``CancelScope.shield`` (PR by John Belmonte) - Added the ``sleep_forever()`` and ``sleep_until()`` functions - Changed asyncio task groups so that if the host and child tasks have only raised ``CancelledErrors``, just one ``CancelledError`` will now be raised instead of an ``ExceptionGroup``, allowing asyncio to ignore it when it propagates out of the task - Changed task names to be converted to ``str`` early on asyncio (PR by Thomas Grainger) - Fixed ``sniffio._impl.AsyncLibraryNotFoundError: unknown async library, or not in async context`` on asyncio and Python 3.6 when ``to_thread.run_sync()`` is used from ``loop.run_until_complete()`` - Fixed odd ``ExceptionGroup: 0 exceptions were raised in the task group`` appearing under certain circumstances on asyncio - Fixed ``wait_all_tasks_blocked()`` returning prematurely on asyncio when a previously blocked task is cancelled (PR by Thomas Grainger) - Fixed declared return type of ``TaskGroup.start()`` (it was declared as ``None``, but anything can be returned from it) - Fixed ``TextStream.extra_attributes`` raising ``AttributeError`` (PR by Thomas Grainger) - Fixed ``await maybe_async(current_task())`` returning ``None`` (PR by Thomas Grainger) - Fixed: ``pickle.dumps(current_task())`` now correctly raises ``TypeError`` instead of pickling to ``None`` (PR by Thomas Grainger) - Fixed return type annotation of ``Event.wait()`` (``bool`` → ``None``) (PR by Thomas Grainger) - Fixed return type annotation of ``RunVar.get()`` to return either the type of the default value or the type of the contained value (PR by Thomas Grainger) - Fixed a deprecation warning message to refer to ``maybe_async()`` and not ``maybe_awaitable()`` (PR by Thomas Grainger) - Filled in argument and return types for all functions and methods previously missing them (PR by Thomas Grainger) **3.0.1** - Fixed ``to_thread.run_sync()`` raising ``RuntimeError`` on asyncio when no "root" task could be found for setting up a cleanup callback. This was a problem at least on Tornado and possibly also Twisted in asyncio compatibility mode. The life of worker threads is now bound to the the host task of the topmost cancel scope hierarchy starting from the current one, or if no cancel scope is active, the current task. **3.0.0** - Curio support has been dropped (see the :doc:`FAQ ` as for why) - API changes: * **BACKWARDS INCOMPATIBLE** Submodules under ``anyio.abc.`` have been made private (use only ``anyio.abc`` from now on). * **BACKWARDS INCOMPATIBLE** The following method was previously a coroutine method and has been converted into a synchronous one: * ``MemoryObjectReceiveStream.receive_nowait()`` * The following functions and methods are no longer asynchronous but can still be awaited on (doing so will emit a deprecation warning): * ``current_time()`` * ``current_effective_deadline()`` * ``get_current_task()`` * ``get_running_tasks()`` * ``CancelScope.cancel()`` * ``CapacityLimiter.acquire_nowait()`` * ``CapacityLimiter.acquire_on_behalf_of_nowait()`` * ``Condition.release()`` * ``Event.set()`` * ``Lock.release()`` * ``MemoryObjectSendStream.send_nowait()`` * ``Semaphore.release()`` * The following functions now return synchronous context managers instead of asynchronous context managers (and emit deprecation warnings if used as async context managers): * ``fail_after()`` * ``move_on_after()`` * ``open_cancel_scope()`` (now just ``CancelScope()``; see below) * ``open_signal_receiver()`` * The following functions and methods have been renamed/moved (will now emit deprecation warnings when you use them by their old names): * ``create_blocking_portal()`` → ``anyio.from_thread.BlockingPortal()`` * ``create_capacity_limiter()`` → ``anyio.CapacityLimiter()`` * ``create_event()`` → ``anyio.Event()`` * ``create_lock()`` → ``anyio.Lock()`` * ``create_condition()`` → ``anyio.Condition()`` * ``create_semaphore()`` → ``anyio.Semaphore()`` * ``current_default_worker_thread_limiter()`` → ``anyio.to_thread.current_default_thread_limiter()`` * ``open_cancel_scope()`` → ``anyio.CancelScope()`` * ``run_sync_in_worker_thread()`` → ``anyio.to_thread.run_sync()`` * ``run_async_from_thread()`` → ``anyio.from_thread.run()`` * ``run_sync_from_thread()`` → ``anyio.from_thread.run_sync()`` * ``BlockingPortal.spawn_task`` → ``BlockingPortal.start_task_soon`` * ``CapacityLimiter.set_total_tokens()`` → ``limiter.total_tokens = ...`` * ``TaskGroup.spawn()`` → ``TaskGroup.start_soon()`` * **BACKWARDS INCOMPATIBLE** ``start_blocking_portal()`` must now be used as a context manager (it no longer returns a BlockingPortal, but a context manager that yields one) * **BACKWARDS INCOMPATIBLE** The ``BlockingPortal.stop_from_external_thread()`` method (use ``portal.call(portal.stop)`` instead now) * **BACKWARDS INCOMPATIBLE** The ``SocketStream`` and ``SocketListener`` classes were made non-generic * Made all non-frozen dataclasses hashable with ``eq=False`` * Removed ``__slots__`` from ``BlockingPortal`` See the :doc:`migration documentation ` for instructions on how to deal with these changes. - Improvements to running synchronous code: * Added the ``run_sync_from_thread()`` function * Added the ``run_sync_in_process()`` function for running code in worker processes (big thanks to Richard Sheridan for his help on this one!) - Improvements to sockets and streaming: * Added the ``UNIXSocketStream`` class which is capable of sending and receiving file descriptors * Added the ``FileReadStream`` and ``FileWriteStream`` classes * ``create_unix_listener()`` now removes any existing socket at the given path before proceeding (instead of raising ``OSError: Address already in use``) - Improvements to task groups and cancellation: * Added the ``TaskGroup.start()`` method and a corresponding ``BlockingPortal.start_task()`` method * Added the ``name`` argument to ``BlockingPortal.start_task_soon()`` (renamed from ``BlockingPortal.spawn_task()``) * Changed ``CancelScope.deadline`` to be writable * Added the following functions in the ``anyio.lowlevel`` module: * ``checkpoint()`` * ``checkpoint_if_cancelled()`` * ``cancel_shielded_checkpoint()`` - Improvements and changes to synchronization primitives: * Added the ``Lock.acquire_nowait()``, ``Condition.acquire_nowait()`` and ``Semaphore.acquire_nowait()`` methods * Added the ``statistics()`` method to ``Event``, ``Lock``, ``Condition``, ``Semaphore``, ``CapacityLimiter``, ``MemoryObjectReceiveStream`` and ``MemoryObjectSendStream`` * ``Lock`` and ``Condition`` can now only be released by the task that acquired them. This behavior is now consistent on all backends whereas previously only Trio enforced this. * The ``CapacityLimiter.total_tokens`` property is now writable and ``CapacityLimiter.set_total_tokens()`` has been deprecated * Added the ``max_value`` property to ``Semaphore`` - Asyncio specific improvements (big thanks to Thomas Grainger for his effort on most of these!): * Cancel scopes are now properly enforced with native asyncio coroutine functions (without any explicit AnyIO checkpoints) * Changed the asyncio ``CancelScope`` to raise a ``RuntimeError`` if a cancel scope is being exited before it was even entered * Changed the asyncio test runner to capture unhandled exceptions from asynchronous callbacks and unbound native tasks which are then raised after the test function (or async fixture setup or teardown) completes * Changed the asyncio ``TaskGroup.start_soon()`` (formerly ``spawn()``) method to call the target function immediately before starting the task, for consistency across backends * Changed the asyncio ``TaskGroup.start_soon()`` (formerly ``spawn()``) method to avoid the use of a coroutine wrapper on Python 3.8+ and added a hint for hiding the wrapper in tracebacks on earlier Pythons (supported by Pytest, Sentry etc.) * Changed the default thread limiter on asyncio to use a ``RunVar`` so it is scoped to the current event loop, thus avoiding potential conflict among multiple running event loops * Thread pooling is now used on asyncio with ``run_sync_in_worker_thread()`` * Fixed ``current_effective_deadline()`` raising ``KeyError`` on asyncio when no cancel scope is active - Added the ``RunVar`` class for scoping variables to the running event loop **2.2.0** - Added the ``maybe_async()`` and ``maybe_async_cm()`` functions to facilitate forward compatibility with AnyIO 3 - Fixed socket stream bug on asyncio where receiving a half-close from the peer would shut down the entire connection - Fixed native task names not being set on asyncio on Python 3.8+ - Fixed ``TLSStream.send_eof()`` raising ``ValueError`` instead of the expected ``NotImplementedError`` - Fixed ``open_signal_receiver()`` on asyncio and curio hanging if the cancel scope was cancelled before the function could run - Fixed Trio test runner causing unwarranted test errors on ``BaseException`` (PR by Matthias Urlichs) - Fixed formatted output of ``ExceptionGroup`` containing too many newlines **2.1.0** - Added the ``spawn_task()`` and ``wrap_async_context_manager()`` methods to ``BlockingPortal`` - Added the ``handshake_timeout`` and ``error_handler`` parameters to ``TLSListener`` - Fixed ``Event`` objects on the trio backend not inheriting from ``anyio.abc.Event`` - Fixed ``run_sync_in_worker_thread()`` raising ``UnboundLocalError`` on asyncio when cancelled - Fixed ``send()`` on socket streams not raising any exception on asyncio, and an unwrapped ``BrokenPipeError`` on trio and curio when the peer has disconnected - Fixed ``MemoryObjectSendStream.send()`` raising ``BrokenResourceError`` when the last receiver is closed right after receiving the item - Fixed ``ValueError: Invalid file descriptor: -1`` when closing a ``SocketListener`` on asyncio **2.0.2** - Fixed one more case of ``AttributeError: 'async_generator_asend' object has no attribute 'cr_await'`` on asyncio **2.0.1** - Fixed broken ``MultiListener.extra()`` (PR by daa) - Fixed ``TLSStream`` returning an empty bytes object instead of raising ``EndOfStream`` when trying to receive from the stream after a closing handshake - Fixed ``AttributeError`` when cancelling a task group's scope inside an async test fixture on asyncio - Fixed ``wait_all_tasks_blocked()`` raising ``AttributeError`` on asyncio if a native task is waiting on an async generator's ``asend()`` method **2.0.0** - General new features: - Added support for subprocesses - Added support for "blocking portals" which allow running functions in the event loop thread from external threads - Added the ``anyio.aclose_forcefully()`` function for closing asynchronous resources as quickly as possible - General changes/fixes: - **BACKWARDS INCOMPATIBLE** Some functions have been renamed or removed (see further below for socket/fileio API changes): - ``finalize()`` → (removed; use ``contextlib.aclosing()`` instead) - ``receive_signals()`` → ``open_signal_receiver()`` - ``run_in_thread()`` → ``run_sync_in_worker_thread()`` - ``current_default_thread_limiter()`` → ``current_default_worker_thread_limiter()`` - ``ResourceBusyError`` → ``BusyResourceError`` - **BACKWARDS INCOMPATIBLE** Exception classes were moved to the top level package - Dropped support for Python 3.5 - Bumped minimum versions of trio and curio to v0.16 and v1.4, respectively - Changed the ``repr()`` of ``ExceptionGroup`` to match trio's ``MultiError`` - Backend specific changes and fixes: - ``asyncio``: Added support for ``ProactorEventLoop``. This allows asyncio applications to use AnyIO on Windows even without using AnyIO as the entry point. - ``asyncio``: The asyncio backend now uses ``asyncio.run()`` behind the scenes which properly shuts down async generators and cancels any leftover native tasks - ``curio``: Worked around the limitation where a task can only be cancelled twice (any cancellations beyond that were ignored) - ``asyncio`` + ``curio``: a cancellation check now calls ``sleep(0)``, allowing the scheduler to switch to a different task - ``asyncio`` + ``curio``: Host name resolution now uses `IDNA 2008`_ (with UTS 46 compatibility mapping, just like trio) - ``asyncio`` + ``curio``: Fixed a bug where a task group would abandon its subtasks if its own cancel scope was cancelled while it was waiting for subtasks to finish - ``asyncio`` + ``curio``: Fixed recursive tracebacks when a single exception from an inner task group is reraised in an outer task group - Socket/stream changes: - **BACKWARDS INCOMPATIBLE** The stream class structure was completely overhauled. There are now separate abstract base classes for receive and send streams, byte streams and reliable and unreliable object streams. Stream wrappers are much better supported by this new ABC structure and a new "typed extra attribute" system that lets you query the wrapper chain for the attributes you want via ``.extra(...)``. - **BACKWARDS INCOMPATIBLE** Socket server functionality has been refactored into a network-agnostic listener system - **BACKWARDS INCOMPATIBLE** TLS functionality has been split off from ``SocketStream`` and can now work over any bidirectional bytes-based stream – you can now establish a TLS encrypted communications pathway over UNIX sockets or even memory object streams. The ``TLSRequired`` exception has also been removed as it is no longer necessary. - **BACKWARDS INCOMPATIBLE** Buffering functionality (``receive_until()`` and ``receive_exactly()``) was split off from ``SocketStream`` into a stream wrapper class (``anyio.streams.buffered.BufferedByteReceiveStream``) - **BACKWARDS INCOMPATIBLE** IPv6 addresses are now reported as 2-tuples. If original 4-tuple form contains a nonzero scope ID, it is appended to the address with ``%`` as the separator. - **BACKWARDS INCOMPATIBLE** Byte streams (including socket streams) now raise ``EndOfStream`` instead of returning an empty bytes object when the stream has been closed from the other end - **BACKWARDS INCOMPATIBLE** The socket API has changes: - ``create_tcp_server()`` → ``create_tcp_listener()`` - ``create_unix_server()`` → ``create_unix_listener()`` - ``create_udp_socket()`` had some of its parameters changed: - ``interface`` → ``local_address`` - ``port`` → ``local_port`` - ``reuse_address`` was replaced with ``reuse_port`` (and sets ``SO_REUSEPORT`` instead of ``SO_REUSEADDR``) - ``connect_tcp()`` had some of its parameters changed: - ``address`` → ``remote_address`` - ``port`` → ``remote_port`` - ``bind_host`` → ``local_address`` - ``bind_port`` → (removed) - ``autostart_tls`` → ``tls`` - ``tls_hostname`` (new parameter, when you want to match the certificate against against something else than ``remote_address``) - ``connect_tcp()`` now returns a ``TLSStream`` if TLS was enabled - ``notify_socket_closing()`` was removed, as it is no longer used by AnyIO - ``SocketStream`` has changes to its methods and attributes: - ``address`` → ``.extra(SocketAttribute.local_address)`` - ``alpn_protocol`` → ``.extra(TLSAttribute.alpn_protocol)`` - ``close()`` → ``aclose()`` - ``get_channel_binding`` → ``.extra(TLSAttribute.channel_binding_tls_unique)`` - ``cipher`` → ``.extra(TLSAttribute.cipher)`` - ``getpeercert`` → ``.extra(SocketAttribute.peer_certificate)`` or ``.extra(SocketAttribute.peer_certificate_binary)`` - ``getsockopt()`` → ``.extra(SocketAttribute.raw_socket).getsockopt(...)`` - ``peer_address`` → ``.extra(SocketAttribute.remote_address)`` - ``receive_chunks()`` → (removed; use ``async for`` on the stream instead) - ``receive_delimited_chunks()`` → (removed) - ``receive_exactly()`` → ``BufferedReceiveStream.receive_exactly()`` - ``receive_some()`` → ``receive()`` - ``receive_until()`` → ``BufferedReceiveStream.receive_until()`` - ``send_all()`` → ``send()`` - ``setsockopt()`` → ``.extra(SocketAttribute.raw_socket).setsockopt(...)`` - ``shared_ciphers`` → ``.extra(TLSAttribute.shared_ciphers)`` - ``server_side`` → ``.extra(TLSAttribute.server_side)`` - ``start_tls()`` → ``stream = TLSStream.wrap(...)`` - ``tls_version`` → ``.extra(TLSAttribute.tls_version)`` - ``UDPSocket`` has changes to its methods and attributes: - ``address`` → ``.extra(SocketAttribute.local_address)`` - ``getsockopt()`` → ``.extra(SocketAttribute.raw_socket).getsockopt(...)`` - ``port`` → ``.extra(SocketAttribute.local_port)`` - ``receive()`` no longer takes a maximum bytes argument - ``receive_packets()`` → (removed; use ``async for`` on the UDP socket instead) - ``send()`` → requires a tuple for destination now (address, port), for compatibility with the new ``UnreliableObjectStream`` interface. The ``sendto()`` method works like the old ``send()`` method. - ``setsockopt()`` → ``.extra(SocketAttribute.raw_socket).setsockopt(...)`` - **BACKWARDS INCOMPATIBLE** Renamed the ``max_size`` parameter to ``max_bytes`` wherever it occurred (this was inconsistently named ``max_bytes`` in some subclasses before) - Added memory object streams as a replacement for queues - Added stream wrappers for encoding/decoding unicode strings - Support for the ``SO_REUSEPORT`` option (allows binding more than one socket to the same address/port combination, as long as they all have this option set) has been added to TCP listeners and UDP sockets - The ``send_eof()`` method was added to all (bidirectional) streams - File I/O changes: - **BACKWARDS INCOMPATIBLE** Asynchronous file I/O functionality now uses a common code base (``anyio.AsyncFile``) instead of backend-native classes - **BACKWARDS INCOMPATIBLE** The File I/O API has changes to its functions and methods: - ``aopen()`` → ``open_file()`` - ``AsyncFileclose()`` → ``AsyncFileaclose()`` - Task synchronization changes: - **BACKWARDS INCOMPATIBLE** Queues were replaced by memory object streams - **BACKWARDS INCOMPATIBLE** Added the ``acquire()`` and ``release()`` methods to the ``Lock``, ``Condition`` and ``Semaphore`` classes - **BACKWARDS INCOMPATIBLE** Removed the ``Event.clear()`` method. You must now replace the event object with a new one rather than clear the old one. - Fixed ``Condition.wait()`` not working on asyncio and curio (PR by Matt Westcott) - Testing changes: - **BACKWARDS INCOMPATIBLE** Removed the ``--anyio-backends`` command line option for the pytest plugin. Use the ``-k`` option to do ad-hoc filtering, and the ``anyio_backend`` fixture to control which backends you wish to run the tests by default. - The pytest plugin was refactored to run the test and all its related async fixtures inside the same event loop, making async fixtures much more useful - Fixed Hypothesis support in the pytest plugin (it was not actually running the Hypothesis tests at all) .. _IDNA 2008: https://tools.ietf.org/html/rfc5895 **1.4.0** - Added async name resolution functions (``anyio.getaddrinfo()`` and ``anyio.getnameinfo()``) - Added the ``family`` and ``reuse_address`` parameters to ``anyio.create_udp_socket()`` (Enables multicast support; test contributed by Matthias Urlichs) - Fixed ``fail.after(0)`` not raising a timeout error on asyncio and curio - Fixed ``move_on_after()`` and ``fail_after()`` getting stuck on curio in some circumstances - Fixed socket operations not allowing timeouts to cancel the task - Fixed API documentation on ``Stream.receive_until()`` which claimed that the delimiter will be included in the returned data when it really isn't - Harmonized the default task names across all backends - ``wait_all_tasks_blocked()`` no longer considers tasks waiting on ``sleep(0)`` to be blocked on asyncio and curio - Fixed the type of the ``address`` parameter in ``UDPSocket.send()`` to include ``IPAddress`` objects (which were already supported by the backing implementation) - Fixed ``UDPSocket.send()`` to resolve host names using ``anyio.getaddrinfo()`` before calling ``socket.sendto()`` to avoid blocking on synchronous name resolution - Switched to using ``anyio.getaddrinfo()`` for name lookups **1.3.1** - Fixed warnings caused by trio 0.15 - Worked around a compatibility issue between uvloop and Python 3.9 (missing ``shutdown_default_executor()`` method) **1.3.0** - Fixed compatibility with Curio 1.0 - Made it possible to assert fine grained control over which AnyIO backends and backend options are being used with each test - Added the ``address`` and ``peer_address`` properties to the ``SocketStream`` interface **1.2.3** - Repackaged release (v1.2.2 contained extra files from an experimental branch which broke imports) **1.2.2** - Fixed ``CancelledError`` leaking from a cancel scope on asyncio if the task previously received a cancellation exception - Fixed ``AttributeError`` when cancelling a generator-based task (asyncio) - Fixed ``wait_all_tasks_blocked()`` not working with generator-based tasks (asyncio) - Fixed an unnecessary delay in ``connect_tcp()`` if an earlier attempt succeeds - Fixed ``AssertionError`` in ``connect_tcp()`` if multiple connection attempts succeed simultaneously **1.2.1** - Fixed cancellation errors leaking from a task group when they are contained in an exception group - Fixed trio v0.13 compatibility on Windows - Fixed inconsistent queue capacity across backends when capacity was defined as 0 (trio = 0, others = infinite) - Fixed socket creation failure crashing ``connect_tcp()`` **1.2.0** - Added the possibility to parametrize regular pytest test functions against the selected list of backends - Added the ``set_total_tokens()`` method to ``CapacityLimiter`` - Added the ``anyio.current_default_thread_limiter()`` function - Added the ``cancellable`` parameter to ``anyio.run_in_thread()`` - Implemented the Happy Eyeballs (:rfc:`6555`) algorithm for ``anyio.connect_tcp()`` - Fixed ``KeyError`` on asyncio and curio where entering and exiting a cancel scope happens in different tasks - Fixed deprecation warnings on Python 3.8 about the ``loop`` argument of ``asyncio.Event()`` - Forced the use ``WindowsSelectorEventLoopPolicy`` in ``asyncio.run`` when on Windows and asyncio to keep network functionality working - Worker threads are now spawned with ``daemon=True`` on all backends, not just trio - Dropped support for trio v0.11 **1.1.0** - Added the ``lock`` parameter to ``anyio.create_condition()`` (PR by Matthias Urlichs) - Added async iteration for queues (PR by Matthias Urlichs) - Added capacity limiters - Added the possibility of using capacity limiters for limiting the maximum number of threads - Fixed compatibility with trio v0.12 - Fixed IPv6 support in ``create_tcp_server()``, ``connect_tcp()`` and ``create_udp_socket()`` - Fixed mishandling of task cancellation while the task is running a worker thread on asyncio and curio **1.0.0** - Fixed pathlib2_ compatibility with ``anyio.aopen()`` - Fixed timeouts not propagating from nested scopes on asyncio and curio (PR by Matthias Urlichs) - Fixed incorrect call order in socket close notifications on asyncio (mostly affecting Windows) - Prefixed backend module names with an underscore to better indicate privateness .. _pathlib2: https://pypi.org/project/pathlib2/ **1.0.0rc2** - Fixed some corner cases of cancellation where behavior on asyncio and curio did not match with that of trio. Thanks to Joshua Oreman for help with this. - Fixed ``current_effective_deadline()`` not taking shielded cancellation scopes into account on asyncio and curio - Fixed task cancellation not happening right away on asyncio and curio when a cancel scope is entered when the deadline has already passed - Fixed exception group containing only cancellation exceptions not being swallowed by a timed out cancel scope on asyncio and curio - Added the ``current_time()`` function - Replaced ``CancelledError`` with ``get_cancelled_exc_class()`` - Added support for Hypothesis_ - Added support for :pep:`561` - Use uvloop for the asyncio backend by default when available (but only on CPython) .. _Hypothesis: https://hypothesis.works/ **1.0.0rc1** - Fixed ``setsockopt()`` passing options to the underlying method in the wrong manner - Fixed cancellation propagation from nested task groups - Fixed ``get_running_tasks()`` returning tasks from other event loops - Added the ``parent_id`` attribute to ``anyio.TaskInfo`` - Added the ``get_current_task()`` function - Added guards to protect against concurrent read/write from/to sockets by multiple tasks - Added the ``notify_socket_close()`` function **1.0.0b2** - Added introspection of running tasks via ``anyio.get_running_tasks()`` - Added the ``getsockopt()`` and ``setsockopt()`` methods to the ``SocketStream`` API - Fixed mishandling of large buffers by ``BaseSocket.sendall()`` - Fixed compatibility with (and upgraded minimum required version to) trio v0.11 **1.0.0b1** - Initial release anyio-4.4.0/pyproject.toml000066400000000000000000000102651462472665400155760ustar00rootroot00000000000000[build-system] requires = [ "setuptools >= 64", "setuptools_scm >= 6.4" ] build-backend = "setuptools.build_meta" [project] name = "anyio" description = "High level compatibility layer for multiple asynchronous event loop implementations" readme = "README.rst" authors = [{name = "Alex Grönholm", email = "alex.gronholm@nextday.fi"}] license = {text = "MIT"} classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Framework :: AnyIO", "Typing :: Typed", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] requires-python = ">= 3.8" dependencies = [ "exceptiongroup >= 1.0.2; python_version < '3.11'", "idna >= 2.8", "sniffio >= 1.1", "typing_extensions >= 4.1; python_version < '3.11'", ] dynamic = ["version"] [project.urls] Documentation = "https://anyio.readthedocs.io/en/latest/" Changelog = "https://anyio.readthedocs.io/en/stable/versionhistory.html" "Source code" = "https://github.com/agronholm/anyio" "Issue tracker" = "https://github.com/agronholm/anyio/issues" [project.optional-dependencies] trio = ["trio >= 0.23"] test = [ "anyio[trio]", "coverage[toml] >= 7", "exceptiongroup >= 1.2.0", "hypothesis >= 4.0", "psutil >= 5.9", "pytest >= 7.0", "pytest-mock >= 3.6.1", "trustme", "uvloop >= 0.17; platform_python_implementation == 'CPython' and platform_system != 'Windows'", ] doc = [ "packaging", "Sphinx >= 7", "sphinx_rtd_theme", "sphinx-autodoc-typehints >= 1.2.0", ] [project.entry-points] pytest11 = {anyio = "anyio.pytest_plugin"} [tool.setuptools_scm] version_scheme = "post-release" local_scheme = "dirty-tag" [tool.ruff] src = ["src"] [tool.ruff.lint] extend-select = [ "ASYNC", # flake8-async "W", # pycodestyle warnings "G", # flake8-logging-format "I", # isort "ISC", # flake8-implicit-str-concat "PGH", # pygrep-hooks "RUF100", # unused noqa (yesqa) "T201", # print "UP", # pyupgrade ] [tool.ruff.lint.isort] "required-imports" = ["from __future__ import annotations"] [tool.mypy] python_version = "3.12" strict = true ignore_missing_imports = true disallow_any_generics = false warn_return_any = false disallow_untyped_decorators = false disallow_subclassing_any = false show_error_codes = true [tool.pytest.ini_options] addopts = "-rsx --tb=short --strict-config --strict-markers -p anyio -p no:asyncio -p no:trio" testpaths = ["tests"] xfail_strict = true # Ignore resource warnings due to a CPython/Windows bug (https://bugs.python.org/issue44428) filterwarnings = [ "error", "ignore:unclosed = (3, 10): from typing import ParamSpec else: from typing_extensions import ParamSpec if sys.version_info >= (3, 11): from asyncio import Runner from typing import TypeVarTuple, Unpack else: import contextvars import enum import signal from asyncio import coroutines, events, exceptions, tasks from exceptiongroup import BaseExceptionGroup from typing_extensions import TypeVarTuple, Unpack class _State(enum.Enum): CREATED = "created" INITIALIZED = "initialized" CLOSED = "closed" class Runner: # Copied from CPython 3.11 def __init__( self, *, debug: bool | None = None, loop_factory: Callable[[], AbstractEventLoop] | None = None, ): self._state = _State.CREATED self._debug = debug self._loop_factory = loop_factory self._loop: AbstractEventLoop | None = None self._context = None self._interrupt_count = 0 self._set_event_loop = False def __enter__(self) -> Runner: self._lazy_init() return self def __exit__( self, exc_type: type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> None: self.close() def close(self) -> None: """Shutdown and close event loop.""" if self._state is not _State.INITIALIZED: return try: loop = self._loop _cancel_all_tasks(loop) loop.run_until_complete(loop.shutdown_asyncgens()) if hasattr(loop, "shutdown_default_executor"): loop.run_until_complete(loop.shutdown_default_executor()) else: loop.run_until_complete(_shutdown_default_executor(loop)) finally: if self._set_event_loop: events.set_event_loop(None) loop.close() self._loop = None self._state = _State.CLOSED def get_loop(self) -> AbstractEventLoop: """Return embedded event loop.""" self._lazy_init() return self._loop def run(self, coro: Coroutine[T_Retval], *, context=None) -> T_Retval: """Run a coroutine inside the embedded event loop.""" if not coroutines.iscoroutine(coro): raise ValueError(f"a coroutine was expected, got {coro!r}") if events._get_running_loop() is not None: # fail fast with short traceback raise RuntimeError( "Runner.run() cannot be called from a running event loop" ) self._lazy_init() if context is None: context = self._context task = context.run(self._loop.create_task, coro) if ( threading.current_thread() is threading.main_thread() and signal.getsignal(signal.SIGINT) is signal.default_int_handler ): sigint_handler = partial(self._on_sigint, main_task=task) try: signal.signal(signal.SIGINT, sigint_handler) except ValueError: # `signal.signal` may throw if `threading.main_thread` does # not support signals (e.g. embedded interpreter with signals # not registered - see gh-91880) sigint_handler = None else: sigint_handler = None self._interrupt_count = 0 try: return self._loop.run_until_complete(task) except exceptions.CancelledError: if self._interrupt_count > 0: uncancel = getattr(task, "uncancel", None) if uncancel is not None and uncancel() == 0: raise KeyboardInterrupt() raise # CancelledError finally: if ( sigint_handler is not None and signal.getsignal(signal.SIGINT) is sigint_handler ): signal.signal(signal.SIGINT, signal.default_int_handler) def _lazy_init(self) -> None: if self._state is _State.CLOSED: raise RuntimeError("Runner is closed") if self._state is _State.INITIALIZED: return if self._loop_factory is None: self._loop = events.new_event_loop() if not self._set_event_loop: # Call set_event_loop only once to avoid calling # attach_loop multiple times on child watchers events.set_event_loop(self._loop) self._set_event_loop = True else: self._loop = self._loop_factory() if self._debug is not None: self._loop.set_debug(self._debug) self._context = contextvars.copy_context() self._state = _State.INITIALIZED def _on_sigint(self, signum, frame, main_task: asyncio.Task) -> None: self._interrupt_count += 1 if self._interrupt_count == 1 and not main_task.done(): main_task.cancel() # wakeup loop if it is blocked by select() with long timeout self._loop.call_soon_threadsafe(lambda: None) return raise KeyboardInterrupt() def _cancel_all_tasks(loop: AbstractEventLoop) -> None: to_cancel = tasks.all_tasks(loop) if not to_cancel: return for task in to_cancel: task.cancel() loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True)) for task in to_cancel: if task.cancelled(): continue if task.exception() is not None: loop.call_exception_handler( { "message": "unhandled exception during asyncio.run() shutdown", "exception": task.exception(), "task": task, } ) async def _shutdown_default_executor(loop: AbstractEventLoop) -> None: """Schedule the shutdown of the default executor.""" def _do_shutdown(future: asyncio.futures.Future) -> None: try: loop._default_executor.shutdown(wait=True) # type: ignore[attr-defined] loop.call_soon_threadsafe(future.set_result, None) except Exception as ex: loop.call_soon_threadsafe(future.set_exception, ex) loop._executor_shutdown_called = True if loop._default_executor is None: return future = loop.create_future() thread = threading.Thread(target=_do_shutdown, args=(future,)) thread.start() try: await future finally: thread.join() T_Retval = TypeVar("T_Retval") T_contra = TypeVar("T_contra", contravariant=True) PosArgsT = TypeVarTuple("PosArgsT") P = ParamSpec("P") _root_task: RunVar[asyncio.Task | None] = RunVar("_root_task") def find_root_task() -> asyncio.Task: root_task = _root_task.get(None) if root_task is not None and not root_task.done(): return root_task # Look for a task that has been started via run_until_complete() for task in all_tasks(): if task._callbacks and not task.done(): callbacks = [cb for cb, context in task._callbacks] for cb in callbacks: if ( cb is _run_until_complete_cb or getattr(cb, "__module__", None) == "uvloop.loop" ): _root_task.set(task) return task # Look up the topmost task in the AnyIO task tree, if possible task = cast(asyncio.Task, current_task()) state = _task_states.get(task) if state: cancel_scope = state.cancel_scope while cancel_scope and cancel_scope._parent_scope is not None: cancel_scope = cancel_scope._parent_scope if cancel_scope is not None: return cast(asyncio.Task, cancel_scope._host_task) return task def get_callable_name(func: Callable) -> str: module = getattr(func, "__module__", None) qualname = getattr(func, "__qualname__", None) return ".".join([x for x in (module, qualname) if x]) # # Event loop # _run_vars: WeakKeyDictionary[asyncio.AbstractEventLoop, Any] = WeakKeyDictionary() def _task_started(task: asyncio.Task) -> bool: """Return ``True`` if the task has been started and has not finished.""" try: return getcoroutinestate(task.get_coro()) in (CORO_RUNNING, CORO_SUSPENDED) except AttributeError: # task coro is async_genenerator_asend https://bugs.python.org/issue37771 raise Exception(f"Cannot determine if task {task} has started or not") from None # # Timeouts and cancellation # class CancelScope(BaseCancelScope): def __new__( cls, *, deadline: float = math.inf, shield: bool = False ) -> CancelScope: return object.__new__(cls) def __init__(self, deadline: float = math.inf, shield: bool = False): self._deadline = deadline self._shield = shield self._parent_scope: CancelScope | None = None self._child_scopes: set[CancelScope] = set() self._cancel_called = False self._cancelled_caught = False self._active = False self._timeout_handle: asyncio.TimerHandle | None = None self._cancel_handle: asyncio.Handle | None = None self._tasks: set[asyncio.Task] = set() self._host_task: asyncio.Task | None = None self._cancel_calls: int = 0 self._cancelling: int | None = None def __enter__(self) -> CancelScope: if self._active: raise RuntimeError( "Each CancelScope may only be used for a single 'with' block" ) self._host_task = host_task = cast(asyncio.Task, current_task()) self._tasks.add(host_task) try: task_state = _task_states[host_task] except KeyError: task_state = TaskState(None, self) _task_states[host_task] = task_state else: self._parent_scope = task_state.cancel_scope task_state.cancel_scope = self if self._parent_scope is not None: self._parent_scope._child_scopes.add(self) self._parent_scope._tasks.remove(host_task) self._timeout() self._active = True if sys.version_info >= (3, 11): self._cancelling = self._host_task.cancelling() # Start cancelling the host task if the scope was cancelled before entering if self._cancel_called: self._deliver_cancellation(self) return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: if not self._active: raise RuntimeError("This cancel scope is not active") if current_task() is not self._host_task: raise RuntimeError( "Attempted to exit cancel scope in a different task than it was " "entered in" ) assert self._host_task is not None host_task_state = _task_states.get(self._host_task) if host_task_state is None or host_task_state.cancel_scope is not self: raise RuntimeError( "Attempted to exit a cancel scope that isn't the current tasks's " "current cancel scope" ) self._active = False if self._timeout_handle: self._timeout_handle.cancel() self._timeout_handle = None self._tasks.remove(self._host_task) if self._parent_scope is not None: self._parent_scope._child_scopes.remove(self) self._parent_scope._tasks.add(self._host_task) host_task_state.cancel_scope = self._parent_scope # Restart the cancellation effort in the closest directly cancelled parent # scope if this one was shielded self._restart_cancellation_in_parent() if self._cancel_called and exc_val is not None: for exc in iterate_exceptions(exc_val): if isinstance(exc, CancelledError): self._cancelled_caught = self._uncancel(exc) if self._cancelled_caught: break return self._cancelled_caught return None def _uncancel(self, cancelled_exc: CancelledError) -> bool: if sys.version_info < (3, 9) or self._host_task is None: self._cancel_calls = 0 return True # Undo all cancellations done by this scope if self._cancelling is not None: while self._cancel_calls: self._cancel_calls -= 1 if self._host_task.uncancel() <= self._cancelling: return True self._cancel_calls = 0 return f"Cancelled by cancel scope {id(self):x}" in cancelled_exc.args def _timeout(self) -> None: if self._deadline != math.inf: loop = get_running_loop() if loop.time() >= self._deadline: self.cancel() else: self._timeout_handle = loop.call_at(self._deadline, self._timeout) def _deliver_cancellation(self, origin: CancelScope) -> bool: """ Deliver cancellation to directly contained tasks and nested cancel scopes. Schedule another run at the end if we still have tasks eligible for cancellation. :param origin: the cancel scope that originated the cancellation :return: ``True`` if the delivery needs to be retried on the next cycle """ should_retry = False current = current_task() for task in self._tasks: if task._must_cancel: # type: ignore[attr-defined] continue # The task is eligible for cancellation if it has started should_retry = True if task is not current and (task is self._host_task or _task_started(task)): waiter = task._fut_waiter # type: ignore[attr-defined] if not isinstance(waiter, asyncio.Future) or not waiter.done(): origin._cancel_calls += 1 if sys.version_info >= (3, 9): task.cancel(f"Cancelled by cancel scope {id(origin):x}") else: task.cancel() # Deliver cancellation to child scopes that aren't shielded or running their own # cancellation callbacks for scope in self._child_scopes: if not scope._shield and not scope.cancel_called: should_retry = scope._deliver_cancellation(origin) or should_retry # Schedule another callback if there are still tasks left if origin is self: if should_retry: self._cancel_handle = get_running_loop().call_soon( self._deliver_cancellation, origin ) else: self._cancel_handle = None return should_retry def _restart_cancellation_in_parent(self) -> None: """ Restart the cancellation effort in the closest directly cancelled parent scope. """ scope = self._parent_scope while scope is not None: if scope._cancel_called: if scope._cancel_handle is None: scope._deliver_cancellation(scope) break # No point in looking beyond any shielded scope if scope._shield: break scope = scope._parent_scope def _parent_cancelled(self) -> bool: # Check whether any parent has been cancelled cancel_scope = self._parent_scope while cancel_scope is not None and not cancel_scope._shield: if cancel_scope._cancel_called: return True else: cancel_scope = cancel_scope._parent_scope return False def cancel(self) -> None: if not self._cancel_called: if self._timeout_handle: self._timeout_handle.cancel() self._timeout_handle = None self._cancel_called = True if self._host_task is not None: self._deliver_cancellation(self) @property def deadline(self) -> float: return self._deadline @deadline.setter def deadline(self, value: float) -> None: self._deadline = float(value) if self._timeout_handle is not None: self._timeout_handle.cancel() self._timeout_handle = None if self._active and not self._cancel_called: self._timeout() @property def cancel_called(self) -> bool: return self._cancel_called @property def cancelled_caught(self) -> bool: return self._cancelled_caught @property def shield(self) -> bool: return self._shield @shield.setter def shield(self, value: bool) -> None: if self._shield != value: self._shield = value if not value: self._restart_cancellation_in_parent() # # Task states # class TaskState: """ Encapsulates auxiliary task information that cannot be added to the Task instance itself because there are no guarantees about its implementation. """ __slots__ = "parent_id", "cancel_scope", "__weakref__" def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None): self.parent_id = parent_id self.cancel_scope = cancel_scope _task_states: WeakKeyDictionary[asyncio.Task, TaskState] = WeakKeyDictionary() # # Task groups # class _AsyncioTaskStatus(abc.TaskStatus): def __init__(self, future: asyncio.Future, parent_id: int): self._future = future self._parent_id = parent_id def started(self, value: T_contra | None = None) -> None: try: self._future.set_result(value) except asyncio.InvalidStateError: if not self._future.cancelled(): raise RuntimeError( "called 'started' twice on the same task status" ) from None task = cast(asyncio.Task, current_task()) _task_states[task].parent_id = self._parent_id def iterate_exceptions( exception: BaseException, ) -> Generator[BaseException, None, None]: if isinstance(exception, BaseExceptionGroup): for exc in exception.exceptions: yield from iterate_exceptions(exc) else: yield exception class TaskGroup(abc.TaskGroup): def __init__(self) -> None: self.cancel_scope: CancelScope = CancelScope() self._active = False self._exceptions: list[BaseException] = [] self._tasks: set[asyncio.Task] = set() async def __aenter__(self) -> TaskGroup: self.cancel_scope.__enter__() self._active = True return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: ignore_exception = self.cancel_scope.__exit__(exc_type, exc_val, exc_tb) if exc_val is not None: self.cancel_scope.cancel() if not isinstance(exc_val, CancelledError): self._exceptions.append(exc_val) cancelled_exc_while_waiting_tasks: CancelledError | None = None while self._tasks: try: await asyncio.wait(self._tasks) except CancelledError as exc: # This task was cancelled natively; reraise the CancelledError later # unless this task was already interrupted by another exception self.cancel_scope.cancel() if cancelled_exc_while_waiting_tasks is None: cancelled_exc_while_waiting_tasks = exc self._active = False if self._exceptions: raise BaseExceptionGroup( "unhandled errors in a TaskGroup", self._exceptions ) # Raise the CancelledError received while waiting for child tasks to exit, # unless the context manager itself was previously exited with another # exception, or if any of the child tasks raised an exception other than # CancelledError if cancelled_exc_while_waiting_tasks: if exc_val is None or ignore_exception: raise cancelled_exc_while_waiting_tasks return ignore_exception def _spawn( self, func: Callable[[Unpack[PosArgsT]], Awaitable[Any]], args: tuple[Unpack[PosArgsT]], name: object, task_status_future: asyncio.Future | None = None, ) -> asyncio.Task: def task_done(_task: asyncio.Task) -> None: task_state = _task_states[_task] assert task_state.cancel_scope is not None assert _task in task_state.cancel_scope._tasks task_state.cancel_scope._tasks.remove(_task) self._tasks.remove(task) del _task_states[_task] try: exc = _task.exception() except CancelledError as e: while isinstance(e.__context__, CancelledError): e = e.__context__ exc = e if exc is not None: # The future can only be in the cancelled state if the host task was # cancelled, so return immediately instead of adding one more # CancelledError to the exceptions list if task_status_future is not None and task_status_future.cancelled(): return if task_status_future is None or task_status_future.done(): if not isinstance(exc, CancelledError): self._exceptions.append(exc) if not self.cancel_scope._parent_cancelled(): self.cancel_scope.cancel() else: task_status_future.set_exception(exc) elif task_status_future is not None and not task_status_future.done(): task_status_future.set_exception( RuntimeError("Child exited without calling task_status.started()") ) if not self._active: raise RuntimeError( "This task group is not active; no new tasks can be started." ) kwargs = {} if task_status_future: parent_id = id(current_task()) kwargs["task_status"] = _AsyncioTaskStatus( task_status_future, id(self.cancel_scope._host_task) ) else: parent_id = id(self.cancel_scope._host_task) coro = func(*args, **kwargs) if not iscoroutine(coro): prefix = f"{func.__module__}." if hasattr(func, "__module__") else "" raise TypeError( f"Expected {prefix}{func.__qualname__}() to return a coroutine, but " f"the return value ({coro!r}) is not a coroutine object" ) name = get_callable_name(func) if name is None else str(name) task = create_task(coro, name=name) task.add_done_callback(task_done) # Make the spawned task inherit the task group's cancel scope _task_states[task] = TaskState( parent_id=parent_id, cancel_scope=self.cancel_scope ) self.cancel_scope._tasks.add(task) self._tasks.add(task) return task def start_soon( self, func: Callable[[Unpack[PosArgsT]], Awaitable[Any]], *args: Unpack[PosArgsT], name: object = None, ) -> None: self._spawn(func, args, name) async def start( self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None ) -> Any: future: asyncio.Future = asyncio.Future() task = self._spawn(func, args, name, future) # If the task raises an exception after sending a start value without a switch # point between, the task group is cancelled and this method never proceeds to # process the completed future. That's why we have to have a shielded cancel # scope here. try: return await future except CancelledError: # Cancel the task and wait for it to exit before returning task.cancel() with CancelScope(shield=True), suppress(CancelledError): await task raise # # Threads # _Retval_Queue_Type = Tuple[Optional[T_Retval], Optional[BaseException]] class WorkerThread(Thread): MAX_IDLE_TIME = 10 # seconds def __init__( self, root_task: asyncio.Task, workers: set[WorkerThread], idle_workers: deque[WorkerThread], ): super().__init__(name="AnyIO worker thread") self.root_task = root_task self.workers = workers self.idle_workers = idle_workers self.loop = root_task._loop self.queue: Queue[ tuple[Context, Callable, tuple, asyncio.Future, CancelScope] | None ] = Queue(2) self.idle_since = AsyncIOBackend.current_time() self.stopping = False def _report_result( self, future: asyncio.Future, result: Any, exc: BaseException | None ) -> None: self.idle_since = AsyncIOBackend.current_time() if not self.stopping: self.idle_workers.append(self) if not future.cancelled(): if exc is not None: if isinstance(exc, StopIteration): new_exc = RuntimeError("coroutine raised StopIteration") new_exc.__cause__ = exc exc = new_exc future.set_exception(exc) else: future.set_result(result) def run(self) -> None: with claim_worker_thread(AsyncIOBackend, self.loop): while True: item = self.queue.get() if item is None: # Shutdown command received return context, func, args, future, cancel_scope = item if not future.cancelled(): result = None exception: BaseException | None = None threadlocals.current_cancel_scope = cancel_scope try: result = context.run(func, *args) except BaseException as exc: exception = exc finally: del threadlocals.current_cancel_scope if not self.loop.is_closed(): self.loop.call_soon_threadsafe( self._report_result, future, result, exception ) self.queue.task_done() def stop(self, f: asyncio.Task | None = None) -> None: self.stopping = True self.queue.put_nowait(None) self.workers.discard(self) try: self.idle_workers.remove(self) except ValueError: pass _threadpool_idle_workers: RunVar[deque[WorkerThread]] = RunVar( "_threadpool_idle_workers" ) _threadpool_workers: RunVar[set[WorkerThread]] = RunVar("_threadpool_workers") class BlockingPortal(abc.BlockingPortal): def __new__(cls) -> BlockingPortal: return object.__new__(cls) def __init__(self) -> None: super().__init__() self._loop = get_running_loop() def _spawn_task_from_thread( self, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], args: tuple[Unpack[PosArgsT]], kwargs: dict[str, Any], name: object, future: Future[T_Retval], ) -> None: AsyncIOBackend.run_sync_from_thread( partial(self._task_group.start_soon, name=name), (self._call_func, func, args, kwargs, future), self._loop, ) # # Subprocesses # @dataclass(eq=False) class StreamReaderWrapper(abc.ByteReceiveStream): _stream: asyncio.StreamReader async def receive(self, max_bytes: int = 65536) -> bytes: data = await self._stream.read(max_bytes) if data: return data else: raise EndOfStream async def aclose(self) -> None: self._stream.feed_eof() await AsyncIOBackend.checkpoint() @dataclass(eq=False) class StreamWriterWrapper(abc.ByteSendStream): _stream: asyncio.StreamWriter async def send(self, item: bytes) -> None: self._stream.write(item) await self._stream.drain() async def aclose(self) -> None: self._stream.close() await AsyncIOBackend.checkpoint() @dataclass(eq=False) class Process(abc.Process): _process: asyncio.subprocess.Process _stdin: StreamWriterWrapper | None _stdout: StreamReaderWrapper | None _stderr: StreamReaderWrapper | None async def aclose(self) -> None: with CancelScope(shield=True): if self._stdin: await self._stdin.aclose() if self._stdout: await self._stdout.aclose() if self._stderr: await self._stderr.aclose() try: await self.wait() except BaseException: self.kill() with CancelScope(shield=True): await self.wait() raise async def wait(self) -> int: return await self._process.wait() def terminate(self) -> None: self._process.terminate() def kill(self) -> None: self._process.kill() def send_signal(self, signal: int) -> None: self._process.send_signal(signal) @property def pid(self) -> int: return self._process.pid @property def returncode(self) -> int | None: return self._process.returncode @property def stdin(self) -> abc.ByteSendStream | None: return self._stdin @property def stdout(self) -> abc.ByteReceiveStream | None: return self._stdout @property def stderr(self) -> abc.ByteReceiveStream | None: return self._stderr def _forcibly_shutdown_process_pool_on_exit( workers: set[Process], _task: object ) -> None: """ Forcibly shuts down worker processes belonging to this event loop.""" child_watcher: asyncio.AbstractChildWatcher | None = None if sys.version_info < (3, 12): try: child_watcher = asyncio.get_event_loop_policy().get_child_watcher() except NotImplementedError: pass # Close as much as possible (w/o async/await) to avoid warnings for process in workers: if process.returncode is None: continue process._stdin._stream._transport.close() # type: ignore[union-attr] process._stdout._stream._transport.close() # type: ignore[union-attr] process._stderr._stream._transport.close() # type: ignore[union-attr] process.kill() if child_watcher: child_watcher.remove_child_handler(process.pid) async def _shutdown_process_pool_on_exit(workers: set[abc.Process]) -> None: """ Shuts down worker processes belonging to this event loop. NOTE: this only works when the event loop was started using asyncio.run() or anyio.run(). """ process: abc.Process try: await sleep(math.inf) except asyncio.CancelledError: for process in workers: if process.returncode is None: process.kill() for process in workers: await process.aclose() # # Sockets and networking # class StreamProtocol(asyncio.Protocol): read_queue: deque[bytes] read_event: asyncio.Event write_event: asyncio.Event exception: Exception | None = None is_at_eof: bool = False def connection_made(self, transport: asyncio.BaseTransport) -> None: self.read_queue = deque() self.read_event = asyncio.Event() self.write_event = asyncio.Event() self.write_event.set() cast(asyncio.Transport, transport).set_write_buffer_limits(0) def connection_lost(self, exc: Exception | None) -> None: if exc: self.exception = BrokenResourceError() self.exception.__cause__ = exc self.read_event.set() self.write_event.set() def data_received(self, data: bytes) -> None: self.read_queue.append(data) self.read_event.set() def eof_received(self) -> bool | None: self.is_at_eof = True self.read_event.set() return True def pause_writing(self) -> None: self.write_event = asyncio.Event() def resume_writing(self) -> None: self.write_event.set() class DatagramProtocol(asyncio.DatagramProtocol): read_queue: deque[tuple[bytes, IPSockAddrType]] read_event: asyncio.Event write_event: asyncio.Event exception: Exception | None = None def connection_made(self, transport: asyncio.BaseTransport) -> None: self.read_queue = deque(maxlen=100) # arbitrary value self.read_event = asyncio.Event() self.write_event = asyncio.Event() self.write_event.set() def connection_lost(self, exc: Exception | None) -> None: self.read_event.set() self.write_event.set() def datagram_received(self, data: bytes, addr: IPSockAddrType) -> None: addr = convert_ipv6_sockaddr(addr) self.read_queue.append((data, addr)) self.read_event.set() def error_received(self, exc: Exception) -> None: self.exception = exc def pause_writing(self) -> None: self.write_event.clear() def resume_writing(self) -> None: self.write_event.set() class SocketStream(abc.SocketStream): def __init__(self, transport: asyncio.Transport, protocol: StreamProtocol): self._transport = transport self._protocol = protocol self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") self._closed = False @property def _raw_socket(self) -> socket.socket: return self._transport.get_extra_info("socket") async def receive(self, max_bytes: int = 65536) -> bytes: with self._receive_guard: if ( not self._protocol.read_event.is_set() and not self._transport.is_closing() and not self._protocol.is_at_eof ): self._transport.resume_reading() await self._protocol.read_event.wait() self._transport.pause_reading() else: await AsyncIOBackend.checkpoint() try: chunk = self._protocol.read_queue.popleft() except IndexError: if self._closed: raise ClosedResourceError from None elif self._protocol.exception: raise self._protocol.exception from None else: raise EndOfStream from None if len(chunk) > max_bytes: # Split the oversized chunk chunk, leftover = chunk[:max_bytes], chunk[max_bytes:] self._protocol.read_queue.appendleft(leftover) # If the read queue is empty, clear the flag so that the next call will # block until data is available if not self._protocol.read_queue: self._protocol.read_event.clear() return chunk async def send(self, item: bytes) -> None: with self._send_guard: await AsyncIOBackend.checkpoint() if self._closed: raise ClosedResourceError elif self._protocol.exception is not None: raise self._protocol.exception try: self._transport.write(item) except RuntimeError as exc: if self._transport.is_closing(): raise BrokenResourceError from exc else: raise await self._protocol.write_event.wait() async def send_eof(self) -> None: try: self._transport.write_eof() except OSError: pass async def aclose(self) -> None: if not self._transport.is_closing(): self._closed = True try: self._transport.write_eof() except OSError: pass self._transport.close() await sleep(0) self._transport.abort() class _RawSocketMixin: _receive_future: asyncio.Future | None = None _send_future: asyncio.Future | None = None _closing = False def __init__(self, raw_socket: socket.socket): self.__raw_socket = raw_socket self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") @property def _raw_socket(self) -> socket.socket: return self.__raw_socket def _wait_until_readable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: def callback(f: object) -> None: del self._receive_future loop.remove_reader(self.__raw_socket) f = self._receive_future = asyncio.Future() loop.add_reader(self.__raw_socket, f.set_result, None) f.add_done_callback(callback) return f def _wait_until_writable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: def callback(f: object) -> None: del self._send_future loop.remove_writer(self.__raw_socket) f = self._send_future = asyncio.Future() loop.add_writer(self.__raw_socket, f.set_result, None) f.add_done_callback(callback) return f async def aclose(self) -> None: if not self._closing: self._closing = True if self.__raw_socket.fileno() != -1: self.__raw_socket.close() if self._receive_future: self._receive_future.set_result(None) if self._send_future: self._send_future.set_result(None) class UNIXSocketStream(_RawSocketMixin, abc.UNIXSocketStream): async def send_eof(self) -> None: with self._send_guard: self._raw_socket.shutdown(socket.SHUT_WR) async def receive(self, max_bytes: int = 65536) -> bytes: loop = get_running_loop() await AsyncIOBackend.checkpoint() with self._receive_guard: while True: try: data = self._raw_socket.recv(max_bytes) except BlockingIOError: await self._wait_until_readable(loop) except OSError as exc: if self._closing: raise ClosedResourceError from None else: raise BrokenResourceError from exc else: if not data: raise EndOfStream return data async def send(self, item: bytes) -> None: loop = get_running_loop() await AsyncIOBackend.checkpoint() with self._send_guard: view = memoryview(item) while view: try: bytes_sent = self._raw_socket.send(view) except BlockingIOError: await self._wait_until_writable(loop) except OSError as exc: if self._closing: raise ClosedResourceError from None else: raise BrokenResourceError from exc else: view = view[bytes_sent:] async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: if not isinstance(msglen, int) or msglen < 0: raise ValueError("msglen must be a non-negative integer") if not isinstance(maxfds, int) or maxfds < 1: raise ValueError("maxfds must be a positive integer") loop = get_running_loop() fds = array.array("i") await AsyncIOBackend.checkpoint() with self._receive_guard: while True: try: message, ancdata, flags, addr = self._raw_socket.recvmsg( msglen, socket.CMSG_LEN(maxfds * fds.itemsize) ) except BlockingIOError: await self._wait_until_readable(loop) except OSError as exc: if self._closing: raise ClosedResourceError from None else: raise BrokenResourceError from exc else: if not message and not ancdata: raise EndOfStream break for cmsg_level, cmsg_type, cmsg_data in ancdata: if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: raise RuntimeError( f"Received unexpected ancillary data; message = {message!r}, " f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" ) fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) return message, list(fds) async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: if not message: raise ValueError("message must not be empty") if not fds: raise ValueError("fds must not be empty") loop = get_running_loop() filenos: list[int] = [] for fd in fds: if isinstance(fd, int): filenos.append(fd) elif isinstance(fd, IOBase): filenos.append(fd.fileno()) fdarray = array.array("i", filenos) await AsyncIOBackend.checkpoint() with self._send_guard: while True: try: # The ignore can be removed after mypy picks up # https://github.com/python/typeshed/pull/5545 self._raw_socket.sendmsg( [message], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fdarray)] ) break except BlockingIOError: await self._wait_until_writable(loop) except OSError as exc: if self._closing: raise ClosedResourceError from None else: raise BrokenResourceError from exc class TCPSocketListener(abc.SocketListener): _accept_scope: CancelScope | None = None _closed = False def __init__(self, raw_socket: socket.socket): self.__raw_socket = raw_socket self._loop = cast(asyncio.BaseEventLoop, get_running_loop()) self._accept_guard = ResourceGuard("accepting connections from") @property def _raw_socket(self) -> socket.socket: return self.__raw_socket async def accept(self) -> abc.SocketStream: if self._closed: raise ClosedResourceError with self._accept_guard: await AsyncIOBackend.checkpoint() with CancelScope() as self._accept_scope: try: client_sock, _addr = await self._loop.sock_accept(self._raw_socket) except asyncio.CancelledError: # Workaround for https://bugs.python.org/issue41317 try: self._loop.remove_reader(self._raw_socket) except (ValueError, NotImplementedError): pass if self._closed: raise ClosedResourceError from None raise finally: self._accept_scope = None client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) transport, protocol = await self._loop.connect_accepted_socket( StreamProtocol, client_sock ) return SocketStream(transport, protocol) async def aclose(self) -> None: if self._closed: return self._closed = True if self._accept_scope: # Workaround for https://bugs.python.org/issue41317 try: self._loop.remove_reader(self._raw_socket) except (ValueError, NotImplementedError): pass self._accept_scope.cancel() await sleep(0) self._raw_socket.close() class UNIXSocketListener(abc.SocketListener): def __init__(self, raw_socket: socket.socket): self.__raw_socket = raw_socket self._loop = get_running_loop() self._accept_guard = ResourceGuard("accepting connections from") self._closed = False async def accept(self) -> abc.SocketStream: await AsyncIOBackend.checkpoint() with self._accept_guard: while True: try: client_sock, _ = self.__raw_socket.accept() client_sock.setblocking(False) return UNIXSocketStream(client_sock) except BlockingIOError: f: asyncio.Future = asyncio.Future() self._loop.add_reader(self.__raw_socket, f.set_result, None) f.add_done_callback( lambda _: self._loop.remove_reader(self.__raw_socket) ) await f except OSError as exc: if self._closed: raise ClosedResourceError from None else: raise BrokenResourceError from exc async def aclose(self) -> None: self._closed = True self.__raw_socket.close() @property def _raw_socket(self) -> socket.socket: return self.__raw_socket class UDPSocket(abc.UDPSocket): def __init__( self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol ): self._transport = transport self._protocol = protocol self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") self._closed = False @property def _raw_socket(self) -> socket.socket: return self._transport.get_extra_info("socket") async def aclose(self) -> None: if not self._transport.is_closing(): self._closed = True self._transport.close() async def receive(self) -> tuple[bytes, IPSockAddrType]: with self._receive_guard: await AsyncIOBackend.checkpoint() # If the buffer is empty, ask for more data if not self._protocol.read_queue and not self._transport.is_closing(): self._protocol.read_event.clear() await self._protocol.read_event.wait() try: return self._protocol.read_queue.popleft() except IndexError: if self._closed: raise ClosedResourceError from None else: raise BrokenResourceError from None async def send(self, item: UDPPacketType) -> None: with self._send_guard: await AsyncIOBackend.checkpoint() await self._protocol.write_event.wait() if self._closed: raise ClosedResourceError elif self._transport.is_closing(): raise BrokenResourceError else: self._transport.sendto(*item) class ConnectedUDPSocket(abc.ConnectedUDPSocket): def __init__( self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol ): self._transport = transport self._protocol = protocol self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") self._closed = False @property def _raw_socket(self) -> socket.socket: return self._transport.get_extra_info("socket") async def aclose(self) -> None: if not self._transport.is_closing(): self._closed = True self._transport.close() async def receive(self) -> bytes: with self._receive_guard: await AsyncIOBackend.checkpoint() # If the buffer is empty, ask for more data if not self._protocol.read_queue and not self._transport.is_closing(): self._protocol.read_event.clear() await self._protocol.read_event.wait() try: packet = self._protocol.read_queue.popleft() except IndexError: if self._closed: raise ClosedResourceError from None else: raise BrokenResourceError from None return packet[0] async def send(self, item: bytes) -> None: with self._send_guard: await AsyncIOBackend.checkpoint() await self._protocol.write_event.wait() if self._closed: raise ClosedResourceError elif self._transport.is_closing(): raise BrokenResourceError else: self._transport.sendto(item) class UNIXDatagramSocket(_RawSocketMixin, abc.UNIXDatagramSocket): async def receive(self) -> UNIXDatagramPacketType: loop = get_running_loop() await AsyncIOBackend.checkpoint() with self._receive_guard: while True: try: data = self._raw_socket.recvfrom(65536) except BlockingIOError: await self._wait_until_readable(loop) except OSError as exc: if self._closing: raise ClosedResourceError from None else: raise BrokenResourceError from exc else: return data async def send(self, item: UNIXDatagramPacketType) -> None: loop = get_running_loop() await AsyncIOBackend.checkpoint() with self._send_guard: while True: try: self._raw_socket.sendto(*item) except BlockingIOError: await self._wait_until_writable(loop) except OSError as exc: if self._closing: raise ClosedResourceError from None else: raise BrokenResourceError from exc else: return class ConnectedUNIXDatagramSocket(_RawSocketMixin, abc.ConnectedUNIXDatagramSocket): async def receive(self) -> bytes: loop = get_running_loop() await AsyncIOBackend.checkpoint() with self._receive_guard: while True: try: data = self._raw_socket.recv(65536) except BlockingIOError: await self._wait_until_readable(loop) except OSError as exc: if self._closing: raise ClosedResourceError from None else: raise BrokenResourceError from exc else: return data async def send(self, item: bytes) -> None: loop = get_running_loop() await AsyncIOBackend.checkpoint() with self._send_guard: while True: try: self._raw_socket.send(item) except BlockingIOError: await self._wait_until_writable(loop) except OSError as exc: if self._closing: raise ClosedResourceError from None else: raise BrokenResourceError from exc else: return _read_events: RunVar[dict[Any, asyncio.Event]] = RunVar("read_events") _write_events: RunVar[dict[Any, asyncio.Event]] = RunVar("write_events") # # Synchronization # class Event(BaseEvent): def __new__(cls) -> Event: return object.__new__(cls) def __init__(self) -> None: self._event = asyncio.Event() def set(self) -> None: self._event.set() def is_set(self) -> bool: return self._event.is_set() async def wait(self) -> None: if self.is_set(): await AsyncIOBackend.checkpoint() else: await self._event.wait() def statistics(self) -> EventStatistics: return EventStatistics(len(self._event._waiters)) class CapacityLimiter(BaseCapacityLimiter): _total_tokens: float = 0 def __new__(cls, total_tokens: float) -> CapacityLimiter: return object.__new__(cls) def __init__(self, total_tokens: float): self._borrowers: set[Any] = set() self._wait_queue: OrderedDict[Any, asyncio.Event] = OrderedDict() self.total_tokens = total_tokens async def __aenter__(self) -> None: await self.acquire() async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.release() @property def total_tokens(self) -> float: return self._total_tokens @total_tokens.setter def total_tokens(self, value: float) -> None: if not isinstance(value, int) and not math.isinf(value): raise TypeError("total_tokens must be an int or math.inf") if value < 1: raise ValueError("total_tokens must be >= 1") waiters_to_notify = max(value - self._total_tokens, 0) self._total_tokens = value # Notify waiting tasks that they have acquired the limiter while self._wait_queue and waiters_to_notify: event = self._wait_queue.popitem(last=False)[1] event.set() waiters_to_notify -= 1 @property def borrowed_tokens(self) -> int: return len(self._borrowers) @property def available_tokens(self) -> float: return self._total_tokens - len(self._borrowers) def acquire_nowait(self) -> None: self.acquire_on_behalf_of_nowait(current_task()) def acquire_on_behalf_of_nowait(self, borrower: object) -> None: if borrower in self._borrowers: raise RuntimeError( "this borrower is already holding one of this CapacityLimiter's " "tokens" ) if self._wait_queue or len(self._borrowers) >= self._total_tokens: raise WouldBlock self._borrowers.add(borrower) async def acquire(self) -> None: return await self.acquire_on_behalf_of(current_task()) async def acquire_on_behalf_of(self, borrower: object) -> None: await AsyncIOBackend.checkpoint_if_cancelled() try: self.acquire_on_behalf_of_nowait(borrower) except WouldBlock: event = asyncio.Event() self._wait_queue[borrower] = event try: await event.wait() except BaseException: self._wait_queue.pop(borrower, None) raise self._borrowers.add(borrower) else: try: await AsyncIOBackend.cancel_shielded_checkpoint() except BaseException: self.release() raise def release(self) -> None: self.release_on_behalf_of(current_task()) def release_on_behalf_of(self, borrower: object) -> None: try: self._borrowers.remove(borrower) except KeyError: raise RuntimeError( "this borrower isn't holding any of this CapacityLimiter's tokens" ) from None # Notify the next task in line if this limiter has free capacity now if self._wait_queue and len(self._borrowers) < self._total_tokens: event = self._wait_queue.popitem(last=False)[1] event.set() def statistics(self) -> CapacityLimiterStatistics: return CapacityLimiterStatistics( self.borrowed_tokens, self.total_tokens, tuple(self._borrowers), len(self._wait_queue), ) _default_thread_limiter: RunVar[CapacityLimiter] = RunVar("_default_thread_limiter") # # Operating system signals # class _SignalReceiver: def __init__(self, signals: tuple[Signals, ...]): self._signals = signals self._loop = get_running_loop() self._signal_queue: deque[Signals] = deque() self._future: asyncio.Future = asyncio.Future() self._handled_signals: set[Signals] = set() def _deliver(self, signum: Signals) -> None: self._signal_queue.append(signum) if not self._future.done(): self._future.set_result(None) def __enter__(self) -> _SignalReceiver: for sig in set(self._signals): self._loop.add_signal_handler(sig, self._deliver, sig) self._handled_signals.add(sig) return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: for sig in self._handled_signals: self._loop.remove_signal_handler(sig) return None def __aiter__(self) -> _SignalReceiver: return self async def __anext__(self) -> Signals: await AsyncIOBackend.checkpoint() if not self._signal_queue: self._future = asyncio.Future() await self._future return self._signal_queue.popleft() # # Testing and debugging # class AsyncIOTaskInfo(TaskInfo): def __init__(self, task: asyncio.Task): task_state = _task_states.get(task) if task_state is None: parent_id = None else: parent_id = task_state.parent_id super().__init__(id(task), parent_id, task.get_name(), task.get_coro()) self._task = weakref.ref(task) def has_pending_cancellation(self) -> bool: if not (task := self._task()): # If the task isn't around anymore, it won't have a pending cancellation return False if sys.version_info >= (3, 11): if task.cancelling(): return True elif ( isinstance(task._fut_waiter, asyncio.Future) and task._fut_waiter.cancelled() ): return True if task_state := _task_states.get(task): if cancel_scope := task_state.cancel_scope: return cancel_scope.cancel_called or cancel_scope._parent_cancelled() return False class TestRunner(abc.TestRunner): _send_stream: MemoryObjectSendStream[tuple[Awaitable[Any], asyncio.Future[Any]]] def __init__( self, *, debug: bool | None = None, use_uvloop: bool = False, loop_factory: Callable[[], AbstractEventLoop] | None = None, ) -> None: if use_uvloop and loop_factory is None: import uvloop loop_factory = uvloop.new_event_loop self._runner = Runner(debug=debug, loop_factory=loop_factory) self._exceptions: list[BaseException] = [] self._runner_task: asyncio.Task | None = None def __enter__(self) -> TestRunner: self._runner.__enter__() self.get_loop().set_exception_handler(self._exception_handler) return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self._runner.__exit__(exc_type, exc_val, exc_tb) def get_loop(self) -> AbstractEventLoop: return self._runner.get_loop() def _exception_handler( self, loop: asyncio.AbstractEventLoop, context: dict[str, Any] ) -> None: if isinstance(context.get("exception"), Exception): self._exceptions.append(context["exception"]) else: loop.default_exception_handler(context) def _raise_async_exceptions(self) -> None: # Re-raise any exceptions raised in asynchronous callbacks if self._exceptions: exceptions, self._exceptions = self._exceptions, [] if len(exceptions) == 1: raise exceptions[0] elif exceptions: raise BaseExceptionGroup( "Multiple exceptions occurred in asynchronous callbacks", exceptions ) async def _run_tests_and_fixtures( self, receive_stream: MemoryObjectReceiveStream[ tuple[Awaitable[T_Retval], asyncio.Future[T_Retval]] ], ) -> None: with receive_stream, self._send_stream: async for coro, future in receive_stream: try: retval = await coro except BaseException as exc: if not future.cancelled(): future.set_exception(exc) else: if not future.cancelled(): future.set_result(retval) async def _call_in_runner_task( self, func: Callable[P, Awaitable[T_Retval]], *args: P.args, **kwargs: P.kwargs, ) -> T_Retval: if not self._runner_task: self._send_stream, receive_stream = create_memory_object_stream[ Tuple[Awaitable[Any], asyncio.Future] ](1) self._runner_task = self.get_loop().create_task( self._run_tests_and_fixtures(receive_stream) ) coro = func(*args, **kwargs) future: asyncio.Future[T_Retval] = self.get_loop().create_future() self._send_stream.send_nowait((coro, future)) return await future def run_asyncgen_fixture( self, fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], kwargs: dict[str, Any], ) -> Iterable[T_Retval]: asyncgen = fixture_func(**kwargs) fixturevalue: T_Retval = self.get_loop().run_until_complete( self._call_in_runner_task(asyncgen.asend, None) ) self._raise_async_exceptions() yield fixturevalue try: self.get_loop().run_until_complete( self._call_in_runner_task(asyncgen.asend, None) ) except StopAsyncIteration: self._raise_async_exceptions() else: self.get_loop().run_until_complete(asyncgen.aclose()) raise RuntimeError("Async generator fixture did not stop") def run_fixture( self, fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], kwargs: dict[str, Any], ) -> T_Retval: retval = self.get_loop().run_until_complete( self._call_in_runner_task(fixture_func, **kwargs) ) self._raise_async_exceptions() return retval def run_test( self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] ) -> None: try: self.get_loop().run_until_complete( self._call_in_runner_task(test_func, **kwargs) ) except Exception as exc: self._exceptions.append(exc) self._raise_async_exceptions() class AsyncIOBackend(AsyncBackend): @classmethod def run( cls, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], args: tuple[Unpack[PosArgsT]], kwargs: dict[str, Any], options: dict[str, Any], ) -> T_Retval: @wraps(func) async def wrapper() -> T_Retval: task = cast(asyncio.Task, current_task()) task.set_name(get_callable_name(func)) _task_states[task] = TaskState(None, None) try: return await func(*args) finally: del _task_states[task] debug = options.get("debug", None) loop_factory = options.get("loop_factory", None) if loop_factory is None and options.get("use_uvloop", False): import uvloop loop_factory = uvloop.new_event_loop with Runner(debug=debug, loop_factory=loop_factory) as runner: return runner.run(wrapper()) @classmethod def current_token(cls) -> object: return get_running_loop() @classmethod def current_time(cls) -> float: return get_running_loop().time() @classmethod def cancelled_exception_class(cls) -> type[BaseException]: return CancelledError @classmethod async def checkpoint(cls) -> None: await sleep(0) @classmethod async def checkpoint_if_cancelled(cls) -> None: task = current_task() if task is None: return try: cancel_scope = _task_states[task].cancel_scope except KeyError: return while cancel_scope: if cancel_scope.cancel_called: await sleep(0) elif cancel_scope.shield: break else: cancel_scope = cancel_scope._parent_scope @classmethod async def cancel_shielded_checkpoint(cls) -> None: with CancelScope(shield=True): await sleep(0) @classmethod async def sleep(cls, delay: float) -> None: await sleep(delay) @classmethod def create_cancel_scope( cls, *, deadline: float = math.inf, shield: bool = False ) -> CancelScope: return CancelScope(deadline=deadline, shield=shield) @classmethod def current_effective_deadline(cls) -> float: try: cancel_scope = _task_states[ current_task() # type: ignore[index] ].cancel_scope except KeyError: return math.inf deadline = math.inf while cancel_scope: deadline = min(deadline, cancel_scope.deadline) if cancel_scope._cancel_called: deadline = -math.inf break elif cancel_scope.shield: break else: cancel_scope = cancel_scope._parent_scope return deadline @classmethod def create_task_group(cls) -> abc.TaskGroup: return TaskGroup() @classmethod def create_event(cls) -> abc.Event: return Event() @classmethod def create_capacity_limiter(cls, total_tokens: float) -> abc.CapacityLimiter: return CapacityLimiter(total_tokens) @classmethod async def run_sync_in_worker_thread( cls, func: Callable[[Unpack[PosArgsT]], T_Retval], args: tuple[Unpack[PosArgsT]], abandon_on_cancel: bool = False, limiter: abc.CapacityLimiter | None = None, ) -> T_Retval: await cls.checkpoint() # If this is the first run in this event loop thread, set up the necessary # variables try: idle_workers = _threadpool_idle_workers.get() workers = _threadpool_workers.get() except LookupError: idle_workers = deque() workers = set() _threadpool_idle_workers.set(idle_workers) _threadpool_workers.set(workers) async with limiter or cls.current_default_thread_limiter(): with CancelScope(shield=not abandon_on_cancel) as scope: future: asyncio.Future = asyncio.Future() root_task = find_root_task() if not idle_workers: worker = WorkerThread(root_task, workers, idle_workers) worker.start() workers.add(worker) root_task.add_done_callback(worker.stop) else: worker = idle_workers.pop() # Prune any other workers that have been idle for MAX_IDLE_TIME # seconds or longer now = cls.current_time() while idle_workers: if ( now - idle_workers[0].idle_since < WorkerThread.MAX_IDLE_TIME ): break expired_worker = idle_workers.popleft() expired_worker.root_task.remove_done_callback( expired_worker.stop ) expired_worker.stop() context = copy_context() context.run(sniffio.current_async_library_cvar.set, None) if abandon_on_cancel or scope._parent_scope is None: worker_scope = scope else: worker_scope = scope._parent_scope worker.queue.put_nowait((context, func, args, future, worker_scope)) return await future @classmethod def check_cancelled(cls) -> None: scope: CancelScope | None = threadlocals.current_cancel_scope while scope is not None: if scope.cancel_called: raise CancelledError(f"Cancelled by cancel scope {id(scope):x}") if scope.shield: return scope = scope._parent_scope @classmethod def run_async_from_thread( cls, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], args: tuple[Unpack[PosArgsT]], token: object, ) -> T_Retval: async def task_wrapper(scope: CancelScope) -> T_Retval: __tracebackhide__ = True task = cast(asyncio.Task, current_task()) _task_states[task] = TaskState(None, scope) scope._tasks.add(task) try: return await func(*args) except CancelledError as exc: raise concurrent.futures.CancelledError(str(exc)) from None finally: scope._tasks.discard(task) loop = cast(AbstractEventLoop, token) context = copy_context() context.run(sniffio.current_async_library_cvar.set, "asyncio") wrapper = task_wrapper(threadlocals.current_cancel_scope) f: concurrent.futures.Future[T_Retval] = context.run( asyncio.run_coroutine_threadsafe, wrapper, loop ) return f.result() @classmethod def run_sync_from_thread( cls, func: Callable[[Unpack[PosArgsT]], T_Retval], args: tuple[Unpack[PosArgsT]], token: object, ) -> T_Retval: @wraps(func) def wrapper() -> None: try: sniffio.current_async_library_cvar.set("asyncio") f.set_result(func(*args)) except BaseException as exc: f.set_exception(exc) if not isinstance(exc, Exception): raise f: concurrent.futures.Future[T_Retval] = Future() loop = cast(AbstractEventLoop, token) loop.call_soon_threadsafe(wrapper) return f.result() @classmethod def create_blocking_portal(cls) -> abc.BlockingPortal: return BlockingPortal() @classmethod async def open_process( cls, command: str | bytes | Sequence[str | bytes], *, shell: bool, stdin: int | IO[Any] | None, stdout: int | IO[Any] | None, stderr: int | IO[Any] | None, cwd: str | bytes | PathLike | None = None, env: Mapping[str, str] | None = None, start_new_session: bool = False, ) -> Process: await cls.checkpoint() if shell: process = await asyncio.create_subprocess_shell( cast("str | bytes", command), stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd, env=env, start_new_session=start_new_session, ) else: process = await asyncio.create_subprocess_exec( *command, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd, env=env, start_new_session=start_new_session, ) stdin_stream = StreamWriterWrapper(process.stdin) if process.stdin else None stdout_stream = StreamReaderWrapper(process.stdout) if process.stdout else None stderr_stream = StreamReaderWrapper(process.stderr) if process.stderr else None return Process(process, stdin_stream, stdout_stream, stderr_stream) @classmethod def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: create_task( _shutdown_process_pool_on_exit(workers), name="AnyIO process pool shutdown task", ) find_root_task().add_done_callback( partial(_forcibly_shutdown_process_pool_on_exit, workers) ) @classmethod async def connect_tcp( cls, host: str, port: int, local_address: IPSockAddrType | None = None ) -> abc.SocketStream: transport, protocol = cast( Tuple[asyncio.Transport, StreamProtocol], await get_running_loop().create_connection( StreamProtocol, host, port, local_addr=local_address ), ) transport.pause_reading() return SocketStream(transport, protocol) @classmethod async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: await cls.checkpoint() loop = get_running_loop() raw_socket = socket.socket(socket.AF_UNIX) raw_socket.setblocking(False) while True: try: raw_socket.connect(path) except BlockingIOError: f: asyncio.Future = asyncio.Future() loop.add_writer(raw_socket, f.set_result, None) f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) await f except BaseException: raw_socket.close() raise else: return UNIXSocketStream(raw_socket) @classmethod def create_tcp_listener(cls, sock: socket.socket) -> SocketListener: return TCPSocketListener(sock) @classmethod def create_unix_listener(cls, sock: socket.socket) -> SocketListener: return UNIXSocketListener(sock) @classmethod async def create_udp_socket( cls, family: AddressFamily, local_address: IPSockAddrType | None, remote_address: IPSockAddrType | None, reuse_port: bool, ) -> UDPSocket | ConnectedUDPSocket: transport, protocol = await get_running_loop().create_datagram_endpoint( DatagramProtocol, local_addr=local_address, remote_addr=remote_address, family=family, reuse_port=reuse_port, ) if protocol.exception: transport.close() raise protocol.exception if not remote_address: return UDPSocket(transport, protocol) else: return ConnectedUDPSocket(transport, protocol) @classmethod async def create_unix_datagram_socket( # type: ignore[override] cls, raw_socket: socket.socket, remote_path: str | bytes | None ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: await cls.checkpoint() loop = get_running_loop() if remote_path: while True: try: raw_socket.connect(remote_path) except BlockingIOError: f: asyncio.Future = asyncio.Future() loop.add_writer(raw_socket, f.set_result, None) f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) await f except BaseException: raw_socket.close() raise else: return ConnectedUNIXDatagramSocket(raw_socket) else: return UNIXDatagramSocket(raw_socket) @classmethod async def getaddrinfo( cls, host: bytes | str | None, port: str | int | None, *, family: int | AddressFamily = 0, type: int | SocketKind = 0, proto: int = 0, flags: int = 0, ) -> list[ tuple[ AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int], ] ]: return await get_running_loop().getaddrinfo( host, port, family=family, type=type, proto=proto, flags=flags ) @classmethod async def getnameinfo( cls, sockaddr: IPSockAddrType, flags: int = 0 ) -> tuple[str, str]: return await get_running_loop().getnameinfo(sockaddr, flags) @classmethod async def wait_socket_readable(cls, sock: socket.socket) -> None: await cls.checkpoint() try: read_events = _read_events.get() except LookupError: read_events = {} _read_events.set(read_events) if read_events.get(sock): raise BusyResourceError("reading from") from None loop = get_running_loop() event = read_events[sock] = asyncio.Event() loop.add_reader(sock, event.set) try: await event.wait() finally: if read_events.pop(sock, None) is not None: loop.remove_reader(sock) readable = True else: readable = False if not readable: raise ClosedResourceError @classmethod async def wait_socket_writable(cls, sock: socket.socket) -> None: await cls.checkpoint() try: write_events = _write_events.get() except LookupError: write_events = {} _write_events.set(write_events) if write_events.get(sock): raise BusyResourceError("writing to") from None loop = get_running_loop() event = write_events[sock] = asyncio.Event() loop.add_writer(sock.fileno(), event.set) try: await event.wait() finally: if write_events.pop(sock, None) is not None: loop.remove_writer(sock) writable = True else: writable = False if not writable: raise ClosedResourceError @classmethod def current_default_thread_limiter(cls) -> CapacityLimiter: try: return _default_thread_limiter.get() except LookupError: limiter = CapacityLimiter(40) _default_thread_limiter.set(limiter) return limiter @classmethod def open_signal_receiver( cls, *signals: Signals ) -> ContextManager[AsyncIterator[Signals]]: return _SignalReceiver(signals) @classmethod def get_current_task(cls) -> TaskInfo: return AsyncIOTaskInfo(current_task()) # type: ignore[arg-type] @classmethod def get_running_tasks(cls) -> Sequence[TaskInfo]: return [AsyncIOTaskInfo(task) for task in all_tasks() if not task.done()] @classmethod async def wait_all_tasks_blocked(cls) -> None: await cls.checkpoint() this_task = current_task() while True: for task in all_tasks(): if task is this_task: continue waiter = task._fut_waiter # type: ignore[attr-defined] if waiter is None or waiter.done(): await sleep(0.1) break else: return @classmethod def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: return TestRunner(**options) backend_class = AsyncIOBackend anyio-4.4.0/src/anyio/_backends/_trio.py000066400000000000000000001061051462472665400201660ustar00rootroot00000000000000from __future__ import annotations import array import math import socket import sys import types import weakref from collections.abc import AsyncIterator, Iterable from concurrent.futures import Future from dataclasses import dataclass from functools import partial from io import IOBase from os import PathLike from signal import Signals from socket import AddressFamily, SocketKind from types import TracebackType from typing import ( IO, Any, AsyncGenerator, Awaitable, Callable, Collection, ContextManager, Coroutine, Generic, Mapping, NoReturn, Sequence, TypeVar, cast, overload, ) import trio.from_thread import trio.lowlevel from outcome import Error, Outcome, Value from trio.lowlevel import ( current_root_task, current_task, wait_readable, wait_writable, ) from trio.socket import SocketType as TrioSocketType from trio.to_thread import run_sync from .. import CapacityLimiterStatistics, EventStatistics, TaskInfo, abc from .._core._eventloop import claim_worker_thread from .._core._exceptions import ( BrokenResourceError, BusyResourceError, ClosedResourceError, EndOfStream, ) from .._core._sockets import convert_ipv6_sockaddr from .._core._streams import create_memory_object_stream from .._core._synchronization import CapacityLimiter as BaseCapacityLimiter from .._core._synchronization import Event as BaseEvent from .._core._synchronization import ResourceGuard from .._core._tasks import CancelScope as BaseCancelScope from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType from ..abc._eventloop import AsyncBackend from ..streams.memory import MemoryObjectSendStream if sys.version_info >= (3, 10): from typing import ParamSpec else: from typing_extensions import ParamSpec if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: from exceptiongroup import BaseExceptionGroup from typing_extensions import TypeVarTuple, Unpack T = TypeVar("T") T_Retval = TypeVar("T_Retval") T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType) PosArgsT = TypeVarTuple("PosArgsT") P = ParamSpec("P") # # Event loop # RunVar = trio.lowlevel.RunVar # # Timeouts and cancellation # class CancelScope(BaseCancelScope): def __new__( cls, original: trio.CancelScope | None = None, **kwargs: object ) -> CancelScope: return object.__new__(cls) def __init__(self, original: trio.CancelScope | None = None, **kwargs: Any) -> None: self.__original = original or trio.CancelScope(**kwargs) def __enter__(self) -> CancelScope: self.__original.__enter__() return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: # https://github.com/python-trio/trio-typing/pull/79 return self.__original.__exit__(exc_type, exc_val, exc_tb) def cancel(self) -> None: self.__original.cancel() @property def deadline(self) -> float: return self.__original.deadline @deadline.setter def deadline(self, value: float) -> None: self.__original.deadline = value @property def cancel_called(self) -> bool: return self.__original.cancel_called @property def cancelled_caught(self) -> bool: return self.__original.cancelled_caught @property def shield(self) -> bool: return self.__original.shield @shield.setter def shield(self, value: bool) -> None: self.__original.shield = value # # Task groups # class TaskGroup(abc.TaskGroup): def __init__(self) -> None: self._active = False self._nursery_manager = trio.open_nursery(strict_exception_groups=True) self.cancel_scope = None # type: ignore[assignment] async def __aenter__(self) -> TaskGroup: self._active = True self._nursery = await self._nursery_manager.__aenter__() self.cancel_scope = CancelScope(self._nursery.cancel_scope) return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: try: return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb) except BaseExceptionGroup as exc: _, rest = exc.split(trio.Cancelled) if not rest: cancelled_exc = trio.Cancelled._create() raise cancelled_exc from exc raise finally: self._active = False def start_soon( self, func: Callable[[Unpack[PosArgsT]], Awaitable[Any]], *args: Unpack[PosArgsT], name: object = None, ) -> None: if not self._active: raise RuntimeError( "This task group is not active; no new tasks can be started." ) self._nursery.start_soon(func, *args, name=name) async def start( self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None ) -> Any: if not self._active: raise RuntimeError( "This task group is not active; no new tasks can be started." ) return await self._nursery.start(func, *args, name=name) # # Threads # class BlockingPortal(abc.BlockingPortal): def __new__(cls) -> BlockingPortal: return object.__new__(cls) def __init__(self) -> None: super().__init__() self._token = trio.lowlevel.current_trio_token() def _spawn_task_from_thread( self, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], args: tuple[Unpack[PosArgsT]], kwargs: dict[str, Any], name: object, future: Future[T_Retval], ) -> None: trio.from_thread.run_sync( partial(self._task_group.start_soon, name=name), self._call_func, func, args, kwargs, future, trio_token=self._token, ) # # Subprocesses # @dataclass(eq=False) class ReceiveStreamWrapper(abc.ByteReceiveStream): _stream: trio.abc.ReceiveStream async def receive(self, max_bytes: int | None = None) -> bytes: try: data = await self._stream.receive_some(max_bytes) except trio.ClosedResourceError as exc: raise ClosedResourceError from exc.__cause__ except trio.BrokenResourceError as exc: raise BrokenResourceError from exc.__cause__ if data: return data else: raise EndOfStream async def aclose(self) -> None: await self._stream.aclose() @dataclass(eq=False) class SendStreamWrapper(abc.ByteSendStream): _stream: trio.abc.SendStream async def send(self, item: bytes) -> None: try: await self._stream.send_all(item) except trio.ClosedResourceError as exc: raise ClosedResourceError from exc.__cause__ except trio.BrokenResourceError as exc: raise BrokenResourceError from exc.__cause__ async def aclose(self) -> None: await self._stream.aclose() @dataclass(eq=False) class Process(abc.Process): _process: trio.Process _stdin: abc.ByteSendStream | None _stdout: abc.ByteReceiveStream | None _stderr: abc.ByteReceiveStream | None async def aclose(self) -> None: with CancelScope(shield=True): if self._stdin: await self._stdin.aclose() if self._stdout: await self._stdout.aclose() if self._stderr: await self._stderr.aclose() try: await self.wait() except BaseException: self.kill() with CancelScope(shield=True): await self.wait() raise async def wait(self) -> int: return await self._process.wait() def terminate(self) -> None: self._process.terminate() def kill(self) -> None: self._process.kill() def send_signal(self, signal: Signals) -> None: self._process.send_signal(signal) @property def pid(self) -> int: return self._process.pid @property def returncode(self) -> int | None: return self._process.returncode @property def stdin(self) -> abc.ByteSendStream | None: return self._stdin @property def stdout(self) -> abc.ByteReceiveStream | None: return self._stdout @property def stderr(self) -> abc.ByteReceiveStream | None: return self._stderr class _ProcessPoolShutdownInstrument(trio.abc.Instrument): def after_run(self) -> None: super().after_run() current_default_worker_process_limiter: trio.lowlevel.RunVar = RunVar( "current_default_worker_process_limiter" ) async def _shutdown_process_pool(workers: set[abc.Process]) -> None: try: await trio.sleep(math.inf) except trio.Cancelled: for process in workers: if process.returncode is None: process.kill() with CancelScope(shield=True): for process in workers: await process.aclose() # # Sockets and networking # class _TrioSocketMixin(Generic[T_SockAddr]): def __init__(self, trio_socket: TrioSocketType) -> None: self._trio_socket = trio_socket self._closed = False def _check_closed(self) -> None: if self._closed: raise ClosedResourceError if self._trio_socket.fileno() < 0: raise BrokenResourceError @property def _raw_socket(self) -> socket.socket: return self._trio_socket._sock # type: ignore[attr-defined] async def aclose(self) -> None: if self._trio_socket.fileno() >= 0: self._closed = True self._trio_socket.close() def _convert_socket_error(self, exc: BaseException) -> NoReturn: if isinstance(exc, trio.ClosedResourceError): raise ClosedResourceError from exc elif self._trio_socket.fileno() < 0 and self._closed: raise ClosedResourceError from None elif isinstance(exc, OSError): raise BrokenResourceError from exc else: raise exc class SocketStream(_TrioSocketMixin, abc.SocketStream): def __init__(self, trio_socket: TrioSocketType) -> None: super().__init__(trio_socket) self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") async def receive(self, max_bytes: int = 65536) -> bytes: with self._receive_guard: try: data = await self._trio_socket.recv(max_bytes) except BaseException as exc: self._convert_socket_error(exc) if data: return data else: raise EndOfStream async def send(self, item: bytes) -> None: with self._send_guard: view = memoryview(item) while view: try: bytes_sent = await self._trio_socket.send(view) except BaseException as exc: self._convert_socket_error(exc) view = view[bytes_sent:] async def send_eof(self) -> None: self._trio_socket.shutdown(socket.SHUT_WR) class UNIXSocketStream(SocketStream, abc.UNIXSocketStream): async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: if not isinstance(msglen, int) or msglen < 0: raise ValueError("msglen must be a non-negative integer") if not isinstance(maxfds, int) or maxfds < 1: raise ValueError("maxfds must be a positive integer") fds = array.array("i") await trio.lowlevel.checkpoint() with self._receive_guard: while True: try: message, ancdata, flags, addr = await self._trio_socket.recvmsg( msglen, socket.CMSG_LEN(maxfds * fds.itemsize) ) except BaseException as exc: self._convert_socket_error(exc) else: if not message and not ancdata: raise EndOfStream break for cmsg_level, cmsg_type, cmsg_data in ancdata: if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: raise RuntimeError( f"Received unexpected ancillary data; message = {message!r}, " f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" ) fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) return message, list(fds) async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: if not message: raise ValueError("message must not be empty") if not fds: raise ValueError("fds must not be empty") filenos: list[int] = [] for fd in fds: if isinstance(fd, int): filenos.append(fd) elif isinstance(fd, IOBase): filenos.append(fd.fileno()) fdarray = array.array("i", filenos) await trio.lowlevel.checkpoint() with self._send_guard: while True: try: await self._trio_socket.sendmsg( [message], [ ( socket.SOL_SOCKET, socket.SCM_RIGHTS, fdarray, ) ], ) break except BaseException as exc: self._convert_socket_error(exc) class TCPSocketListener(_TrioSocketMixin, abc.SocketListener): def __init__(self, raw_socket: socket.socket): super().__init__(trio.socket.from_stdlib_socket(raw_socket)) self._accept_guard = ResourceGuard("accepting connections from") async def accept(self) -> SocketStream: with self._accept_guard: try: trio_socket, _addr = await self._trio_socket.accept() except BaseException as exc: self._convert_socket_error(exc) trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) return SocketStream(trio_socket) class UNIXSocketListener(_TrioSocketMixin, abc.SocketListener): def __init__(self, raw_socket: socket.socket): super().__init__(trio.socket.from_stdlib_socket(raw_socket)) self._accept_guard = ResourceGuard("accepting connections from") async def accept(self) -> UNIXSocketStream: with self._accept_guard: try: trio_socket, _addr = await self._trio_socket.accept() except BaseException as exc: self._convert_socket_error(exc) return UNIXSocketStream(trio_socket) class UDPSocket(_TrioSocketMixin[IPSockAddrType], abc.UDPSocket): def __init__(self, trio_socket: TrioSocketType) -> None: super().__init__(trio_socket) self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") async def receive(self) -> tuple[bytes, IPSockAddrType]: with self._receive_guard: try: data, addr = await self._trio_socket.recvfrom(65536) return data, convert_ipv6_sockaddr(addr) except BaseException as exc: self._convert_socket_error(exc) async def send(self, item: UDPPacketType) -> None: with self._send_guard: try: await self._trio_socket.sendto(*item) except BaseException as exc: self._convert_socket_error(exc) class ConnectedUDPSocket(_TrioSocketMixin[IPSockAddrType], abc.ConnectedUDPSocket): def __init__(self, trio_socket: TrioSocketType) -> None: super().__init__(trio_socket) self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") async def receive(self) -> bytes: with self._receive_guard: try: return await self._trio_socket.recv(65536) except BaseException as exc: self._convert_socket_error(exc) async def send(self, item: bytes) -> None: with self._send_guard: try: await self._trio_socket.send(item) except BaseException as exc: self._convert_socket_error(exc) class UNIXDatagramSocket(_TrioSocketMixin[str], abc.UNIXDatagramSocket): def __init__(self, trio_socket: TrioSocketType) -> None: super().__init__(trio_socket) self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") async def receive(self) -> UNIXDatagramPacketType: with self._receive_guard: try: data, addr = await self._trio_socket.recvfrom(65536) return data, addr except BaseException as exc: self._convert_socket_error(exc) async def send(self, item: UNIXDatagramPacketType) -> None: with self._send_guard: try: await self._trio_socket.sendto(*item) except BaseException as exc: self._convert_socket_error(exc) class ConnectedUNIXDatagramSocket( _TrioSocketMixin[str], abc.ConnectedUNIXDatagramSocket ): def __init__(self, trio_socket: TrioSocketType) -> None: super().__init__(trio_socket) self._receive_guard = ResourceGuard("reading from") self._send_guard = ResourceGuard("writing to") async def receive(self) -> bytes: with self._receive_guard: try: return await self._trio_socket.recv(65536) except BaseException as exc: self._convert_socket_error(exc) async def send(self, item: bytes) -> None: with self._send_guard: try: await self._trio_socket.send(item) except BaseException as exc: self._convert_socket_error(exc) # # Synchronization # class Event(BaseEvent): def __new__(cls) -> Event: return object.__new__(cls) def __init__(self) -> None: self.__original = trio.Event() def is_set(self) -> bool: return self.__original.is_set() async def wait(self) -> None: return await self.__original.wait() def statistics(self) -> EventStatistics: orig_statistics = self.__original.statistics() return EventStatistics(tasks_waiting=orig_statistics.tasks_waiting) def set(self) -> None: self.__original.set() class CapacityLimiter(BaseCapacityLimiter): def __new__( cls, total_tokens: float | None = None, *, original: trio.CapacityLimiter | None = None, ) -> CapacityLimiter: return object.__new__(cls) def __init__( self, total_tokens: float | None = None, *, original: trio.CapacityLimiter | None = None, ) -> None: if original is not None: self.__original = original else: assert total_tokens is not None self.__original = trio.CapacityLimiter(total_tokens) async def __aenter__(self) -> None: return await self.__original.__aenter__() async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self.__original.__aexit__(exc_type, exc_val, exc_tb) @property def total_tokens(self) -> float: return self.__original.total_tokens @total_tokens.setter def total_tokens(self, value: float) -> None: self.__original.total_tokens = value @property def borrowed_tokens(self) -> int: return self.__original.borrowed_tokens @property def available_tokens(self) -> float: return self.__original.available_tokens def acquire_nowait(self) -> None: self.__original.acquire_nowait() def acquire_on_behalf_of_nowait(self, borrower: object) -> None: self.__original.acquire_on_behalf_of_nowait(borrower) async def acquire(self) -> None: await self.__original.acquire() async def acquire_on_behalf_of(self, borrower: object) -> None: await self.__original.acquire_on_behalf_of(borrower) def release(self) -> None: return self.__original.release() def release_on_behalf_of(self, borrower: object) -> None: return self.__original.release_on_behalf_of(borrower) def statistics(self) -> CapacityLimiterStatistics: orig = self.__original.statistics() return CapacityLimiterStatistics( borrowed_tokens=orig.borrowed_tokens, total_tokens=orig.total_tokens, borrowers=tuple(orig.borrowers), tasks_waiting=orig.tasks_waiting, ) _capacity_limiter_wrapper: trio.lowlevel.RunVar = RunVar("_capacity_limiter_wrapper") # # Signal handling # class _SignalReceiver: _iterator: AsyncIterator[int] def __init__(self, signals: tuple[Signals, ...]): self._signals = signals def __enter__(self) -> _SignalReceiver: self._cm = trio.open_signal_receiver(*self._signals) self._iterator = self._cm.__enter__() return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: return self._cm.__exit__(exc_type, exc_val, exc_tb) def __aiter__(self) -> _SignalReceiver: return self async def __anext__(self) -> Signals: signum = await self._iterator.__anext__() return Signals(signum) # # Testing and debugging # class TestRunner(abc.TestRunner): def __init__(self, **options: Any) -> None: from queue import Queue self._call_queue: Queue[Callable[[], object]] = Queue() self._send_stream: MemoryObjectSendStream | None = None self._options = options def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None, ) -> None: if self._send_stream: self._send_stream.close() while self._send_stream is not None: self._call_queue.get()() async def _run_tests_and_fixtures(self) -> None: self._send_stream, receive_stream = create_memory_object_stream(1) with receive_stream: async for coro, outcome_holder in receive_stream: try: retval = await coro except BaseException as exc: outcome_holder.append(Error(exc)) else: outcome_holder.append(Value(retval)) def _main_task_finished(self, outcome: object) -> None: self._send_stream = None def _call_in_runner_task( self, func: Callable[P, Awaitable[T_Retval]], *args: P.args, **kwargs: P.kwargs, ) -> T_Retval: if self._send_stream is None: trio.lowlevel.start_guest_run( self._run_tests_and_fixtures, run_sync_soon_threadsafe=self._call_queue.put, done_callback=self._main_task_finished, **self._options, ) while self._send_stream is None: self._call_queue.get()() outcome_holder: list[Outcome] = [] self._send_stream.send_nowait((func(*args, **kwargs), outcome_holder)) while not outcome_holder: self._call_queue.get()() return outcome_holder[0].unwrap() def run_asyncgen_fixture( self, fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], kwargs: dict[str, Any], ) -> Iterable[T_Retval]: asyncgen = fixture_func(**kwargs) fixturevalue: T_Retval = self._call_in_runner_task(asyncgen.asend, None) yield fixturevalue try: self._call_in_runner_task(asyncgen.asend, None) except StopAsyncIteration: pass else: self._call_in_runner_task(asyncgen.aclose) raise RuntimeError("Async generator fixture did not stop") def run_fixture( self, fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], kwargs: dict[str, Any], ) -> T_Retval: return self._call_in_runner_task(fixture_func, **kwargs) def run_test( self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] ) -> None: self._call_in_runner_task(test_func, **kwargs) class TrioTaskInfo(TaskInfo): def __init__(self, task: trio.lowlevel.Task): parent_id = None if task.parent_nursery and task.parent_nursery.parent_task: parent_id = id(task.parent_nursery.parent_task) super().__init__(id(task), parent_id, task.name, task.coro) self._task = weakref.proxy(task) def has_pending_cancellation(self) -> bool: try: return self._task._cancel_status.effectively_cancelled except ReferenceError: # If the task is no longer around, it surely doesn't have a cancellation # pending return False class TrioBackend(AsyncBackend): @classmethod def run( cls, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], args: tuple[Unpack[PosArgsT]], kwargs: dict[str, Any], options: dict[str, Any], ) -> T_Retval: return trio.run(func, *args) @classmethod def current_token(cls) -> object: return trio.lowlevel.current_trio_token() @classmethod def current_time(cls) -> float: return trio.current_time() @classmethod def cancelled_exception_class(cls) -> type[BaseException]: return trio.Cancelled @classmethod async def checkpoint(cls) -> None: await trio.lowlevel.checkpoint() @classmethod async def checkpoint_if_cancelled(cls) -> None: await trio.lowlevel.checkpoint_if_cancelled() @classmethod async def cancel_shielded_checkpoint(cls) -> None: await trio.lowlevel.cancel_shielded_checkpoint() @classmethod async def sleep(cls, delay: float) -> None: await trio.sleep(delay) @classmethod def create_cancel_scope( cls, *, deadline: float = math.inf, shield: bool = False ) -> abc.CancelScope: return CancelScope(deadline=deadline, shield=shield) @classmethod def current_effective_deadline(cls) -> float: return trio.current_effective_deadline() @classmethod def create_task_group(cls) -> abc.TaskGroup: return TaskGroup() @classmethod def create_event(cls) -> abc.Event: return Event() @classmethod def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: return CapacityLimiter(total_tokens) @classmethod async def run_sync_in_worker_thread( cls, func: Callable[[Unpack[PosArgsT]], T_Retval], args: tuple[Unpack[PosArgsT]], abandon_on_cancel: bool = False, limiter: abc.CapacityLimiter | None = None, ) -> T_Retval: def wrapper() -> T_Retval: with claim_worker_thread(TrioBackend, token): return func(*args) token = TrioBackend.current_token() return await run_sync( wrapper, abandon_on_cancel=abandon_on_cancel, limiter=cast(trio.CapacityLimiter, limiter), ) @classmethod def check_cancelled(cls) -> None: trio.from_thread.check_cancelled() @classmethod def run_async_from_thread( cls, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], args: tuple[Unpack[PosArgsT]], token: object, ) -> T_Retval: return trio.from_thread.run(func, *args) @classmethod def run_sync_from_thread( cls, func: Callable[[Unpack[PosArgsT]], T_Retval], args: tuple[Unpack[PosArgsT]], token: object, ) -> T_Retval: return trio.from_thread.run_sync(func, *args) @classmethod def create_blocking_portal(cls) -> abc.BlockingPortal: return BlockingPortal() @classmethod async def open_process( cls, command: str | bytes | Sequence[str | bytes], *, shell: bool, stdin: int | IO[Any] | None, stdout: int | IO[Any] | None, stderr: int | IO[Any] | None, cwd: str | bytes | PathLike | None = None, env: Mapping[str, str] | None = None, start_new_session: bool = False, ) -> Process: process = await trio.lowlevel.open_process( # type: ignore[misc] command, # type: ignore[arg-type] stdin=stdin, stdout=stdout, stderr=stderr, shell=shell, cwd=cwd, env=env, start_new_session=start_new_session, ) stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None return Process(process, stdin_stream, stdout_stream, stderr_stream) @classmethod def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: trio.lowlevel.spawn_system_task(_shutdown_process_pool, workers) @classmethod async def connect_tcp( cls, host: str, port: int, local_address: IPSockAddrType | None = None ) -> SocketStream: family = socket.AF_INET6 if ":" in host else socket.AF_INET trio_socket = trio.socket.socket(family) trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) if local_address: await trio_socket.bind(local_address) try: await trio_socket.connect((host, port)) except BaseException: trio_socket.close() raise return SocketStream(trio_socket) @classmethod async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: trio_socket = trio.socket.socket(socket.AF_UNIX) try: await trio_socket.connect(path) except BaseException: trio_socket.close() raise return UNIXSocketStream(trio_socket) @classmethod def create_tcp_listener(cls, sock: socket.socket) -> abc.SocketListener: return TCPSocketListener(sock) @classmethod def create_unix_listener(cls, sock: socket.socket) -> abc.SocketListener: return UNIXSocketListener(sock) @classmethod async def create_udp_socket( cls, family: socket.AddressFamily, local_address: IPSockAddrType | None, remote_address: IPSockAddrType | None, reuse_port: bool, ) -> UDPSocket | ConnectedUDPSocket: trio_socket = trio.socket.socket(family=family, type=socket.SOCK_DGRAM) if reuse_port: trio_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) if local_address: await trio_socket.bind(local_address) if remote_address: await trio_socket.connect(remote_address) return ConnectedUDPSocket(trio_socket) else: return UDPSocket(trio_socket) @classmethod @overload async def create_unix_datagram_socket( cls, raw_socket: socket.socket, remote_path: None ) -> abc.UNIXDatagramSocket: ... @classmethod @overload async def create_unix_datagram_socket( cls, raw_socket: socket.socket, remote_path: str | bytes ) -> abc.ConnectedUNIXDatagramSocket: ... @classmethod async def create_unix_datagram_socket( cls, raw_socket: socket.socket, remote_path: str | bytes | None ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: trio_socket = trio.socket.from_stdlib_socket(raw_socket) if remote_path: await trio_socket.connect(remote_path) return ConnectedUNIXDatagramSocket(trio_socket) else: return UNIXDatagramSocket(trio_socket) @classmethod async def getaddrinfo( cls, host: bytes | str | None, port: str | int | None, *, family: int | AddressFamily = 0, type: int | SocketKind = 0, proto: int = 0, flags: int = 0, ) -> list[ tuple[ AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int], ] ]: return await trio.socket.getaddrinfo(host, port, family, type, proto, flags) @classmethod async def getnameinfo( cls, sockaddr: IPSockAddrType, flags: int = 0 ) -> tuple[str, str]: return await trio.socket.getnameinfo(sockaddr, flags) @classmethod async def wait_socket_readable(cls, sock: socket.socket) -> None: try: await wait_readable(sock) except trio.ClosedResourceError as exc: raise ClosedResourceError().with_traceback(exc.__traceback__) from None except trio.BusyResourceError: raise BusyResourceError("reading from") from None @classmethod async def wait_socket_writable(cls, sock: socket.socket) -> None: try: await wait_writable(sock) except trio.ClosedResourceError as exc: raise ClosedResourceError().with_traceback(exc.__traceback__) from None except trio.BusyResourceError: raise BusyResourceError("writing to") from None @classmethod def current_default_thread_limiter(cls) -> CapacityLimiter: try: return _capacity_limiter_wrapper.get() except LookupError: limiter = CapacityLimiter( original=trio.to_thread.current_default_thread_limiter() ) _capacity_limiter_wrapper.set(limiter) return limiter @classmethod def open_signal_receiver( cls, *signals: Signals ) -> ContextManager[AsyncIterator[Signals]]: return _SignalReceiver(signals) @classmethod def get_current_task(cls) -> TaskInfo: task = current_task() return TrioTaskInfo(task) @classmethod def get_running_tasks(cls) -> Sequence[TaskInfo]: root_task = current_root_task() assert root_task task_infos = [TrioTaskInfo(root_task)] nurseries = root_task.child_nurseries while nurseries: new_nurseries: list[trio.Nursery] = [] for nursery in nurseries: for task in nursery.child_tasks: task_infos.append(TrioTaskInfo(task)) new_nurseries.extend(task.child_nurseries) nurseries = new_nurseries return task_infos @classmethod async def wait_all_tasks_blocked(cls) -> None: from trio.testing import wait_all_tasks_blocked await wait_all_tasks_blocked() @classmethod def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: return TestRunner(**options) backend_class = TrioBackend anyio-4.4.0/src/anyio/_core/000077500000000000000000000000001462472665400156535ustar00rootroot00000000000000anyio-4.4.0/src/anyio/_core/__init__.py000066400000000000000000000000001462472665400177520ustar00rootroot00000000000000anyio-4.4.0/src/anyio/_core/_eventloop.py000066400000000000000000000111271462472665400204010ustar00rootroot00000000000000from __future__ import annotations import math import sys import threading from collections.abc import Awaitable, Callable, Generator from contextlib import contextmanager from importlib import import_module from typing import TYPE_CHECKING, Any, TypeVar import sniffio if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: from typing_extensions import TypeVarTuple, Unpack if TYPE_CHECKING: from ..abc import AsyncBackend # This must be updated when new backends are introduced BACKENDS = "asyncio", "trio" T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") threadlocals = threading.local() loaded_backends: dict[str, type[AsyncBackend]] = {} def run( func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], *args: Unpack[PosArgsT], backend: str = "asyncio", backend_options: dict[str, Any] | None = None, ) -> T_Retval: """ Run the given coroutine function in an asynchronous event loop. The current thread must not be already running an event loop. :param func: a coroutine function :param args: positional arguments to ``func`` :param backend: name of the asynchronous event loop implementation – currently either ``asyncio`` or ``trio`` :param backend_options: keyword arguments to call the backend ``run()`` implementation with (documented :ref:`here `) :return: the return value of the coroutine function :raises RuntimeError: if an asynchronous event loop is already running in this thread :raises LookupError: if the named backend is not found """ try: asynclib_name = sniffio.current_async_library() except sniffio.AsyncLibraryNotFoundError: pass else: raise RuntimeError(f"Already running {asynclib_name} in this thread") try: async_backend = get_async_backend(backend) except ImportError as exc: raise LookupError(f"No such backend: {backend}") from exc token = None if sniffio.current_async_library_cvar.get(None) is None: # Since we're in control of the event loop, we can cache the name of the async # library token = sniffio.current_async_library_cvar.set(backend) try: backend_options = backend_options or {} return async_backend.run(func, args, {}, backend_options) finally: if token: sniffio.current_async_library_cvar.reset(token) async def sleep(delay: float) -> None: """ Pause the current task for the specified duration. :param delay: the duration, in seconds """ return await get_async_backend().sleep(delay) async def sleep_forever() -> None: """ Pause the current task until it's cancelled. This is a shortcut for ``sleep(math.inf)``. .. versionadded:: 3.1 """ await sleep(math.inf) async def sleep_until(deadline: float) -> None: """ Pause the current task until the given time. :param deadline: the absolute time to wake up at (according to the internal monotonic clock of the event loop) .. versionadded:: 3.1 """ now = current_time() await sleep(max(deadline - now, 0)) def current_time() -> float: """ Return the current value of the event loop's internal clock. :return: the clock value (seconds) """ return get_async_backend().current_time() def get_all_backends() -> tuple[str, ...]: """Return a tuple of the names of all built-in backends.""" return BACKENDS def get_cancelled_exc_class() -> type[BaseException]: """Return the current async library's cancellation exception class.""" return get_async_backend().cancelled_exception_class() # # Private API # @contextmanager def claim_worker_thread( backend_class: type[AsyncBackend], token: object ) -> Generator[Any, None, None]: threadlocals.current_async_backend = backend_class threadlocals.current_token = token try: yield finally: del threadlocals.current_async_backend del threadlocals.current_token def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]: if asynclib_name is None: asynclib_name = sniffio.current_async_library() # We use our own dict instead of sys.modules to get the already imported back-end # class because the appropriate modules in sys.modules could potentially be only # partially initialized try: return loaded_backends[asynclib_name] except KeyError: module = import_module(f"anyio._backends._{asynclib_name}") loaded_backends[asynclib_name] = module.backend_class return module.backend_class anyio-4.4.0/src/anyio/_core/_exceptions.py000066400000000000000000000040361462472665400205500ustar00rootroot00000000000000from __future__ import annotations class BrokenResourceError(Exception): """ Raised when trying to use a resource that has been rendered unusable due to external causes (e.g. a send stream whose peer has disconnected). """ class BrokenWorkerProcess(Exception): """ Raised by :func:`run_sync_in_process` if the worker process terminates abruptly or otherwise misbehaves. """ class BusyResourceError(Exception): """ Raised when two tasks are trying to read from or write to the same resource concurrently. """ def __init__(self, action: str): super().__init__(f"Another task is already {action} this resource") class ClosedResourceError(Exception): """Raised when trying to use a resource that has been closed.""" class DelimiterNotFound(Exception): """ Raised during :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the maximum number of bytes has been read without the delimiter being found. """ def __init__(self, max_bytes: int) -> None: super().__init__( f"The delimiter was not found among the first {max_bytes} bytes" ) class EndOfStream(Exception): """ Raised when trying to read from a stream that has been closed from the other end. """ class IncompleteRead(Exception): """ Raised during :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_exactly` or :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the connection is closed before the requested amount of bytes has been read. """ def __init__(self) -> None: super().__init__( "The stream was closed before the read operation could be completed" ) class TypedAttributeLookupError(LookupError): """ Raised by :meth:`~anyio.TypedAttributeProvider.extra` when the given typed attribute is not found and no default value has been given. """ class WouldBlock(Exception): """Raised by ``X_nowait`` functions if ``X()`` would block.""" anyio-4.4.0/src/anyio/_core/_fileio.py000066400000000000000000000460001462472665400176330ustar00rootroot00000000000000from __future__ import annotations import os import pathlib import sys from collections.abc import Callable, Iterable, Iterator, Sequence from dataclasses import dataclass from functools import partial from os import PathLike from typing import ( IO, TYPE_CHECKING, Any, AnyStr, AsyncIterator, Final, Generic, overload, ) from .. import to_thread from ..abc import AsyncResource if TYPE_CHECKING: from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer else: ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object class AsyncFile(AsyncResource, Generic[AnyStr]): """ An asynchronous file object. This class wraps a standard file object and provides async friendly versions of the following blocking methods (where available on the original file object): * read * read1 * readline * readlines * readinto * readinto1 * write * writelines * truncate * seek * tell * flush All other methods are directly passed through. This class supports the asynchronous context manager protocol which closes the underlying file at the end of the context block. This class also supports asynchronous iteration:: async with await open_file(...) as f: async for line in f: print(line) """ def __init__(self, fp: IO[AnyStr]) -> None: self._fp: Any = fp def __getattr__(self, name: str) -> object: return getattr(self._fp, name) @property def wrapped(self) -> IO[AnyStr]: """The wrapped file object.""" return self._fp async def __aiter__(self) -> AsyncIterator[AnyStr]: while True: line = await self.readline() if line: yield line else: break async def aclose(self) -> None: return await to_thread.run_sync(self._fp.close) async def read(self, size: int = -1) -> AnyStr: return await to_thread.run_sync(self._fp.read, size) async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes: return await to_thread.run_sync(self._fp.read1, size) async def readline(self) -> AnyStr: return await to_thread.run_sync(self._fp.readline) async def readlines(self) -> list[AnyStr]: return await to_thread.run_sync(self._fp.readlines) async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> bytes: return await to_thread.run_sync(self._fp.readinto, b) async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> bytes: return await to_thread.run_sync(self._fp.readinto1, b) @overload async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int: ... @overload async def write(self: AsyncFile[str], b: str) -> int: ... async def write(self, b: ReadableBuffer | str) -> int: return await to_thread.run_sync(self._fp.write, b) @overload async def writelines( self: AsyncFile[bytes], lines: Iterable[ReadableBuffer] ) -> None: ... @overload async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None: ... async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None: return await to_thread.run_sync(self._fp.writelines, lines) async def truncate(self, size: int | None = None) -> int: return await to_thread.run_sync(self._fp.truncate, size) async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: return await to_thread.run_sync(self._fp.seek, offset, whence) async def tell(self) -> int: return await to_thread.run_sync(self._fp.tell) async def flush(self) -> None: return await to_thread.run_sync(self._fp.flush) @overload async def open_file( file: str | PathLike[str] | int, mode: OpenBinaryMode, buffering: int = ..., encoding: str | None = ..., errors: str | None = ..., newline: str | None = ..., closefd: bool = ..., opener: Callable[[str, int], int] | None = ..., ) -> AsyncFile[bytes]: ... @overload async def open_file( file: str | PathLike[str] | int, mode: OpenTextMode = ..., buffering: int = ..., encoding: str | None = ..., errors: str | None = ..., newline: str | None = ..., closefd: bool = ..., opener: Callable[[str, int], int] | None = ..., ) -> AsyncFile[str]: ... async def open_file( file: str | PathLike[str] | int, mode: str = "r", buffering: int = -1, encoding: str | None = None, errors: str | None = None, newline: str | None = None, closefd: bool = True, opener: Callable[[str, int], int] | None = None, ) -> AsyncFile[Any]: """ Open a file asynchronously. The arguments are exactly the same as for the builtin :func:`open`. :return: an asynchronous file object """ fp = await to_thread.run_sync( open, file, mode, buffering, encoding, errors, newline, closefd, opener ) return AsyncFile(fp) def wrap_file(file: IO[AnyStr]) -> AsyncFile[AnyStr]: """ Wrap an existing file as an asynchronous file. :param file: an existing file-like object :return: an asynchronous file object """ return AsyncFile(file) @dataclass(eq=False) class _PathIterator(AsyncIterator["Path"]): iterator: Iterator[PathLike[str]] async def __anext__(self) -> Path: nextval = await to_thread.run_sync( next, self.iterator, None, abandon_on_cancel=True ) if nextval is None: raise StopAsyncIteration from None return Path(nextval) class Path: """ An asynchronous version of :class:`pathlib.Path`. This class cannot be substituted for :class:`pathlib.Path` or :class:`pathlib.PurePath`, but it is compatible with the :class:`os.PathLike` interface. It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for the deprecated :meth:`~pathlib.Path.link_to` method. Any methods that do disk I/O need to be awaited on. These methods are: * :meth:`~pathlib.Path.absolute` * :meth:`~pathlib.Path.chmod` * :meth:`~pathlib.Path.cwd` * :meth:`~pathlib.Path.exists` * :meth:`~pathlib.Path.expanduser` * :meth:`~pathlib.Path.group` * :meth:`~pathlib.Path.hardlink_to` * :meth:`~pathlib.Path.home` * :meth:`~pathlib.Path.is_block_device` * :meth:`~pathlib.Path.is_char_device` * :meth:`~pathlib.Path.is_dir` * :meth:`~pathlib.Path.is_fifo` * :meth:`~pathlib.Path.is_file` * :meth:`~pathlib.Path.is_mount` * :meth:`~pathlib.Path.lchmod` * :meth:`~pathlib.Path.lstat` * :meth:`~pathlib.Path.mkdir` * :meth:`~pathlib.Path.open` * :meth:`~pathlib.Path.owner` * :meth:`~pathlib.Path.read_bytes` * :meth:`~pathlib.Path.read_text` * :meth:`~pathlib.Path.readlink` * :meth:`~pathlib.Path.rename` * :meth:`~pathlib.Path.replace` * :meth:`~pathlib.Path.rmdir` * :meth:`~pathlib.Path.samefile` * :meth:`~pathlib.Path.stat` * :meth:`~pathlib.Path.touch` * :meth:`~pathlib.Path.unlink` * :meth:`~pathlib.Path.write_bytes` * :meth:`~pathlib.Path.write_text` Additionally, the following methods return an async iterator yielding :class:`~.Path` objects: * :meth:`~pathlib.Path.glob` * :meth:`~pathlib.Path.iterdir` * :meth:`~pathlib.Path.rglob` """ __slots__ = "_path", "__weakref__" __weakref__: Any def __init__(self, *args: str | PathLike[str]) -> None: self._path: Final[pathlib.Path] = pathlib.Path(*args) def __fspath__(self) -> str: return self._path.__fspath__() def __str__(self) -> str: return self._path.__str__() def __repr__(self) -> str: return f"{self.__class__.__name__}({self.as_posix()!r})" def __bytes__(self) -> bytes: return self._path.__bytes__() def __hash__(self) -> int: return self._path.__hash__() def __eq__(self, other: object) -> bool: target = other._path if isinstance(other, Path) else other return self._path.__eq__(target) def __lt__(self, other: pathlib.PurePath | Path) -> bool: target = other._path if isinstance(other, Path) else other return self._path.__lt__(target) def __le__(self, other: pathlib.PurePath | Path) -> bool: target = other._path if isinstance(other, Path) else other return self._path.__le__(target) def __gt__(self, other: pathlib.PurePath | Path) -> bool: target = other._path if isinstance(other, Path) else other return self._path.__gt__(target) def __ge__(self, other: pathlib.PurePath | Path) -> bool: target = other._path if isinstance(other, Path) else other return self._path.__ge__(target) def __truediv__(self, other: str | PathLike[str]) -> Path: return Path(self._path / other) def __rtruediv__(self, other: str | PathLike[str]) -> Path: return Path(other) / self @property def parts(self) -> tuple[str, ...]: return self._path.parts @property def drive(self) -> str: return self._path.drive @property def root(self) -> str: return self._path.root @property def anchor(self) -> str: return self._path.anchor @property def parents(self) -> Sequence[Path]: return tuple(Path(p) for p in self._path.parents) @property def parent(self) -> Path: return Path(self._path.parent) @property def name(self) -> str: return self._path.name @property def suffix(self) -> str: return self._path.suffix @property def suffixes(self) -> list[str]: return self._path.suffixes @property def stem(self) -> str: return self._path.stem async def absolute(self) -> Path: path = await to_thread.run_sync(self._path.absolute) return Path(path) def as_posix(self) -> str: return self._path.as_posix() def as_uri(self) -> str: return self._path.as_uri() def match(self, path_pattern: str) -> bool: return self._path.match(path_pattern) def is_relative_to(self, other: str | PathLike[str]) -> bool: try: self.relative_to(other) return True except ValueError: return False async def is_junction(self) -> bool: return await to_thread.run_sync(self._path.is_junction) async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: func = partial(os.chmod, follow_symlinks=follow_symlinks) return await to_thread.run_sync(func, self._path, mode) @classmethod async def cwd(cls) -> Path: path = await to_thread.run_sync(pathlib.Path.cwd) return cls(path) async def exists(self) -> bool: return await to_thread.run_sync(self._path.exists, abandon_on_cancel=True) async def expanduser(self) -> Path: return Path( await to_thread.run_sync(self._path.expanduser, abandon_on_cancel=True) ) def glob(self, pattern: str) -> AsyncIterator[Path]: gen = self._path.glob(pattern) return _PathIterator(gen) async def group(self) -> str: return await to_thread.run_sync(self._path.group, abandon_on_cancel=True) async def hardlink_to( self, target: str | bytes | PathLike[str] | PathLike[bytes] ) -> None: if isinstance(target, Path): target = target._path await to_thread.run_sync(os.link, target, self) @classmethod async def home(cls) -> Path: home_path = await to_thread.run_sync(pathlib.Path.home) return cls(home_path) def is_absolute(self) -> bool: return self._path.is_absolute() async def is_block_device(self) -> bool: return await to_thread.run_sync( self._path.is_block_device, abandon_on_cancel=True ) async def is_char_device(self) -> bool: return await to_thread.run_sync( self._path.is_char_device, abandon_on_cancel=True ) async def is_dir(self) -> bool: return await to_thread.run_sync(self._path.is_dir, abandon_on_cancel=True) async def is_fifo(self) -> bool: return await to_thread.run_sync(self._path.is_fifo, abandon_on_cancel=True) async def is_file(self) -> bool: return await to_thread.run_sync(self._path.is_file, abandon_on_cancel=True) async def is_mount(self) -> bool: return await to_thread.run_sync( os.path.ismount, self._path, abandon_on_cancel=True ) def is_reserved(self) -> bool: return self._path.is_reserved() async def is_socket(self) -> bool: return await to_thread.run_sync(self._path.is_socket, abandon_on_cancel=True) async def is_symlink(self) -> bool: return await to_thread.run_sync(self._path.is_symlink, abandon_on_cancel=True) def iterdir(self) -> AsyncIterator[Path]: gen = self._path.iterdir() return _PathIterator(gen) def joinpath(self, *args: str | PathLike[str]) -> Path: return Path(self._path.joinpath(*args)) async def lchmod(self, mode: int) -> None: await to_thread.run_sync(self._path.lchmod, mode) async def lstat(self) -> os.stat_result: return await to_thread.run_sync(self._path.lstat, abandon_on_cancel=True) async def mkdir( self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False ) -> None: await to_thread.run_sync(self._path.mkdir, mode, parents, exist_ok) @overload async def open( self, mode: OpenBinaryMode, buffering: int = ..., encoding: str | None = ..., errors: str | None = ..., newline: str | None = ..., ) -> AsyncFile[bytes]: ... @overload async def open( self, mode: OpenTextMode = ..., buffering: int = ..., encoding: str | None = ..., errors: str | None = ..., newline: str | None = ..., ) -> AsyncFile[str]: ... async def open( self, mode: str = "r", buffering: int = -1, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> AsyncFile[Any]: fp = await to_thread.run_sync( self._path.open, mode, buffering, encoding, errors, newline ) return AsyncFile(fp) async def owner(self) -> str: return await to_thread.run_sync(self._path.owner, abandon_on_cancel=True) async def read_bytes(self) -> bytes: return await to_thread.run_sync(self._path.read_bytes) async def read_text( self, encoding: str | None = None, errors: str | None = None ) -> str: return await to_thread.run_sync(self._path.read_text, encoding, errors) if sys.version_info >= (3, 12): def relative_to( self, *other: str | PathLike[str], walk_up: bool = False ) -> Path: return Path(self._path.relative_to(*other, walk_up=walk_up)) else: def relative_to(self, *other: str | PathLike[str]) -> Path: return Path(self._path.relative_to(*other)) async def readlink(self) -> Path: target = await to_thread.run_sync(os.readlink, self._path) return Path(target) async def rename(self, target: str | pathlib.PurePath | Path) -> Path: if isinstance(target, Path): target = target._path await to_thread.run_sync(self._path.rename, target) return Path(target) async def replace(self, target: str | pathlib.PurePath | Path) -> Path: if isinstance(target, Path): target = target._path await to_thread.run_sync(self._path.replace, target) return Path(target) async def resolve(self, strict: bool = False) -> Path: func = partial(self._path.resolve, strict=strict) return Path(await to_thread.run_sync(func, abandon_on_cancel=True)) def rglob(self, pattern: str) -> AsyncIterator[Path]: gen = self._path.rglob(pattern) return _PathIterator(gen) async def rmdir(self) -> None: await to_thread.run_sync(self._path.rmdir) async def samefile(self, other_path: str | PathLike[str]) -> bool: if isinstance(other_path, Path): other_path = other_path._path return await to_thread.run_sync( self._path.samefile, other_path, abandon_on_cancel=True ) async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: func = partial(os.stat, follow_symlinks=follow_symlinks) return await to_thread.run_sync(func, self._path, abandon_on_cancel=True) async def symlink_to( self, target: str | bytes | PathLike[str] | PathLike[bytes], target_is_directory: bool = False, ) -> None: if isinstance(target, Path): target = target._path await to_thread.run_sync(self._path.symlink_to, target, target_is_directory) async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: await to_thread.run_sync(self._path.touch, mode, exist_ok) async def unlink(self, missing_ok: bool = False) -> None: try: await to_thread.run_sync(self._path.unlink) except FileNotFoundError: if not missing_ok: raise if sys.version_info >= (3, 12): async def walk( self, top_down: bool = True, on_error: Callable[[OSError], object] | None = None, follow_symlinks: bool = False, ) -> AsyncIterator[tuple[Path, list[str], list[str]]]: def get_next_value() -> tuple[pathlib.Path, list[str], list[str]] | None: try: return next(gen) except StopIteration: return None gen = self._path.walk(top_down, on_error, follow_symlinks) while True: value = await to_thread.run_sync(get_next_value) if value is None: return root, dirs, paths = value yield Path(root), dirs, paths def with_name(self, name: str) -> Path: return Path(self._path.with_name(name)) def with_stem(self, stem: str) -> Path: return Path(self._path.with_name(stem + self._path.suffix)) def with_suffix(self, suffix: str) -> Path: return Path(self._path.with_suffix(suffix)) def with_segments(self, *pathsegments: str | PathLike[str]) -> Path: return Path(*pathsegments) async def write_bytes(self, data: bytes) -> int: return await to_thread.run_sync(self._path.write_bytes, data) async def write_text( self, data: str, encoding: str | None = None, errors: str | None = None, newline: str | None = None, ) -> int: # Path.write_text() does not support the "newline" parameter before Python 3.10 def sync_write_text() -> int: with self._path.open( "w", encoding=encoding, errors=errors, newline=newline ) as fp: return fp.write(data) return await to_thread.run_sync(sync_write_text) PathLike.register(Path) anyio-4.4.0/src/anyio/_core/_resources.py000066400000000000000000000006631462472665400204030ustar00rootroot00000000000000from __future__ import annotations from ..abc import AsyncResource from ._tasks import CancelScope async def aclose_forcefully(resource: AsyncResource) -> None: """ Close an asynchronous resource in a cancelled scope. Doing this closes the resource without waiting on anything. :param resource: the resource to close """ with CancelScope() as scope: scope.cancel() await resource.aclose() anyio-4.4.0/src/anyio/_core/_signals.py000066400000000000000000000015561462472665400200330ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncIterator from signal import Signals from typing import ContextManager from ._eventloop import get_async_backend def open_signal_receiver(*signals: Signals) -> ContextManager[AsyncIterator[Signals]]: """ Start receiving operating system signals. :param signals: signals to receive (e.g. ``signal.SIGINT``) :return: an asynchronous context manager for an asynchronous iterator which yields signal numbers .. warning:: Windows does not support signals natively so it is best to avoid relying on this in cross-platform applications. .. warning:: On asyncio, this permanently replaces any previous signal handler for the given signals, as set via :meth:`~asyncio.loop.add_signal_handler`. """ return get_async_backend().open_signal_receiver(*signals) anyio-4.4.0/src/anyio/_core/_sockets.py000066400000000000000000000567341462472665400200560ustar00rootroot00000000000000from __future__ import annotations import errno import os import socket import ssl import stat import sys from collections.abc import Awaitable from ipaddress import IPv6Address, ip_address from os import PathLike, chmod from socket import AddressFamily, SocketKind from typing import Any, Literal, cast, overload from .. import to_thread from ..abc import ( ConnectedUDPSocket, ConnectedUNIXDatagramSocket, IPAddressType, IPSockAddrType, SocketListener, SocketStream, UDPSocket, UNIXDatagramSocket, UNIXSocketStream, ) from ..streams.stapled import MultiListener from ..streams.tls import TLSStream from ._eventloop import get_async_backend from ._resources import aclose_forcefully from ._synchronization import Event from ._tasks import create_task_group, move_on_after if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) # https://bugs.python.org/issue29515 AnyIPAddressFamily = Literal[ AddressFamily.AF_UNSPEC, AddressFamily.AF_INET, AddressFamily.AF_INET6 ] IPAddressFamily = Literal[AddressFamily.AF_INET, AddressFamily.AF_INET6] # tls_hostname given @overload async def connect_tcp( remote_host: IPAddressType, remote_port: int, *, local_host: IPAddressType | None = ..., ssl_context: ssl.SSLContext | None = ..., tls_standard_compatible: bool = ..., tls_hostname: str, happy_eyeballs_delay: float = ..., ) -> TLSStream: ... # ssl_context given @overload async def connect_tcp( remote_host: IPAddressType, remote_port: int, *, local_host: IPAddressType | None = ..., ssl_context: ssl.SSLContext, tls_standard_compatible: bool = ..., tls_hostname: str | None = ..., happy_eyeballs_delay: float = ..., ) -> TLSStream: ... # tls=True @overload async def connect_tcp( remote_host: IPAddressType, remote_port: int, *, local_host: IPAddressType | None = ..., tls: Literal[True], ssl_context: ssl.SSLContext | None = ..., tls_standard_compatible: bool = ..., tls_hostname: str | None = ..., happy_eyeballs_delay: float = ..., ) -> TLSStream: ... # tls=False @overload async def connect_tcp( remote_host: IPAddressType, remote_port: int, *, local_host: IPAddressType | None = ..., tls: Literal[False], ssl_context: ssl.SSLContext | None = ..., tls_standard_compatible: bool = ..., tls_hostname: str | None = ..., happy_eyeballs_delay: float = ..., ) -> SocketStream: ... # No TLS arguments @overload async def connect_tcp( remote_host: IPAddressType, remote_port: int, *, local_host: IPAddressType | None = ..., happy_eyeballs_delay: float = ..., ) -> SocketStream: ... async def connect_tcp( remote_host: IPAddressType, remote_port: int, *, local_host: IPAddressType | None = None, tls: bool = False, ssl_context: ssl.SSLContext | None = None, tls_standard_compatible: bool = True, tls_hostname: str | None = None, happy_eyeballs_delay: float = 0.25, ) -> SocketStream | TLSStream: """ Connect to a host using the TCP protocol. This function implements the stateless version of the Happy Eyeballs algorithm (RFC 6555). If ``remote_host`` is a host name that resolves to multiple IP addresses, each one is tried until one connection attempt succeeds. If the first attempt does not connected within 250 milliseconds, a second attempt is started using the next address in the list, and so on. On IPv6 enabled systems, an IPv6 address (if available) is tried first. When the connection has been established, a TLS handshake will be done if either ``ssl_context`` or ``tls_hostname`` is not ``None``, or if ``tls`` is ``True``. :param remote_host: the IP address or host name to connect to :param remote_port: port on the target host to connect to :param local_host: the interface address or name to bind the socket to before connecting :param tls: ``True`` to do a TLS handshake with the connected stream and return a :class:`~anyio.streams.tls.TLSStream` instead :param ssl_context: the SSL context object to use (if omitted, a default context is created) :param tls_standard_compatible: If ``True``, performs the TLS shutdown handshake before closing the stream and requires that the server does this as well. Otherwise, :exc:`~ssl.SSLEOFError` may be raised during reads from the stream. Some protocols, such as HTTP, require this option to be ``False``. See :meth:`~ssl.SSLContext.wrap_socket` for details. :param tls_hostname: host name to check the server certificate against (defaults to the value of ``remote_host``) :param happy_eyeballs_delay: delay (in seconds) before starting the next connection attempt :return: a socket stream object if no TLS handshake was done, otherwise a TLS stream :raises OSError: if the connection attempt fails """ # Placed here due to https://github.com/python/mypy/issues/7057 connected_stream: SocketStream | None = None async def try_connect(remote_host: str, event: Event) -> None: nonlocal connected_stream try: stream = await asynclib.connect_tcp(remote_host, remote_port, local_address) except OSError as exc: oserrors.append(exc) return else: if connected_stream is None: connected_stream = stream tg.cancel_scope.cancel() else: await stream.aclose() finally: event.set() asynclib = get_async_backend() local_address: IPSockAddrType | None = None family = socket.AF_UNSPEC if local_host: gai_res = await getaddrinfo(str(local_host), None) family, *_, local_address = gai_res[0] target_host = str(remote_host) try: addr_obj = ip_address(remote_host) except ValueError: # getaddrinfo() will raise an exception if name resolution fails gai_res = await getaddrinfo( target_host, remote_port, family=family, type=socket.SOCK_STREAM ) # Organize the list so that the first address is an IPv6 address (if available) # and the second one is an IPv4 addresses. The rest can be in whatever order. v6_found = v4_found = False target_addrs: list[tuple[socket.AddressFamily, str]] = [] for af, *rest, sa in gai_res: if af == socket.AF_INET6 and not v6_found: v6_found = True target_addrs.insert(0, (af, sa[0])) elif af == socket.AF_INET and not v4_found and v6_found: v4_found = True target_addrs.insert(1, (af, sa[0])) else: target_addrs.append((af, sa[0])) else: if isinstance(addr_obj, IPv6Address): target_addrs = [(socket.AF_INET6, addr_obj.compressed)] else: target_addrs = [(socket.AF_INET, addr_obj.compressed)] oserrors: list[OSError] = [] async with create_task_group() as tg: for i, (af, addr) in enumerate(target_addrs): event = Event() tg.start_soon(try_connect, addr, event) with move_on_after(happy_eyeballs_delay): await event.wait() if connected_stream is None: cause = ( oserrors[0] if len(oserrors) == 1 else ExceptionGroup("multiple connection attempts failed", oserrors) ) raise OSError("All connection attempts failed") from cause if tls or tls_hostname or ssl_context: try: return await TLSStream.wrap( connected_stream, server_side=False, hostname=tls_hostname or str(remote_host), ssl_context=ssl_context, standard_compatible=tls_standard_compatible, ) except BaseException: await aclose_forcefully(connected_stream) raise return connected_stream async def connect_unix(path: str | bytes | PathLike[Any]) -> UNIXSocketStream: """ Connect to the given UNIX socket. Not available on Windows. :param path: path to the socket :return: a socket stream object """ path = os.fspath(path) return await get_async_backend().connect_unix(path) async def create_tcp_listener( *, local_host: IPAddressType | None = None, local_port: int = 0, family: AnyIPAddressFamily = socket.AddressFamily.AF_UNSPEC, backlog: int = 65536, reuse_port: bool = False, ) -> MultiListener[SocketStream]: """ Create a TCP socket listener. :param local_port: port number to listen on :param local_host: IP address of the interface to listen on. If omitted, listen on all IPv4 and IPv6 interfaces. To listen on all interfaces on a specific address family, use ``0.0.0.0`` for IPv4 or ``::`` for IPv6. :param family: address family (used if ``local_host`` was omitted) :param backlog: maximum number of queued incoming connections (up to a maximum of 2**16, or 65536) :param reuse_port: ``True`` to allow multiple sockets to bind to the same address/port (not supported on Windows) :return: a list of listener objects """ asynclib = get_async_backend() backlog = min(backlog, 65536) local_host = str(local_host) if local_host is not None else None gai_res = await getaddrinfo( local_host, local_port, family=family, type=socket.SocketKind.SOCK_STREAM if sys.platform == "win32" else 0, flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, ) listeners: list[SocketListener] = [] try: # The set() is here to work around a glibc bug: # https://sourceware.org/bugzilla/show_bug.cgi?id=14969 sockaddr: tuple[str, int] | tuple[str, int, int, int] for fam, kind, *_, sockaddr in sorted(set(gai_res)): # Workaround for an uvloop bug where we don't get the correct scope ID for # IPv6 link-local addresses when passing type=socket.SOCK_STREAM to # getaddrinfo(): https://github.com/MagicStack/uvloop/issues/539 if sys.platform != "win32" and kind is not SocketKind.SOCK_STREAM: continue raw_socket = socket.socket(fam) raw_socket.setblocking(False) # For Windows, enable exclusive address use. For others, enable address # reuse. if sys.platform == "win32": raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) else: raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) if reuse_port: raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) # If only IPv6 was requested, disable dual stack operation if fam == socket.AF_INET6: raw_socket.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) # Workaround for #554 if "%" in sockaddr[0]: addr, scope_id = sockaddr[0].split("%", 1) sockaddr = (addr, sockaddr[1], 0, int(scope_id)) raw_socket.bind(sockaddr) raw_socket.listen(backlog) listener = asynclib.create_tcp_listener(raw_socket) listeners.append(listener) except BaseException: for listener in listeners: await listener.aclose() raise return MultiListener(listeners) async def create_unix_listener( path: str | bytes | PathLike[Any], *, mode: int | None = None, backlog: int = 65536, ) -> SocketListener: """ Create a UNIX socket listener. Not available on Windows. :param path: path of the socket :param mode: permissions to set on the socket :param backlog: maximum number of queued incoming connections (up to a maximum of 2**16, or 65536) :return: a listener object .. versionchanged:: 3.0 If a socket already exists on the file system in the given path, it will be removed first. """ backlog = min(backlog, 65536) raw_socket = await setup_unix_local_socket(path, mode, socket.SOCK_STREAM) try: raw_socket.listen(backlog) return get_async_backend().create_unix_listener(raw_socket) except BaseException: raw_socket.close() raise async def create_udp_socket( family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, *, local_host: IPAddressType | None = None, local_port: int = 0, reuse_port: bool = False, ) -> UDPSocket: """ Create a UDP socket. If ``port`` has been given, the socket will be bound to this port on the local machine, making this socket suitable for providing UDP based services. :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically determined from ``local_host`` if omitted :param local_host: IP address or host name of the local interface to bind to :param local_port: local port to bind to :param reuse_port: ``True`` to allow multiple sockets to bind to the same address/port (not supported on Windows) :return: a UDP socket """ if family is AddressFamily.AF_UNSPEC and not local_host: raise ValueError('Either "family" or "local_host" must be given') if local_host: gai_res = await getaddrinfo( str(local_host), local_port, family=family, type=socket.SOCK_DGRAM, flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, ) family = cast(AnyIPAddressFamily, gai_res[0][0]) local_address = gai_res[0][-1] elif family is AddressFamily.AF_INET6: local_address = ("::", 0) else: local_address = ("0.0.0.0", 0) sock = await get_async_backend().create_udp_socket( family, local_address, None, reuse_port ) return cast(UDPSocket, sock) async def create_connected_udp_socket( remote_host: IPAddressType, remote_port: int, *, family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, local_host: IPAddressType | None = None, local_port: int = 0, reuse_port: bool = False, ) -> ConnectedUDPSocket: """ Create a connected UDP socket. Connected UDP sockets can only communicate with the specified remote host/port, an any packets sent from other sources are dropped. :param remote_host: remote host to set as the default target :param remote_port: port on the remote host to set as the default target :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically determined from ``local_host`` or ``remote_host`` if omitted :param local_host: IP address or host name of the local interface to bind to :param local_port: local port to bind to :param reuse_port: ``True`` to allow multiple sockets to bind to the same address/port (not supported on Windows) :return: a connected UDP socket """ local_address = None if local_host: gai_res = await getaddrinfo( str(local_host), local_port, family=family, type=socket.SOCK_DGRAM, flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, ) family = cast(AnyIPAddressFamily, gai_res[0][0]) local_address = gai_res[0][-1] gai_res = await getaddrinfo( str(remote_host), remote_port, family=family, type=socket.SOCK_DGRAM ) family = cast(AnyIPAddressFamily, gai_res[0][0]) remote_address = gai_res[0][-1] sock = await get_async_backend().create_udp_socket( family, local_address, remote_address, reuse_port ) return cast(ConnectedUDPSocket, sock) async def create_unix_datagram_socket( *, local_path: None | str | bytes | PathLike[Any] = None, local_mode: int | None = None, ) -> UNIXDatagramSocket: """ Create a UNIX datagram socket. Not available on Windows. If ``local_path`` has been given, the socket will be bound to this path, making this socket suitable for receiving datagrams from other processes. Other processes can send datagrams to this socket only if ``local_path`` is set. If a socket already exists on the file system in the ``local_path``, it will be removed first. :param local_path: the path on which to bind to :param local_mode: permissions to set on the local socket :return: a UNIX datagram socket """ raw_socket = await setup_unix_local_socket( local_path, local_mode, socket.SOCK_DGRAM ) return await get_async_backend().create_unix_datagram_socket(raw_socket, None) async def create_connected_unix_datagram_socket( remote_path: str | bytes | PathLike[Any], *, local_path: None | str | bytes | PathLike[Any] = None, local_mode: int | None = None, ) -> ConnectedUNIXDatagramSocket: """ Create a connected UNIX datagram socket. Connected datagram sockets can only communicate with the specified remote path. If ``local_path`` has been given, the socket will be bound to this path, making this socket suitable for receiving datagrams from other processes. Other processes can send datagrams to this socket only if ``local_path`` is set. If a socket already exists on the file system in the ``local_path``, it will be removed first. :param remote_path: the path to set as the default target :param local_path: the path on which to bind to :param local_mode: permissions to set on the local socket :return: a connected UNIX datagram socket """ remote_path = os.fspath(remote_path) raw_socket = await setup_unix_local_socket( local_path, local_mode, socket.SOCK_DGRAM ) return await get_async_backend().create_unix_datagram_socket( raw_socket, remote_path ) async def getaddrinfo( host: bytes | str | None, port: str | int | None, *, family: int | AddressFamily = 0, type: int | SocketKind = 0, proto: int = 0, flags: int = 0, ) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int]]]: """ Look up a numeric IP address given a host name. Internationalized domain names are translated according to the (non-transitional) IDNA 2008 standard. .. note:: 4-tuple IPv6 socket addresses are automatically converted to 2-tuples of (host, port), unlike what :func:`socket.getaddrinfo` does. :param host: host name :param port: port number :param family: socket family (`'AF_INET``, ...) :param type: socket type (``SOCK_STREAM``, ...) :param proto: protocol number :param flags: flags to pass to upstream ``getaddrinfo()`` :return: list of tuples containing (family, type, proto, canonname, sockaddr) .. seealso:: :func:`socket.getaddrinfo` """ # Handle unicode hostnames if isinstance(host, str): try: encoded_host: bytes | None = host.encode("ascii") except UnicodeEncodeError: import idna encoded_host = idna.encode(host, uts46=True) else: encoded_host = host gai_res = await get_async_backend().getaddrinfo( encoded_host, port, family=family, type=type, proto=proto, flags=flags ) return [ (family, type, proto, canonname, convert_ipv6_sockaddr(sockaddr)) for family, type, proto, canonname, sockaddr in gai_res ] def getnameinfo(sockaddr: IPSockAddrType, flags: int = 0) -> Awaitable[tuple[str, str]]: """ Look up the host name of an IP address. :param sockaddr: socket address (e.g. (ipaddress, port) for IPv4) :param flags: flags to pass to upstream ``getnameinfo()`` :return: a tuple of (host name, service name) .. seealso:: :func:`socket.getnameinfo` """ return get_async_backend().getnameinfo(sockaddr, flags) def wait_socket_readable(sock: socket.socket) -> Awaitable[None]: """ Wait until the given socket has data to be read. This does **NOT** work on Windows when using the asyncio backend with a proactor event loop (default on py3.8+). .. warning:: Only use this on raw sockets that have not been wrapped by any higher level constructs like socket streams! :param sock: a socket object :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the socket to become readable :raises ~anyio.BusyResourceError: if another task is already waiting for the socket to become readable """ return get_async_backend().wait_socket_readable(sock) def wait_socket_writable(sock: socket.socket) -> Awaitable[None]: """ Wait until the given socket can be written to. This does **NOT** work on Windows when using the asyncio backend with a proactor event loop (default on py3.8+). .. warning:: Only use this on raw sockets that have not been wrapped by any higher level constructs like socket streams! :param sock: a socket object :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the socket to become writable :raises ~anyio.BusyResourceError: if another task is already waiting for the socket to become writable """ return get_async_backend().wait_socket_writable(sock) # # Private API # def convert_ipv6_sockaddr( sockaddr: tuple[str, int, int, int] | tuple[str, int], ) -> tuple[str, int]: """ Convert a 4-tuple IPv6 socket address to a 2-tuple (address, port) format. If the scope ID is nonzero, it is added to the address, separated with ``%``. Otherwise the flow id and scope id are simply cut off from the tuple. Any other kinds of socket addresses are returned as-is. :param sockaddr: the result of :meth:`~socket.socket.getsockname` :return: the converted socket address """ # This is more complicated than it should be because of MyPy if isinstance(sockaddr, tuple) and len(sockaddr) == 4: host, port, flowinfo, scope_id = sockaddr if scope_id: # PyPy (as of v7.3.11) leaves the interface name in the result, so # we discard it and only get the scope ID from the end # (https://foss.heptapod.net/pypy/pypy/-/issues/3938) host = host.split("%")[0] # Add scope_id to the address return f"{host}%{scope_id}", port else: return host, port else: return sockaddr async def setup_unix_local_socket( path: None | str | bytes | PathLike[Any], mode: int | None, socktype: int, ) -> socket.socket: """ Create a UNIX local socket object, deleting the socket at the given path if it exists. Not available on Windows. :param path: path of the socket :param mode: permissions to set on the socket :param socktype: socket.SOCK_STREAM or socket.SOCK_DGRAM """ path_str: str | bytes | None if path is not None: path_str = os.fspath(path) # Copied from pathlib... try: stat_result = os.stat(path) except OSError as e: if e.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EBADF, errno.ELOOP): raise else: if stat.S_ISSOCK(stat_result.st_mode): os.unlink(path) else: path_str = None raw_socket = socket.socket(socket.AF_UNIX, socktype) raw_socket.setblocking(False) if path_str is not None: try: await to_thread.run_sync(raw_socket.bind, path_str, abandon_on_cancel=True) if mode is not None: await to_thread.run_sync(chmod, path_str, mode, abandon_on_cancel=True) except BaseException: raw_socket.close() raise return raw_socket anyio-4.4.0/src/anyio/_core/_streams.py000066400000000000000000000034231462472665400200440ustar00rootroot00000000000000from __future__ import annotations import math from typing import Tuple, TypeVar from warnings import warn from ..streams.memory import ( MemoryObjectReceiveStream, MemoryObjectSendStream, MemoryObjectStreamState, ) T_Item = TypeVar("T_Item") class create_memory_object_stream( Tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]], ): """ Create a memory object stream. The stream's item type can be annotated like :func:`create_memory_object_stream[T_Item]`. :param max_buffer_size: number of items held in the buffer until ``send()`` starts blocking :param item_type: old way of marking the streams with the right generic type for static typing (does nothing on AnyIO 4) .. deprecated:: 4.0 Use ``create_memory_object_stream[YourItemType](...)`` instead. :return: a tuple of (send stream, receive stream) """ def __new__( # type: ignore[misc] cls, max_buffer_size: float = 0, item_type: object = None ) -> tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]]: if max_buffer_size != math.inf and not isinstance(max_buffer_size, int): raise ValueError("max_buffer_size must be either an integer or math.inf") if max_buffer_size < 0: raise ValueError("max_buffer_size cannot be negative") if item_type is not None: warn( "The item_type argument has been deprecated in AnyIO 4.0. " "Use create_memory_object_stream[YourItemType](...) instead.", DeprecationWarning, stacklevel=2, ) state = MemoryObjectStreamState[T_Item](max_buffer_size) return (MemoryObjectSendStream(state), MemoryObjectReceiveStream(state)) anyio-4.4.0/src/anyio/_core/_subprocesses.py000066400000000000000000000122301462472665400211020ustar00rootroot00000000000000from __future__ import annotations from collections.abc import AsyncIterable, Mapping, Sequence from io import BytesIO from os import PathLike from subprocess import DEVNULL, PIPE, CalledProcessError, CompletedProcess from typing import IO, Any, cast from ..abc import Process from ._eventloop import get_async_backend from ._tasks import create_task_group async def run_process( command: str | bytes | Sequence[str | bytes], *, input: bytes | None = None, stdout: int | IO[Any] | None = PIPE, stderr: int | IO[Any] | None = PIPE, check: bool = True, cwd: str | bytes | PathLike[str] | None = None, env: Mapping[str, str] | None = None, start_new_session: bool = False, ) -> CompletedProcess[bytes]: """ Run an external command in a subprocess and wait until it completes. .. seealso:: :func:`subprocess.run` :param command: either a string to pass to the shell, or an iterable of strings containing the executable name or path and its arguments :param input: bytes passed to the standard input of the subprocess :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a file-like object, or `None` :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, :data:`subprocess.STDOUT`, a file-like object, or `None` :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the process terminates with a return code other than 0 :param cwd: If not ``None``, change the working directory to this before running the command :param env: if not ``None``, this mapping replaces the inherited environment variables from the parent process :param start_new_session: if ``true`` the setsid() system call will be made in the child process prior to the execution of the subprocess. (POSIX only) :return: an object representing the completed process :raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process exits with a nonzero return code """ async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: buffer = BytesIO() async for chunk in stream: buffer.write(chunk) stream_contents[index] = buffer.getvalue() async with await open_process( command, stdin=PIPE if input else DEVNULL, stdout=stdout, stderr=stderr, cwd=cwd, env=env, start_new_session=start_new_session, ) as process: stream_contents: list[bytes | None] = [None, None] async with create_task_group() as tg: if process.stdout: tg.start_soon(drain_stream, process.stdout, 0) if process.stderr: tg.start_soon(drain_stream, process.stderr, 1) if process.stdin and input: await process.stdin.send(input) await process.stdin.aclose() await process.wait() output, errors = stream_contents if check and process.returncode != 0: raise CalledProcessError(cast(int, process.returncode), command, output, errors) return CompletedProcess(command, cast(int, process.returncode), output, errors) async def open_process( command: str | bytes | Sequence[str | bytes], *, stdin: int | IO[Any] | None = PIPE, stdout: int | IO[Any] | None = PIPE, stderr: int | IO[Any] | None = PIPE, cwd: str | bytes | PathLike[str] | None = None, env: Mapping[str, str] | None = None, start_new_session: bool = False, ) -> Process: """ Start an external command in a subprocess. .. seealso:: :class:`subprocess.Popen` :param command: either a string to pass to the shell, or an iterable of strings containing the executable name or path and its arguments :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a file-like object, or ``None`` :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a file-like object, or ``None`` :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, :data:`subprocess.STDOUT`, a file-like object, or ``None`` :param cwd: If not ``None``, the working directory is changed before executing :param env: If env is not ``None``, it must be a mapping that defines the environment variables for the new process :param start_new_session: if ``true`` the setsid() system call will be made in the child process prior to the execution of the subprocess. (POSIX only) :return: an asynchronous process object """ if isinstance(command, (str, bytes)): return await get_async_backend().open_process( command, shell=True, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd, env=env, start_new_session=start_new_session, ) else: return await get_async_backend().open_process( command, shell=False, stdin=stdin, stdout=stdout, stderr=stderr, cwd=cwd, env=env, start_new_session=start_new_session, ) anyio-4.4.0/src/anyio/_core/_synchronization.py000066400000000000000000000440141462472665400216300ustar00rootroot00000000000000from __future__ import annotations import math from collections import deque from dataclasses import dataclass from types import TracebackType from sniffio import AsyncLibraryNotFoundError from ..lowlevel import cancel_shielded_checkpoint, checkpoint, checkpoint_if_cancelled from ._eventloop import get_async_backend from ._exceptions import BusyResourceError, WouldBlock from ._tasks import CancelScope from ._testing import TaskInfo, get_current_task @dataclass(frozen=True) class EventStatistics: """ :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait` """ tasks_waiting: int @dataclass(frozen=True) class CapacityLimiterStatistics: """ :ivar int borrowed_tokens: number of tokens currently borrowed by tasks :ivar float total_tokens: total number of available tokens :ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from this limiter :ivar int tasks_waiting: number of tasks waiting on :meth:`~.CapacityLimiter.acquire` or :meth:`~.CapacityLimiter.acquire_on_behalf_of` """ borrowed_tokens: int total_tokens: float borrowers: tuple[object, ...] tasks_waiting: int @dataclass(frozen=True) class LockStatistics: """ :ivar bool locked: flag indicating if this lock is locked or not :ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the lock is not held by any task) :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire` """ locked: bool owner: TaskInfo | None tasks_waiting: int @dataclass(frozen=True) class ConditionStatistics: """ :ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait` :ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying :class:`~.Lock` """ tasks_waiting: int lock_statistics: LockStatistics @dataclass(frozen=True) class SemaphoreStatistics: """ :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire` """ tasks_waiting: int class Event: def __new__(cls) -> Event: try: return get_async_backend().create_event() except AsyncLibraryNotFoundError: return EventAdapter() def set(self) -> None: """Set the flag, notifying all listeners.""" raise NotImplementedError def is_set(self) -> bool: """Return ``True`` if the flag is set, ``False`` if not.""" raise NotImplementedError async def wait(self) -> None: """ Wait until the flag has been set. If the flag has already been set when this method is called, it returns immediately. """ raise NotImplementedError def statistics(self) -> EventStatistics: """Return statistics about the current state of this event.""" raise NotImplementedError class EventAdapter(Event): _internal_event: Event | None = None def __new__(cls) -> EventAdapter: return object.__new__(cls) @property def _event(self) -> Event: if self._internal_event is None: self._internal_event = get_async_backend().create_event() return self._internal_event def set(self) -> None: self._event.set() def is_set(self) -> bool: return self._internal_event is not None and self._internal_event.is_set() async def wait(self) -> None: await self._event.wait() def statistics(self) -> EventStatistics: if self._internal_event is None: return EventStatistics(tasks_waiting=0) return self._internal_event.statistics() class Lock: _owner_task: TaskInfo | None = None def __init__(self) -> None: self._waiters: deque[tuple[TaskInfo, Event]] = deque() async def __aenter__(self) -> None: await self.acquire() async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.release() async def acquire(self) -> None: """Acquire the lock.""" await checkpoint_if_cancelled() try: self.acquire_nowait() except WouldBlock: task = get_current_task() event = Event() token = task, event self._waiters.append(token) try: await event.wait() except BaseException: if not event.is_set(): self._waiters.remove(token) elif self._owner_task == task: self.release() raise assert self._owner_task == task else: try: await cancel_shielded_checkpoint() except BaseException: self.release() raise def acquire_nowait(self) -> None: """ Acquire the lock, without blocking. :raises ~anyio.WouldBlock: if the operation would block """ task = get_current_task() if self._owner_task == task: raise RuntimeError("Attempted to acquire an already held Lock") if self._owner_task is not None: raise WouldBlock self._owner_task = task def release(self) -> None: """Release the lock.""" if self._owner_task != get_current_task(): raise RuntimeError("The current task is not holding this lock") if self._waiters: self._owner_task, event = self._waiters.popleft() event.set() else: del self._owner_task def locked(self) -> bool: """Return True if the lock is currently held.""" return self._owner_task is not None def statistics(self) -> LockStatistics: """ Return statistics about the current state of this lock. .. versionadded:: 3.0 """ return LockStatistics(self.locked(), self._owner_task, len(self._waiters)) class Condition: _owner_task: TaskInfo | None = None def __init__(self, lock: Lock | None = None): self._lock = lock or Lock() self._waiters: deque[Event] = deque() async def __aenter__(self) -> None: await self.acquire() async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.release() def _check_acquired(self) -> None: if self._owner_task != get_current_task(): raise RuntimeError("The current task is not holding the underlying lock") async def acquire(self) -> None: """Acquire the underlying lock.""" await self._lock.acquire() self._owner_task = get_current_task() def acquire_nowait(self) -> None: """ Acquire the underlying lock, without blocking. :raises ~anyio.WouldBlock: if the operation would block """ self._lock.acquire_nowait() self._owner_task = get_current_task() def release(self) -> None: """Release the underlying lock.""" self._lock.release() def locked(self) -> bool: """Return True if the lock is set.""" return self._lock.locked() def notify(self, n: int = 1) -> None: """Notify exactly n listeners.""" self._check_acquired() for _ in range(n): try: event = self._waiters.popleft() except IndexError: break event.set() def notify_all(self) -> None: """Notify all the listeners.""" self._check_acquired() for event in self._waiters: event.set() self._waiters.clear() async def wait(self) -> None: """Wait for a notification.""" await checkpoint() event = Event() self._waiters.append(event) self.release() try: await event.wait() except BaseException: if not event.is_set(): self._waiters.remove(event) raise finally: with CancelScope(shield=True): await self.acquire() def statistics(self) -> ConditionStatistics: """ Return statistics about the current state of this condition. .. versionadded:: 3.0 """ return ConditionStatistics(len(self._waiters), self._lock.statistics()) class Semaphore: def __init__(self, initial_value: int, *, max_value: int | None = None): if not isinstance(initial_value, int): raise TypeError("initial_value must be an integer") if initial_value < 0: raise ValueError("initial_value must be >= 0") if max_value is not None: if not isinstance(max_value, int): raise TypeError("max_value must be an integer or None") if max_value < initial_value: raise ValueError( "max_value must be equal to or higher than initial_value" ) self._value = initial_value self._max_value = max_value self._waiters: deque[Event] = deque() async def __aenter__(self) -> Semaphore: await self.acquire() return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.release() async def acquire(self) -> None: """Decrement the semaphore value, blocking if necessary.""" await checkpoint_if_cancelled() try: self.acquire_nowait() except WouldBlock: event = Event() self._waiters.append(event) try: await event.wait() except BaseException: if not event.is_set(): self._waiters.remove(event) else: self.release() raise else: try: await cancel_shielded_checkpoint() except BaseException: self.release() raise def acquire_nowait(self) -> None: """ Acquire the underlying lock, without blocking. :raises ~anyio.WouldBlock: if the operation would block """ if self._value == 0: raise WouldBlock self._value -= 1 def release(self) -> None: """Increment the semaphore value.""" if self._max_value is not None and self._value == self._max_value: raise ValueError("semaphore released too many times") if self._waiters: self._waiters.popleft().set() else: self._value += 1 @property def value(self) -> int: """The current value of the semaphore.""" return self._value @property def max_value(self) -> int | None: """The maximum value of the semaphore.""" return self._max_value def statistics(self) -> SemaphoreStatistics: """ Return statistics about the current state of this semaphore. .. versionadded:: 3.0 """ return SemaphoreStatistics(len(self._waiters)) class CapacityLimiter: def __new__(cls, total_tokens: float) -> CapacityLimiter: try: return get_async_backend().create_capacity_limiter(total_tokens) except AsyncLibraryNotFoundError: return CapacityLimiterAdapter(total_tokens) async def __aenter__(self) -> None: raise NotImplementedError async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: raise NotImplementedError @property def total_tokens(self) -> float: """ The total number of tokens available for borrowing. This is a read-write property. If the total number of tokens is increased, the proportionate number of tasks waiting on this limiter will be granted their tokens. .. versionchanged:: 3.0 The property is now writable. """ raise NotImplementedError @total_tokens.setter def total_tokens(self, value: float) -> None: raise NotImplementedError @property def borrowed_tokens(self) -> int: """The number of tokens that have currently been borrowed.""" raise NotImplementedError @property def available_tokens(self) -> float: """The number of tokens currently available to be borrowed""" raise NotImplementedError def acquire_nowait(self) -> None: """ Acquire a token for the current task without waiting for one to become available. :raises ~anyio.WouldBlock: if there are no tokens available for borrowing """ raise NotImplementedError def acquire_on_behalf_of_nowait(self, borrower: object) -> None: """ Acquire a token without waiting for one to become available. :param borrower: the entity borrowing a token :raises ~anyio.WouldBlock: if there are no tokens available for borrowing """ raise NotImplementedError async def acquire(self) -> None: """ Acquire a token for the current task, waiting if necessary for one to become available. """ raise NotImplementedError async def acquire_on_behalf_of(self, borrower: object) -> None: """ Acquire a token, waiting if necessary for one to become available. :param borrower: the entity borrowing a token """ raise NotImplementedError def release(self) -> None: """ Release the token held by the current task. :raises RuntimeError: if the current task has not borrowed a token from this limiter. """ raise NotImplementedError def release_on_behalf_of(self, borrower: object) -> None: """ Release the token held by the given borrower. :raises RuntimeError: if the borrower has not borrowed a token from this limiter. """ raise NotImplementedError def statistics(self) -> CapacityLimiterStatistics: """ Return statistics about the current state of this limiter. .. versionadded:: 3.0 """ raise NotImplementedError class CapacityLimiterAdapter(CapacityLimiter): _internal_limiter: CapacityLimiter | None = None def __new__(cls, total_tokens: float) -> CapacityLimiterAdapter: return object.__new__(cls) def __init__(self, total_tokens: float) -> None: self.total_tokens = total_tokens @property def _limiter(self) -> CapacityLimiter: if self._internal_limiter is None: self._internal_limiter = get_async_backend().create_capacity_limiter( self._total_tokens ) return self._internal_limiter async def __aenter__(self) -> None: await self._limiter.__aenter__() async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: return await self._limiter.__aexit__(exc_type, exc_val, exc_tb) @property def total_tokens(self) -> float: if self._internal_limiter is None: return self._total_tokens return self._internal_limiter.total_tokens @total_tokens.setter def total_tokens(self, value: float) -> None: if not isinstance(value, int) and value is not math.inf: raise TypeError("total_tokens must be an int or math.inf") elif value < 1: raise ValueError("total_tokens must be >= 1") if self._internal_limiter is None: self._total_tokens = value return self._limiter.total_tokens = value @property def borrowed_tokens(self) -> int: if self._internal_limiter is None: return 0 return self._internal_limiter.borrowed_tokens @property def available_tokens(self) -> float: if self._internal_limiter is None: return self._total_tokens return self._internal_limiter.available_tokens def acquire_nowait(self) -> None: self._limiter.acquire_nowait() def acquire_on_behalf_of_nowait(self, borrower: object) -> None: self._limiter.acquire_on_behalf_of_nowait(borrower) async def acquire(self) -> None: await self._limiter.acquire() async def acquire_on_behalf_of(self, borrower: object) -> None: await self._limiter.acquire_on_behalf_of(borrower) def release(self) -> None: self._limiter.release() def release_on_behalf_of(self, borrower: object) -> None: self._limiter.release_on_behalf_of(borrower) def statistics(self) -> CapacityLimiterStatistics: if self._internal_limiter is None: return CapacityLimiterStatistics( borrowed_tokens=0, total_tokens=self.total_tokens, borrowers=(), tasks_waiting=0, ) return self._internal_limiter.statistics() class ResourceGuard: """ A context manager for ensuring that a resource is only used by a single task at a time. Entering this context manager while the previous has not exited it yet will trigger :exc:`BusyResourceError`. :param action: the action to guard against (visible in the :exc:`BusyResourceError` when triggered, e.g. "Another task is already {action} this resource") .. versionadded:: 4.1 """ __slots__ = "action", "_guarded" def __init__(self, action: str = "using"): self.action: str = action self._guarded = False def __enter__(self) -> None: if self._guarded: raise BusyResourceError(self.action) self._guarded = True def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: self._guarded = False return None anyio-4.4.0/src/anyio/_core/_tasks.py000066400000000000000000000112341462472665400175120ustar00rootroot00000000000000from __future__ import annotations import math from collections.abc import Generator from contextlib import contextmanager from types import TracebackType from ..abc._tasks import TaskGroup, TaskStatus from ._eventloop import get_async_backend class _IgnoredTaskStatus(TaskStatus[object]): def started(self, value: object = None) -> None: pass TASK_STATUS_IGNORED = _IgnoredTaskStatus() class CancelScope: """ Wraps a unit of work that can be made separately cancellable. :param deadline: The time (clock value) when this scope is cancelled automatically :param shield: ``True`` to shield the cancel scope from external cancellation """ def __new__( cls, *, deadline: float = math.inf, shield: bool = False ) -> CancelScope: return get_async_backend().create_cancel_scope(shield=shield, deadline=deadline) def cancel(self) -> None: """Cancel this scope immediately.""" raise NotImplementedError @property def deadline(self) -> float: """ The time (clock value) when this scope is cancelled automatically. Will be ``float('inf')`` if no timeout has been set. """ raise NotImplementedError @deadline.setter def deadline(self, value: float) -> None: raise NotImplementedError @property def cancel_called(self) -> bool: """``True`` if :meth:`cancel` has been called.""" raise NotImplementedError @property def cancelled_caught(self) -> bool: """ ``True`` if this scope suppressed a cancellation exception it itself raised. This is typically used to check if any work was interrupted, or to see if the scope was cancelled due to its deadline being reached. The value will, however, only be ``True`` if the cancellation was triggered by the scope itself (and not an outer scope). """ raise NotImplementedError @property def shield(self) -> bool: """ ``True`` if this scope is shielded from external cancellation. While a scope is shielded, it will not receive cancellations from outside. """ raise NotImplementedError @shield.setter def shield(self, value: bool) -> None: raise NotImplementedError def __enter__(self) -> CancelScope: raise NotImplementedError def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: raise NotImplementedError @contextmanager def fail_after( delay: float | None, shield: bool = False ) -> Generator[CancelScope, None, None]: """ Create a context manager which raises a :class:`TimeoutError` if does not finish in time. :param delay: maximum allowed time (in seconds) before raising the exception, or ``None`` to disable the timeout :param shield: ``True`` to shield the cancel scope from external cancellation :return: a context manager that yields a cancel scope :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\] """ current_time = get_async_backend().current_time deadline = (current_time() + delay) if delay is not None else math.inf with get_async_backend().create_cancel_scope( deadline=deadline, shield=shield ) as cancel_scope: yield cancel_scope if cancel_scope.cancelled_caught and current_time() >= cancel_scope.deadline: raise TimeoutError def move_on_after(delay: float | None, shield: bool = False) -> CancelScope: """ Create a cancel scope with a deadline that expires after the given delay. :param delay: maximum allowed time (in seconds) before exiting the context block, or ``None`` to disable the timeout :param shield: ``True`` to shield the cancel scope from external cancellation :return: a cancel scope """ deadline = ( (get_async_backend().current_time() + delay) if delay is not None else math.inf ) return get_async_backend().create_cancel_scope(deadline=deadline, shield=shield) def current_effective_deadline() -> float: """ Return the nearest deadline among all the cancel scopes effective for the current task. :return: a clock value from the event loop's internal clock (or ``float('inf')`` if there is no deadline in effect, or ``float('-inf')`` if the current scope has been cancelled) :rtype: float """ return get_async_backend().current_effective_deadline() def create_task_group() -> TaskGroup: """ Create a task group. :return: a task group """ return get_async_backend().create_task_group() anyio-4.4.0/src/anyio/_core/_testing.py000066400000000000000000000041061462472665400200420ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Awaitable, Generator from typing import Any, cast from ._eventloop import get_async_backend class TaskInfo: """ Represents an asynchronous task. :ivar int id: the unique identifier of the task :ivar parent_id: the identifier of the parent task, if any :vartype parent_id: Optional[int] :ivar str name: the description of the task (if any) :ivar ~collections.abc.Coroutine coro: the coroutine object of the task """ __slots__ = "_name", "id", "parent_id", "name", "coro" def __init__( self, id: int, parent_id: int | None, name: str | None, coro: Generator[Any, Any, Any] | Awaitable[Any], ): func = get_current_task self._name = f"{func.__module__}.{func.__qualname__}" self.id: int = id self.parent_id: int | None = parent_id self.name: str | None = name self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro def __eq__(self, other: object) -> bool: if isinstance(other, TaskInfo): return self.id == other.id return NotImplemented def __hash__(self) -> int: return hash(self.id) def __repr__(self) -> str: return f"{self.__class__.__name__}(id={self.id!r}, name={self.name!r})" def has_pending_cancellation(self) -> bool: """ Return ``True`` if the task has a cancellation pending, ``False`` otherwise. """ return False def get_current_task() -> TaskInfo: """ Return the current task. :return: a representation of the current task """ return get_async_backend().get_current_task() def get_running_tasks() -> list[TaskInfo]: """ Return a list of running tasks in the current event loop. :return: a list of task info objects """ return cast("list[TaskInfo]", get_async_backend().get_running_tasks()) async def wait_all_tasks_blocked() -> None: """Wait until all other tasks are waiting for something.""" await get_async_backend().wait_all_tasks_blocked() anyio-4.4.0/src/anyio/_core/_typedattr.py000066400000000000000000000047141462472665400204120ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable, Mapping from typing import Any, TypeVar, final, overload from ._exceptions import TypedAttributeLookupError T_Attr = TypeVar("T_Attr") T_Default = TypeVar("T_Default") undefined = object() def typed_attribute() -> Any: """Return a unique object, used to mark typed attributes.""" return object() class TypedAttributeSet: """ Superclass for typed attribute collections. Checks that every public attribute of every subclass has a type annotation. """ def __init_subclass__(cls) -> None: annotations: dict[str, Any] = getattr(cls, "__annotations__", {}) for attrname in dir(cls): if not attrname.startswith("_") and attrname not in annotations: raise TypeError( f"Attribute {attrname!r} is missing its type annotation" ) super().__init_subclass__() class TypedAttributeProvider: """Base class for classes that wish to provide typed extra attributes.""" @property def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]: """ A mapping of the extra attributes to callables that return the corresponding values. If the provider wraps another provider, the attributes from that wrapper should also be included in the returned mapping (but the wrapper may override the callables from the wrapped instance). """ return {} @overload def extra(self, attribute: T_Attr) -> T_Attr: ... @overload def extra(self, attribute: T_Attr, default: T_Default) -> T_Attr | T_Default: ... @final def extra(self, attribute: Any, default: object = undefined) -> object: """ extra(attribute, default=undefined) Return the value of the given typed extra attribute. :param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to look for :param default: the value that should be returned if no value is found for the attribute :raises ~anyio.TypedAttributeLookupError: if the search failed and no default value was given """ try: getter = self.extra_attributes[attribute] except KeyError: if default is undefined: raise TypedAttributeLookupError("Attribute not found") from None else: return default return getter() anyio-4.4.0/src/anyio/abc/000077500000000000000000000000001462472665400153115ustar00rootroot00000000000000anyio-4.4.0/src/anyio/abc/__init__.py000066400000000000000000000051711462472665400174260ustar00rootroot00000000000000from __future__ import annotations from typing import Any from ._eventloop import AsyncBackend as AsyncBackend from ._resources import AsyncResource as AsyncResource from ._sockets import ConnectedUDPSocket as ConnectedUDPSocket from ._sockets import ConnectedUNIXDatagramSocket as ConnectedUNIXDatagramSocket from ._sockets import IPAddressType as IPAddressType from ._sockets import IPSockAddrType as IPSockAddrType from ._sockets import SocketAttribute as SocketAttribute from ._sockets import SocketListener as SocketListener from ._sockets import SocketStream as SocketStream from ._sockets import UDPPacketType as UDPPacketType from ._sockets import UDPSocket as UDPSocket from ._sockets import UNIXDatagramPacketType as UNIXDatagramPacketType from ._sockets import UNIXDatagramSocket as UNIXDatagramSocket from ._sockets import UNIXSocketStream as UNIXSocketStream from ._streams import AnyByteReceiveStream as AnyByteReceiveStream from ._streams import AnyByteSendStream as AnyByteSendStream from ._streams import AnyByteStream as AnyByteStream from ._streams import AnyUnreliableByteReceiveStream as AnyUnreliableByteReceiveStream from ._streams import AnyUnreliableByteSendStream as AnyUnreliableByteSendStream from ._streams import AnyUnreliableByteStream as AnyUnreliableByteStream from ._streams import ByteReceiveStream as ByteReceiveStream from ._streams import ByteSendStream as ByteSendStream from ._streams import ByteStream as ByteStream from ._streams import Listener as Listener from ._streams import ObjectReceiveStream as ObjectReceiveStream from ._streams import ObjectSendStream as ObjectSendStream from ._streams import ObjectStream as ObjectStream from ._streams import UnreliableObjectReceiveStream as UnreliableObjectReceiveStream from ._streams import UnreliableObjectSendStream as UnreliableObjectSendStream from ._streams import UnreliableObjectStream as UnreliableObjectStream from ._subprocesses import Process as Process from ._tasks import TaskGroup as TaskGroup from ._tasks import TaskStatus as TaskStatus from ._testing import TestRunner as TestRunner # Re-exported here, for backwards compatibility # isort: off from .._core._synchronization import ( CapacityLimiter as CapacityLimiter, Condition as Condition, Event as Event, Lock as Lock, Semaphore as Semaphore, ) from .._core._tasks import CancelScope as CancelScope from ..from_thread import BlockingPortal as BlockingPortal # Re-export imports so they look like they live directly in this package key: str value: Any for key, value in list(locals().items()): if getattr(value, "__module__", "").startswith("anyio.abc."): value.__module__ = __name__ anyio-4.4.0/src/anyio/abc/_eventloop.py000066400000000000000000000235451462472665400200460ustar00rootroot00000000000000from __future__ import annotations import math import sys from abc import ABCMeta, abstractmethod from collections.abc import AsyncIterator, Awaitable, Mapping from os import PathLike from signal import Signals from socket import AddressFamily, SocketKind, socket from typing import ( IO, TYPE_CHECKING, Any, Callable, ContextManager, Sequence, TypeVar, overload, ) if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: from typing_extensions import TypeVarTuple, Unpack if TYPE_CHECKING: from typing import Literal from .._core._synchronization import CapacityLimiter, Event from .._core._tasks import CancelScope from .._core._testing import TaskInfo from ..from_thread import BlockingPortal from ._sockets import ( ConnectedUDPSocket, ConnectedUNIXDatagramSocket, IPSockAddrType, SocketListener, SocketStream, UDPSocket, UNIXDatagramSocket, UNIXSocketStream, ) from ._subprocesses import Process from ._tasks import TaskGroup from ._testing import TestRunner T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") class AsyncBackend(metaclass=ABCMeta): @classmethod @abstractmethod def run( cls, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], args: tuple[Unpack[PosArgsT]], kwargs: dict[str, Any], options: dict[str, Any], ) -> T_Retval: """ Run the given coroutine function in an asynchronous event loop. The current thread must not be already running an event loop. :param func: a coroutine function :param args: positional arguments to ``func`` :param kwargs: positional arguments to ``func`` :param options: keyword arguments to call the backend ``run()`` implementation with :return: the return value of the coroutine function """ @classmethod @abstractmethod def current_token(cls) -> object: """ :return: """ @classmethod @abstractmethod def current_time(cls) -> float: """ Return the current value of the event loop's internal clock. :return: the clock value (seconds) """ @classmethod @abstractmethod def cancelled_exception_class(cls) -> type[BaseException]: """Return the exception class that is raised in a task if it's cancelled.""" @classmethod @abstractmethod async def checkpoint(cls) -> None: """ Check if the task has been cancelled, and allow rescheduling of other tasks. This is effectively the same as running :meth:`checkpoint_if_cancelled` and then :meth:`cancel_shielded_checkpoint`. """ @classmethod async def checkpoint_if_cancelled(cls) -> None: """ Check if the current task group has been cancelled. This will check if the task has been cancelled, but will not allow other tasks to be scheduled if not. """ if cls.current_effective_deadline() == -math.inf: await cls.checkpoint() @classmethod async def cancel_shielded_checkpoint(cls) -> None: """ Allow the rescheduling of other tasks. This will give other tasks the opportunity to run, but without checking if the current task group has been cancelled, unlike with :meth:`checkpoint`. """ with cls.create_cancel_scope(shield=True): await cls.sleep(0) @classmethod @abstractmethod async def sleep(cls, delay: float) -> None: """ Pause the current task for the specified duration. :param delay: the duration, in seconds """ @classmethod @abstractmethod def create_cancel_scope( cls, *, deadline: float = math.inf, shield: bool = False ) -> CancelScope: pass @classmethod @abstractmethod def current_effective_deadline(cls) -> float: """ Return the nearest deadline among all the cancel scopes effective for the current task. :return: - a clock value from the event loop's internal clock - ``inf`` if there is no deadline in effect - ``-inf`` if the current scope has been cancelled :rtype: float """ @classmethod @abstractmethod def create_task_group(cls) -> TaskGroup: pass @classmethod @abstractmethod def create_event(cls) -> Event: pass @classmethod @abstractmethod def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: pass @classmethod @abstractmethod async def run_sync_in_worker_thread( cls, func: Callable[[Unpack[PosArgsT]], T_Retval], args: tuple[Unpack[PosArgsT]], abandon_on_cancel: bool = False, limiter: CapacityLimiter | None = None, ) -> T_Retval: pass @classmethod @abstractmethod def check_cancelled(cls) -> None: pass @classmethod @abstractmethod def run_async_from_thread( cls, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], args: tuple[Unpack[PosArgsT]], token: object, ) -> T_Retval: pass @classmethod @abstractmethod def run_sync_from_thread( cls, func: Callable[[Unpack[PosArgsT]], T_Retval], args: tuple[Unpack[PosArgsT]], token: object, ) -> T_Retval: pass @classmethod @abstractmethod def create_blocking_portal(cls) -> BlockingPortal: pass @classmethod @overload async def open_process( cls, command: str | bytes, *, shell: Literal[True], stdin: int | IO[Any] | None, stdout: int | IO[Any] | None, stderr: int | IO[Any] | None, cwd: str | bytes | PathLike[str] | None = None, env: Mapping[str, str] | None = None, start_new_session: bool = False, ) -> Process: pass @classmethod @overload async def open_process( cls, command: Sequence[str | bytes], *, shell: Literal[False], stdin: int | IO[Any] | None, stdout: int | IO[Any] | None, stderr: int | IO[Any] | None, cwd: str | bytes | PathLike[str] | None = None, env: Mapping[str, str] | None = None, start_new_session: bool = False, ) -> Process: pass @classmethod @abstractmethod async def open_process( cls, command: str | bytes | Sequence[str | bytes], *, shell: bool, stdin: int | IO[Any] | None, stdout: int | IO[Any] | None, stderr: int | IO[Any] | None, cwd: str | bytes | PathLike[str] | None = None, env: Mapping[str, str] | None = None, start_new_session: bool = False, ) -> Process: pass @classmethod @abstractmethod def setup_process_pool_exit_at_shutdown(cls, workers: set[Process]) -> None: pass @classmethod @abstractmethod async def connect_tcp( cls, host: str, port: int, local_address: IPSockAddrType | None = None ) -> SocketStream: pass @classmethod @abstractmethod async def connect_unix(cls, path: str | bytes) -> UNIXSocketStream: pass @classmethod @abstractmethod def create_tcp_listener(cls, sock: socket) -> SocketListener: pass @classmethod @abstractmethod def create_unix_listener(cls, sock: socket) -> SocketListener: pass @classmethod @abstractmethod async def create_udp_socket( cls, family: AddressFamily, local_address: IPSockAddrType | None, remote_address: IPSockAddrType | None, reuse_port: bool, ) -> UDPSocket | ConnectedUDPSocket: pass @classmethod @overload async def create_unix_datagram_socket( cls, raw_socket: socket, remote_path: None ) -> UNIXDatagramSocket: ... @classmethod @overload async def create_unix_datagram_socket( cls, raw_socket: socket, remote_path: str | bytes ) -> ConnectedUNIXDatagramSocket: ... @classmethod @abstractmethod async def create_unix_datagram_socket( cls, raw_socket: socket, remote_path: str | bytes | None ) -> UNIXDatagramSocket | ConnectedUNIXDatagramSocket: pass @classmethod @abstractmethod async def getaddrinfo( cls, host: bytes | str | None, port: str | int | None, *, family: int | AddressFamily = 0, type: int | SocketKind = 0, proto: int = 0, flags: int = 0, ) -> list[ tuple[ AddressFamily, SocketKind, int, str, tuple[str, int] | tuple[str, int, int, int], ] ]: pass @classmethod @abstractmethod async def getnameinfo( cls, sockaddr: IPSockAddrType, flags: int = 0 ) -> tuple[str, str]: pass @classmethod @abstractmethod async def wait_socket_readable(cls, sock: socket) -> None: pass @classmethod @abstractmethod async def wait_socket_writable(cls, sock: socket) -> None: pass @classmethod @abstractmethod def current_default_thread_limiter(cls) -> CapacityLimiter: pass @classmethod @abstractmethod def open_signal_receiver( cls, *signals: Signals ) -> ContextManager[AsyncIterator[Signals]]: pass @classmethod @abstractmethod def get_current_task(cls) -> TaskInfo: pass @classmethod @abstractmethod def get_running_tasks(cls) -> Sequence[TaskInfo]: pass @classmethod @abstractmethod async def wait_all_tasks_blocked(cls) -> None: pass @classmethod @abstractmethod def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: pass anyio-4.4.0/src/anyio/abc/_resources.py000066400000000000000000000014171462472665400200370ustar00rootroot00000000000000from __future__ import annotations from abc import ABCMeta, abstractmethod from types import TracebackType from typing import TypeVar T = TypeVar("T") class AsyncResource(metaclass=ABCMeta): """ Abstract base class for all closeable asynchronous resources. Works as an asynchronous context manager which returns the instance itself on enter, and calls :meth:`aclose` on exit. """ __slots__ = () async def __aenter__(self: T) -> T: return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: await self.aclose() @abstractmethod async def aclose(self) -> None: """Close the resource.""" anyio-4.4.0/src/anyio/abc/_sockets.py000066400000000000000000000141751462472665400175050ustar00rootroot00000000000000from __future__ import annotations import socket from abc import abstractmethod from collections.abc import Callable, Collection, Mapping from contextlib import AsyncExitStack from io import IOBase from ipaddress import IPv4Address, IPv6Address from socket import AddressFamily from types import TracebackType from typing import Any, Tuple, TypeVar, Union from .._core._typedattr import ( TypedAttributeProvider, TypedAttributeSet, typed_attribute, ) from ._streams import ByteStream, Listener, UnreliableObjectStream from ._tasks import TaskGroup IPAddressType = Union[str, IPv4Address, IPv6Address] IPSockAddrType = Tuple[str, int] SockAddrType = Union[IPSockAddrType, str] UDPPacketType = Tuple[bytes, IPSockAddrType] UNIXDatagramPacketType = Tuple[bytes, str] T_Retval = TypeVar("T_Retval") class _NullAsyncContextManager: async def __aenter__(self) -> None: pass async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: return None class SocketAttribute(TypedAttributeSet): #: the address family of the underlying socket family: AddressFamily = typed_attribute() #: the local socket address of the underlying socket local_address: SockAddrType = typed_attribute() #: for IP addresses, the local port the underlying socket is bound to local_port: int = typed_attribute() #: the underlying stdlib socket object raw_socket: socket.socket = typed_attribute() #: the remote address the underlying socket is connected to remote_address: SockAddrType = typed_attribute() #: for IP addresses, the remote port the underlying socket is connected to remote_port: int = typed_attribute() class _SocketProvider(TypedAttributeProvider): @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: from .._core._sockets import convert_ipv6_sockaddr as convert attributes: dict[Any, Callable[[], Any]] = { SocketAttribute.family: lambda: self._raw_socket.family, SocketAttribute.local_address: lambda: convert( self._raw_socket.getsockname() ), SocketAttribute.raw_socket: lambda: self._raw_socket, } try: peername: tuple[str, int] | None = convert(self._raw_socket.getpeername()) except OSError: peername = None # Provide the remote address for connected sockets if peername is not None: attributes[SocketAttribute.remote_address] = lambda: peername # Provide local and remote ports for IP based sockets if self._raw_socket.family in (AddressFamily.AF_INET, AddressFamily.AF_INET6): attributes[SocketAttribute.local_port] = ( lambda: self._raw_socket.getsockname()[1] ) if peername is not None: remote_port = peername[1] attributes[SocketAttribute.remote_port] = lambda: remote_port return attributes @property @abstractmethod def _raw_socket(self) -> socket.socket: pass class SocketStream(ByteStream, _SocketProvider): """ Transports bytes over a socket. Supports all relevant extra attributes from :class:`~SocketAttribute`. """ class UNIXSocketStream(SocketStream): @abstractmethod async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: """ Send file descriptors along with a message to the peer. :param message: a non-empty bytestring :param fds: a collection of files (either numeric file descriptors or open file or socket objects) """ @abstractmethod async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: """ Receive file descriptors along with a message from the peer. :param msglen: length of the message to expect from the peer :param maxfds: maximum number of file descriptors to expect from the peer :return: a tuple of (message, file descriptors) """ class SocketListener(Listener[SocketStream], _SocketProvider): """ Listens to incoming socket connections. Supports all relevant extra attributes from :class:`~SocketAttribute`. """ @abstractmethod async def accept(self) -> SocketStream: """Accept an incoming connection.""" async def serve( self, handler: Callable[[SocketStream], Any], task_group: TaskGroup | None = None, ) -> None: from .. import create_task_group async with AsyncExitStack() as stack: if task_group is None: task_group = await stack.enter_async_context(create_task_group()) while True: stream = await self.accept() task_group.start_soon(handler, stream) class UDPSocket(UnreliableObjectStream[UDPPacketType], _SocketProvider): """ Represents an unconnected UDP socket. Supports all relevant extra attributes from :class:`~SocketAttribute`. """ async def sendto(self, data: bytes, host: str, port: int) -> None: """ Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, (host, port))). """ return await self.send((data, (host, port))) class ConnectedUDPSocket(UnreliableObjectStream[bytes], _SocketProvider): """ Represents an connected UDP socket. Supports all relevant extra attributes from :class:`~SocketAttribute`. """ class UNIXDatagramSocket( UnreliableObjectStream[UNIXDatagramPacketType], _SocketProvider ): """ Represents an unconnected Unix datagram socket. Supports all relevant extra attributes from :class:`~SocketAttribute`. """ async def sendto(self, data: bytes, path: str) -> None: """Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, path)).""" return await self.send((data, path)) class ConnectedUNIXDatagramSocket(UnreliableObjectStream[bytes], _SocketProvider): """ Represents a connected Unix datagram socket. Supports all relevant extra attributes from :class:`~SocketAttribute`. """ anyio-4.4.0/src/anyio/abc/_streams.py000066400000000000000000000147061462472665400175100ustar00rootroot00000000000000from __future__ import annotations from abc import abstractmethod from collections.abc import Callable from typing import Any, Generic, TypeVar, Union from .._core._exceptions import EndOfStream from .._core._typedattr import TypedAttributeProvider from ._resources import AsyncResource from ._tasks import TaskGroup T_Item = TypeVar("T_Item") T_co = TypeVar("T_co", covariant=True) T_contra = TypeVar("T_contra", contravariant=True) class UnreliableObjectReceiveStream( Generic[T_co], AsyncResource, TypedAttributeProvider ): """ An interface for receiving objects. This interface makes no guarantees that the received messages arrive in the order in which they were sent, or that no messages are missed. Asynchronously iterating over objects of this type will yield objects matching the given type parameter. """ def __aiter__(self) -> UnreliableObjectReceiveStream[T_co]: return self async def __anext__(self) -> T_co: try: return await self.receive() except EndOfStream: raise StopAsyncIteration @abstractmethod async def receive(self) -> T_co: """ Receive the next item. :raises ~anyio.ClosedResourceError: if the receive stream has been explicitly closed :raises ~anyio.EndOfStream: if this stream has been closed from the other end :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable due to external causes """ class UnreliableObjectSendStream( Generic[T_contra], AsyncResource, TypedAttributeProvider ): """ An interface for sending objects. This interface makes no guarantees that the messages sent will reach the recipient(s) in the same order in which they were sent, or at all. """ @abstractmethod async def send(self, item: T_contra) -> None: """ Send an item to the peer(s). :param item: the item to send :raises ~anyio.ClosedResourceError: if the send stream has been explicitly closed :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable due to external causes """ class UnreliableObjectStream( UnreliableObjectReceiveStream[T_Item], UnreliableObjectSendStream[T_Item] ): """ A bidirectional message stream which does not guarantee the order or reliability of message delivery. """ class ObjectReceiveStream(UnreliableObjectReceiveStream[T_co]): """ A receive message stream which guarantees that messages are received in the same order in which they were sent, and that no messages are missed. """ class ObjectSendStream(UnreliableObjectSendStream[T_contra]): """ A send message stream which guarantees that messages are delivered in the same order in which they were sent, without missing any messages in the middle. """ class ObjectStream( ObjectReceiveStream[T_Item], ObjectSendStream[T_Item], UnreliableObjectStream[T_Item], ): """ A bidirectional message stream which guarantees the order and reliability of message delivery. """ @abstractmethod async def send_eof(self) -> None: """ Send an end-of-file indication to the peer. You should not try to send any further data to this stream after calling this method. This method is idempotent (does nothing on successive calls). """ class ByteReceiveStream(AsyncResource, TypedAttributeProvider): """ An interface for receiving bytes from a single peer. Iterating this byte stream will yield a byte string of arbitrary length, but no more than 65536 bytes. """ def __aiter__(self) -> ByteReceiveStream: return self async def __anext__(self) -> bytes: try: return await self.receive() except EndOfStream: raise StopAsyncIteration @abstractmethod async def receive(self, max_bytes: int = 65536) -> bytes: """ Receive at most ``max_bytes`` bytes from the peer. .. note:: Implementors of this interface should not return an empty :class:`bytes` object, and users should ignore them. :param max_bytes: maximum number of bytes to receive :return: the received bytes :raises ~anyio.EndOfStream: if this stream has been closed from the other end """ class ByteSendStream(AsyncResource, TypedAttributeProvider): """An interface for sending bytes to a single peer.""" @abstractmethod async def send(self, item: bytes) -> None: """ Send the given bytes to the peer. :param item: the bytes to send """ class ByteStream(ByteReceiveStream, ByteSendStream): """A bidirectional byte stream.""" @abstractmethod async def send_eof(self) -> None: """ Send an end-of-file indication to the peer. You should not try to send any further data to this stream after calling this method. This method is idempotent (does nothing on successive calls). """ #: Type alias for all unreliable bytes-oriented receive streams. AnyUnreliableByteReceiveStream = Union[ UnreliableObjectReceiveStream[bytes], ByteReceiveStream ] #: Type alias for all unreliable bytes-oriented send streams. AnyUnreliableByteSendStream = Union[UnreliableObjectSendStream[bytes], ByteSendStream] #: Type alias for all unreliable bytes-oriented streams. AnyUnreliableByteStream = Union[UnreliableObjectStream[bytes], ByteStream] #: Type alias for all bytes-oriented receive streams. AnyByteReceiveStream = Union[ObjectReceiveStream[bytes], ByteReceiveStream] #: Type alias for all bytes-oriented send streams. AnyByteSendStream = Union[ObjectSendStream[bytes], ByteSendStream] #: Type alias for all bytes-oriented streams. AnyByteStream = Union[ObjectStream[bytes], ByteStream] class Listener(Generic[T_co], AsyncResource, TypedAttributeProvider): """An interface for objects that let you accept incoming connections.""" @abstractmethod async def serve( self, handler: Callable[[T_co], Any], task_group: TaskGroup | None = None ) -> None: """ Accept incoming connections as they come in and start tasks to handle them. :param handler: a callable that will be used to handle each accepted connection :param task_group: the task group that will be used to start tasks for handling each accepted connection (if omitted, an ad-hoc task group will be created) """ anyio-4.4.0/src/anyio/abc/_subprocesses.py000066400000000000000000000040231462472665400205410ustar00rootroot00000000000000from __future__ import annotations from abc import abstractmethod from signal import Signals from ._resources import AsyncResource from ._streams import ByteReceiveStream, ByteSendStream class Process(AsyncResource): """An asynchronous version of :class:`subprocess.Popen`.""" @abstractmethod async def wait(self) -> int: """ Wait until the process exits. :return: the exit code of the process """ @abstractmethod def terminate(self) -> None: """ Terminates the process, gracefully if possible. On Windows, this calls ``TerminateProcess()``. On POSIX systems, this sends ``SIGTERM`` to the process. .. seealso:: :meth:`subprocess.Popen.terminate` """ @abstractmethod def kill(self) -> None: """ Kills the process. On Windows, this calls ``TerminateProcess()``. On POSIX systems, this sends ``SIGKILL`` to the process. .. seealso:: :meth:`subprocess.Popen.kill` """ @abstractmethod def send_signal(self, signal: Signals) -> None: """ Send a signal to the subprocess. .. seealso:: :meth:`subprocess.Popen.send_signal` :param signal: the signal number (e.g. :data:`signal.SIGHUP`) """ @property @abstractmethod def pid(self) -> int: """The process ID of the process.""" @property @abstractmethod def returncode(self) -> int | None: """ The return code of the process. If the process has not yet terminated, this will be ``None``. """ @property @abstractmethod def stdin(self) -> ByteSendStream | None: """The stream for the standard input of the process.""" @property @abstractmethod def stdout(self) -> ByteReceiveStream | None: """The stream for the standard output of the process.""" @property @abstractmethod def stderr(self) -> ByteReceiveStream | None: """The stream for the standard error output of the process.""" anyio-4.4.0/src/anyio/abc/_tasks.py000066400000000000000000000052531462472665400171540ustar00rootroot00000000000000from __future__ import annotations import sys from abc import ABCMeta, abstractmethod from collections.abc import Awaitable, Callable from types import TracebackType from typing import TYPE_CHECKING, Any, Protocol, TypeVar, overload if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: from typing_extensions import TypeVarTuple, Unpack if TYPE_CHECKING: from .._core._tasks import CancelScope T_Retval = TypeVar("T_Retval") T_contra = TypeVar("T_contra", contravariant=True) PosArgsT = TypeVarTuple("PosArgsT") class TaskStatus(Protocol[T_contra]): @overload def started(self: TaskStatus[None]) -> None: ... @overload def started(self, value: T_contra) -> None: ... def started(self, value: T_contra | None = None) -> None: """ Signal that the task has started. :param value: object passed back to the starter of the task """ class TaskGroup(metaclass=ABCMeta): """ Groups several asynchronous tasks together. :ivar cancel_scope: the cancel scope inherited by all child tasks :vartype cancel_scope: CancelScope """ cancel_scope: CancelScope @abstractmethod def start_soon( self, func: Callable[[Unpack[PosArgsT]], Awaitable[Any]], *args: Unpack[PosArgsT], name: object = None, ) -> None: """ Start a new task in this task group. :param func: a coroutine function :param args: positional arguments to call the function with :param name: name of the task, for the purposes of introspection and debugging .. versionadded:: 3.0 """ @abstractmethod async def start( self, func: Callable[..., Awaitable[Any]], *args: object, name: object = None, ) -> Any: """ Start a new task and wait until it signals for readiness. :param func: a coroutine function :param args: positional arguments to call the function with :param name: name of the task, for the purposes of introspection and debugging :return: the value passed to ``task_status.started()`` :raises RuntimeError: if the task finishes without calling ``task_status.started()`` .. versionadded:: 3.0 """ @abstractmethod async def __aenter__(self) -> TaskGroup: """Enter the task group context and allow starting new tasks.""" @abstractmethod async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: """Exit the task group context waiting for all tasks to finish.""" anyio-4.4.0/src/anyio/abc/_testing.py000066400000000000000000000034351462472665400175040ustar00rootroot00000000000000from __future__ import annotations import types from abc import ABCMeta, abstractmethod from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable from typing import Any, TypeVar _T = TypeVar("_T") class TestRunner(metaclass=ABCMeta): """ Encapsulates a running event loop. Every call made through this object will use the same event loop. """ def __enter__(self) -> TestRunner: return self @abstractmethod def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: types.TracebackType | None, ) -> bool | None: ... @abstractmethod def run_asyncgen_fixture( self, fixture_func: Callable[..., AsyncGenerator[_T, Any]], kwargs: dict[str, Any], ) -> Iterable[_T]: """ Run an async generator fixture. :param fixture_func: the fixture function :param kwargs: keyword arguments to call the fixture function with :return: an iterator yielding the value yielded from the async generator """ @abstractmethod def run_fixture( self, fixture_func: Callable[..., Coroutine[Any, Any, _T]], kwargs: dict[str, Any], ) -> _T: """ Run an async fixture. :param fixture_func: the fixture function :param kwargs: keyword arguments to call the fixture function with :return: the return value of the fixture function """ @abstractmethod def run_test( self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] ) -> None: """ Run an async test function. :param test_func: the test function :param kwargs: keyword arguments to call the test function with """ anyio-4.4.0/src/anyio/from_thread.py000066400000000000000000000424441462472665400174400ustar00rootroot00000000000000from __future__ import annotations import sys import threading from collections.abc import Awaitable, Callable, Generator from concurrent.futures import FIRST_COMPLETED, Future, ThreadPoolExecutor, wait from contextlib import AbstractContextManager, contextmanager from dataclasses import dataclass, field from inspect import isawaitable from types import TracebackType from typing import ( Any, AsyncContextManager, ContextManager, Generic, Iterable, TypeVar, cast, overload, ) from ._core import _eventloop from ._core._eventloop import get_async_backend, get_cancelled_exc_class, threadlocals from ._core._synchronization import Event from ._core._tasks import CancelScope, create_task_group from .abc import AsyncBackend from .abc._tasks import TaskStatus if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: from typing_extensions import TypeVarTuple, Unpack T_Retval = TypeVar("T_Retval") T_co = TypeVar("T_co", covariant=True) PosArgsT = TypeVarTuple("PosArgsT") def run( func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], *args: Unpack[PosArgsT] ) -> T_Retval: """ Call a coroutine function from a worker thread. :param func: a coroutine function :param args: positional arguments for the callable :return: the return value of the coroutine function """ try: async_backend = threadlocals.current_async_backend token = threadlocals.current_token except AttributeError: raise RuntimeError( "This function can only be run from an AnyIO worker thread" ) from None return async_backend.run_async_from_thread(func, args, token=token) def run_sync( func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] ) -> T_Retval: """ Call a function in the event loop thread from a worker thread. :param func: a callable :param args: positional arguments for the callable :return: the return value of the callable """ try: async_backend = threadlocals.current_async_backend token = threadlocals.current_token except AttributeError: raise RuntimeError( "This function can only be run from an AnyIO worker thread" ) from None return async_backend.run_sync_from_thread(func, args, token=token) class _BlockingAsyncContextManager(Generic[T_co], AbstractContextManager): _enter_future: Future[T_co] _exit_future: Future[bool | None] _exit_event: Event _exit_exc_info: tuple[ type[BaseException] | None, BaseException | None, TracebackType | None ] = (None, None, None) def __init__(self, async_cm: AsyncContextManager[T_co], portal: BlockingPortal): self._async_cm = async_cm self._portal = portal async def run_async_cm(self) -> bool | None: try: self._exit_event = Event() value = await self._async_cm.__aenter__() except BaseException as exc: self._enter_future.set_exception(exc) raise else: self._enter_future.set_result(value) try: # Wait for the sync context manager to exit. # This next statement can raise `get_cancelled_exc_class()` if # something went wrong in a task group in this async context # manager. await self._exit_event.wait() finally: # In case of cancellation, it could be that we end up here before # `_BlockingAsyncContextManager.__exit__` is called, and an # `_exit_exc_info` has been set. result = await self._async_cm.__aexit__(*self._exit_exc_info) return result def __enter__(self) -> T_co: self._enter_future = Future() self._exit_future = self._portal.start_task_soon(self.run_async_cm) return self._enter_future.result() def __exit__( self, __exc_type: type[BaseException] | None, __exc_value: BaseException | None, __traceback: TracebackType | None, ) -> bool | None: self._exit_exc_info = __exc_type, __exc_value, __traceback self._portal.call(self._exit_event.set) return self._exit_future.result() class _BlockingPortalTaskStatus(TaskStatus): def __init__(self, future: Future): self._future = future def started(self, value: object = None) -> None: self._future.set_result(value) class BlockingPortal: """An object that lets external threads run code in an asynchronous event loop.""" def __new__(cls) -> BlockingPortal: return get_async_backend().create_blocking_portal() def __init__(self) -> None: self._event_loop_thread_id: int | None = threading.get_ident() self._stop_event = Event() self._task_group = create_task_group() self._cancelled_exc_class = get_cancelled_exc_class() async def __aenter__(self) -> BlockingPortal: await self._task_group.__aenter__() return self async def __aexit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> bool | None: await self.stop() return await self._task_group.__aexit__(exc_type, exc_val, exc_tb) def _check_running(self) -> None: if self._event_loop_thread_id is None: raise RuntimeError("This portal is not running") if self._event_loop_thread_id == threading.get_ident(): raise RuntimeError( "This method cannot be called from the event loop thread" ) async def sleep_until_stopped(self) -> None: """Sleep until :meth:`stop` is called.""" await self._stop_event.wait() async def stop(self, cancel_remaining: bool = False) -> None: """ Signal the portal to shut down. This marks the portal as no longer accepting new calls and exits from :meth:`sleep_until_stopped`. :param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False`` to let them finish before returning """ self._event_loop_thread_id = None self._stop_event.set() if cancel_remaining: self._task_group.cancel_scope.cancel() async def _call_func( self, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], args: tuple[Unpack[PosArgsT]], kwargs: dict[str, Any], future: Future[T_Retval], ) -> None: def callback(f: Future[T_Retval]) -> None: if f.cancelled() and self._event_loop_thread_id not in ( None, threading.get_ident(), ): self.call(scope.cancel) try: retval_or_awaitable = func(*args, **kwargs) if isawaitable(retval_or_awaitable): with CancelScope() as scope: if future.cancelled(): scope.cancel() else: future.add_done_callback(callback) retval = await retval_or_awaitable else: retval = retval_or_awaitable except self._cancelled_exc_class: future.cancel() future.set_running_or_notify_cancel() except BaseException as exc: if not future.cancelled(): future.set_exception(exc) # Let base exceptions fall through if not isinstance(exc, Exception): raise else: if not future.cancelled(): future.set_result(retval) finally: scope = None # type: ignore[assignment] def _spawn_task_from_thread( self, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], args: tuple[Unpack[PosArgsT]], kwargs: dict[str, Any], name: object, future: Future[T_Retval], ) -> None: """ Spawn a new task using the given callable. Implementors must ensure that the future is resolved when the task finishes. :param func: a callable :param args: positional arguments to be passed to the callable :param kwargs: keyword arguments to be passed to the callable :param name: name of the task (will be coerced to a string if not ``None``) :param future: a future that will resolve to the return value of the callable, or the exception raised during its execution """ raise NotImplementedError @overload def call( self, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], *args: Unpack[PosArgsT], ) -> T_Retval: ... @overload def call( self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] ) -> T_Retval: ... def call( self, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], *args: Unpack[PosArgsT], ) -> T_Retval: """ Call the given function in the event loop thread. If the callable returns a coroutine object, it is awaited on. :param func: any callable :raises RuntimeError: if the portal is not running or if this method is called from within the event loop thread """ return cast(T_Retval, self.start_task_soon(func, *args).result()) @overload def start_task_soon( self, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], *args: Unpack[PosArgsT], name: object = None, ) -> Future[T_Retval]: ... @overload def start_task_soon( self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT], name: object = None, ) -> Future[T_Retval]: ... def start_task_soon( self, func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], *args: Unpack[PosArgsT], name: object = None, ) -> Future[T_Retval]: """ Start a task in the portal's task group. The task will be run inside a cancel scope which can be cancelled by cancelling the returned future. :param func: the target function :param args: positional arguments passed to ``func`` :param name: name of the task (will be coerced to a string if not ``None``) :return: a future that resolves with the return value of the callable if the task completes successfully, or with the exception raised in the task :raises RuntimeError: if the portal is not running or if this method is called from within the event loop thread :rtype: concurrent.futures.Future[T_Retval] .. versionadded:: 3.0 """ self._check_running() f: Future[T_Retval] = Future() self._spawn_task_from_thread(func, args, {}, name, f) return f def start_task( self, func: Callable[..., Awaitable[T_Retval]], *args: object, name: object = None, ) -> tuple[Future[T_Retval], Any]: """ Start a task in the portal's task group and wait until it signals for readiness. This method works the same way as :meth:`.abc.TaskGroup.start`. :param func: the target function :param args: positional arguments passed to ``func`` :param name: name of the task (will be coerced to a string if not ``None``) :return: a tuple of (future, task_status_value) where the ``task_status_value`` is the value passed to ``task_status.started()`` from within the target function :rtype: tuple[concurrent.futures.Future[T_Retval], Any] .. versionadded:: 3.0 """ def task_done(future: Future[T_Retval]) -> None: if not task_status_future.done(): if future.cancelled(): task_status_future.cancel() elif future.exception(): task_status_future.set_exception(future.exception()) else: exc = RuntimeError( "Task exited without calling task_status.started()" ) task_status_future.set_exception(exc) self._check_running() task_status_future: Future = Future() task_status = _BlockingPortalTaskStatus(task_status_future) f: Future = Future() f.add_done_callback(task_done) self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f) return f, task_status_future.result() def wrap_async_context_manager( self, cm: AsyncContextManager[T_co] ) -> ContextManager[T_co]: """ Wrap an async context manager as a synchronous context manager via this portal. Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping in the middle until the synchronous context manager exits. :param cm: an asynchronous context manager :return: a synchronous context manager .. versionadded:: 2.1 """ return _BlockingAsyncContextManager(cm, self) @dataclass class BlockingPortalProvider: """ A manager for a blocking portal. Used as a context manager. The first thread to enter this context manager causes a blocking portal to be started with the specific parameters, and the last thread to exit causes the portal to be shut down. Thus, there will be exactly one blocking portal running in this context as long as at least one thread has entered this context manager. The parameters are the same as for :func:`~anyio.run`. :param backend: name of the backend :param backend_options: backend options .. versionadded:: 4.4 """ backend: str = "asyncio" backend_options: dict[str, Any] | None = None _lock: threading.Lock = field(init=False, default_factory=threading.Lock) _leases: int = field(init=False, default=0) _portal: BlockingPortal = field(init=False) _portal_cm: AbstractContextManager[BlockingPortal] | None = field( init=False, default=None ) def __enter__(self) -> BlockingPortal: with self._lock: if self._portal_cm is None: self._portal_cm = start_blocking_portal( self.backend, self.backend_options ) self._portal = self._portal_cm.__enter__() self._leases += 1 return self._portal def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: portal_cm: AbstractContextManager[BlockingPortal] | None = None with self._lock: assert self._portal_cm assert self._leases > 0 self._leases -= 1 if not self._leases: portal_cm = self._portal_cm self._portal_cm = None del self._portal if portal_cm: portal_cm.__exit__(None, None, None) @contextmanager def start_blocking_portal( backend: str = "asyncio", backend_options: dict[str, Any] | None = None ) -> Generator[BlockingPortal, Any, None]: """ Start a new event loop in a new thread and run a blocking portal in its main task. The parameters are the same as for :func:`~anyio.run`. :param backend: name of the backend :param backend_options: backend options :return: a context manager that yields a blocking portal .. versionchanged:: 3.0 Usage as a context manager is now required. """ async def run_portal() -> None: async with BlockingPortal() as portal_: if future.set_running_or_notify_cancel(): future.set_result(portal_) await portal_.sleep_until_stopped() future: Future[BlockingPortal] = Future() with ThreadPoolExecutor(1) as executor: run_future = executor.submit( _eventloop.run, # type: ignore[arg-type] run_portal, backend=backend, backend_options=backend_options, ) try: wait( cast(Iterable[Future], [run_future, future]), return_when=FIRST_COMPLETED, ) except BaseException: future.cancel() run_future.cancel() raise if future.done(): portal = future.result() cancel_remaining_tasks = False try: yield portal except BaseException: cancel_remaining_tasks = True raise finally: try: portal.call(portal.stop, cancel_remaining_tasks) except RuntimeError: pass run_future.result() def check_cancelled() -> None: """ Check if the cancel scope of the host task's running the current worker thread has been cancelled. If the host task's current cancel scope has indeed been cancelled, the backend-specific cancellation exception will be raised. :raises RuntimeError: if the current thread was not spawned by :func:`.to_thread.run_sync` """ try: async_backend: AsyncBackend = threadlocals.current_async_backend except AttributeError: raise RuntimeError( "This function can only be run from an AnyIO worker thread" ) from None async_backend.check_cancelled() anyio-4.4.0/src/anyio/lowlevel.py000066400000000000000000000101111462472665400167610ustar00rootroot00000000000000from __future__ import annotations import enum from dataclasses import dataclass from typing import Any, Generic, Literal, TypeVar, overload from weakref import WeakKeyDictionary from ._core._eventloop import get_async_backend T = TypeVar("T") D = TypeVar("D") async def checkpoint() -> None: """ Check for cancellation and allow the scheduler to switch to another task. Equivalent to (but more efficient than):: await checkpoint_if_cancelled() await cancel_shielded_checkpoint() .. versionadded:: 3.0 """ await get_async_backend().checkpoint() async def checkpoint_if_cancelled() -> None: """ Enter a checkpoint if the enclosing cancel scope has been cancelled. This does not allow the scheduler to switch to a different task. .. versionadded:: 3.0 """ await get_async_backend().checkpoint_if_cancelled() async def cancel_shielded_checkpoint() -> None: """ Allow the scheduler to switch to another task but without checking for cancellation. Equivalent to (but potentially more efficient than):: with CancelScope(shield=True): await checkpoint() .. versionadded:: 3.0 """ await get_async_backend().cancel_shielded_checkpoint() def current_token() -> object: """ Return a backend specific token object that can be used to get back to the event loop. """ return get_async_backend().current_token() _run_vars: WeakKeyDictionary[Any, dict[str, Any]] = WeakKeyDictionary() _token_wrappers: dict[Any, _TokenWrapper] = {} @dataclass(frozen=True) class _TokenWrapper: __slots__ = "_token", "__weakref__" _token: object class _NoValueSet(enum.Enum): NO_VALUE_SET = enum.auto() class RunvarToken(Generic[T]): __slots__ = "_var", "_value", "_redeemed" def __init__(self, var: RunVar[T], value: T | Literal[_NoValueSet.NO_VALUE_SET]): self._var = var self._value: T | Literal[_NoValueSet.NO_VALUE_SET] = value self._redeemed = False class RunVar(Generic[T]): """ Like a :class:`~contextvars.ContextVar`, except scoped to the running event loop. """ __slots__ = "_name", "_default" NO_VALUE_SET: Literal[_NoValueSet.NO_VALUE_SET] = _NoValueSet.NO_VALUE_SET _token_wrappers: set[_TokenWrapper] = set() def __init__( self, name: str, default: T | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET ): self._name = name self._default = default @property def _current_vars(self) -> dict[str, T]: token = current_token() try: return _run_vars[token] except KeyError: run_vars = _run_vars[token] = {} return run_vars @overload def get(self, default: D) -> T | D: ... @overload def get(self) -> T: ... def get( self, default: D | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET ) -> T | D: try: return self._current_vars[self._name] except KeyError: if default is not RunVar.NO_VALUE_SET: return default elif self._default is not RunVar.NO_VALUE_SET: return self._default raise LookupError( f'Run variable "{self._name}" has no value and no default set' ) def set(self, value: T) -> RunvarToken[T]: current_vars = self._current_vars token = RunvarToken(self, current_vars.get(self._name, RunVar.NO_VALUE_SET)) current_vars[self._name] = value return token def reset(self, token: RunvarToken[T]) -> None: if token._var is not self: raise ValueError("This token does not belong to this RunVar") if token._redeemed: raise ValueError("This token has already been used") if token._value is _NoValueSet.NO_VALUE_SET: try: del self._current_vars[self._name] except KeyError: pass else: self._current_vars[self._name] = token._value token._redeemed = True def __repr__(self) -> str: return f"" anyio-4.4.0/src/anyio/py.typed000066400000000000000000000000001462472665400162510ustar00rootroot00000000000000anyio-4.4.0/src/anyio/pytest_plugin.py000066400000000000000000000124121462472665400200440ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Iterator from contextlib import ExitStack, contextmanager from inspect import isasyncgenfunction, iscoroutinefunction from typing import Any, Dict, Tuple, cast import pytest import sniffio from ._core._eventloop import get_all_backends, get_async_backend from .abc import TestRunner _current_runner: TestRunner | None = None _runner_stack: ExitStack | None = None _runner_leases = 0 def extract_backend_and_options(backend: object) -> tuple[str, dict[str, Any]]: if isinstance(backend, str): return backend, {} elif isinstance(backend, tuple) and len(backend) == 2: if isinstance(backend[0], str) and isinstance(backend[1], dict): return cast(Tuple[str, Dict[str, Any]], backend) raise TypeError("anyio_backend must be either a string or tuple of (string, dict)") @contextmanager def get_runner( backend_name: str, backend_options: dict[str, Any] ) -> Iterator[TestRunner]: global _current_runner, _runner_leases, _runner_stack if _current_runner is None: asynclib = get_async_backend(backend_name) _runner_stack = ExitStack() if sniffio.current_async_library_cvar.get(None) is None: # Since we're in control of the event loop, we can cache the name of the # async library token = sniffio.current_async_library_cvar.set(backend_name) _runner_stack.callback(sniffio.current_async_library_cvar.reset, token) backend_options = backend_options or {} _current_runner = _runner_stack.enter_context( asynclib.create_test_runner(backend_options) ) _runner_leases += 1 try: yield _current_runner finally: _runner_leases -= 1 if not _runner_leases: assert _runner_stack is not None _runner_stack.close() _runner_stack = _current_runner = None def pytest_configure(config: Any) -> None: config.addinivalue_line( "markers", "anyio: mark the (coroutine function) test to be run " "asynchronously via anyio.", ) def pytest_fixture_setup(fixturedef: Any, request: Any) -> None: def wrapper(*args, anyio_backend, **kwargs): # type: ignore[no-untyped-def] backend_name, backend_options = extract_backend_and_options(anyio_backend) if has_backend_arg: kwargs["anyio_backend"] = anyio_backend with get_runner(backend_name, backend_options) as runner: if isasyncgenfunction(func): yield from runner.run_asyncgen_fixture(func, kwargs) else: yield runner.run_fixture(func, kwargs) # Only apply this to coroutine functions and async generator functions in requests # that involve the anyio_backend fixture func = fixturedef.func if isasyncgenfunction(func) or iscoroutinefunction(func): if "anyio_backend" in request.fixturenames: has_backend_arg = "anyio_backend" in fixturedef.argnames fixturedef.func = wrapper if not has_backend_arg: fixturedef.argnames += ("anyio_backend",) @pytest.hookimpl(tryfirst=True) def pytest_pycollect_makeitem(collector: Any, name: Any, obj: Any) -> None: if collector.istestfunction(obj, name): inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj if iscoroutinefunction(inner_func): marker = collector.get_closest_marker("anyio") own_markers = getattr(obj, "pytestmark", ()) if marker or any(marker.name == "anyio" for marker in own_markers): pytest.mark.usefixtures("anyio_backend")(obj) @pytest.hookimpl(tryfirst=True) def pytest_pyfunc_call(pyfuncitem: Any) -> bool | None: def run_with_hypothesis(**kwargs: Any) -> None: with get_runner(backend_name, backend_options) as runner: runner.run_test(original_func, kwargs) backend = pyfuncitem.funcargs.get("anyio_backend") if backend: backend_name, backend_options = extract_backend_and_options(backend) if hasattr(pyfuncitem.obj, "hypothesis"): # Wrap the inner test function unless it's already wrapped original_func = pyfuncitem.obj.hypothesis.inner_test if original_func.__qualname__ != run_with_hypothesis.__qualname__: if iscoroutinefunction(original_func): pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis return None if iscoroutinefunction(pyfuncitem.obj): funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} with get_runner(backend_name, backend_options) as runner: runner.run_test(pyfuncitem.obj, testargs) return True return None @pytest.fixture(scope="module", params=get_all_backends()) def anyio_backend(request: Any) -> Any: return request.param @pytest.fixture def anyio_backend_name(anyio_backend: Any) -> str: if isinstance(anyio_backend, str): return anyio_backend else: return anyio_backend[0] @pytest.fixture def anyio_backend_options(anyio_backend: Any) -> dict[str, Any]: if isinstance(anyio_backend, str): return {} else: return anyio_backend[1] anyio-4.4.0/src/anyio/streams/000077500000000000000000000000001462472665400162425ustar00rootroot00000000000000anyio-4.4.0/src/anyio/streams/__init__.py000066400000000000000000000000001462472665400203410ustar00rootroot00000000000000anyio-4.4.0/src/anyio/streams/buffered.py000066400000000000000000000106241462472665400204010ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable, Mapping from dataclasses import dataclass, field from typing import Any from .. import ClosedResourceError, DelimiterNotFound, EndOfStream, IncompleteRead from ..abc import AnyByteReceiveStream, ByteReceiveStream @dataclass(eq=False) class BufferedByteReceiveStream(ByteReceiveStream): """ Wraps any bytes-based receive stream and uses a buffer to provide sophisticated receiving capabilities in the form of a byte stream. """ receive_stream: AnyByteReceiveStream _buffer: bytearray = field(init=False, default_factory=bytearray) _closed: bool = field(init=False, default=False) async def aclose(self) -> None: await self.receive_stream.aclose() self._closed = True @property def buffer(self) -> bytes: """The bytes currently in the buffer.""" return bytes(self._buffer) @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return self.receive_stream.extra_attributes async def receive(self, max_bytes: int = 65536) -> bytes: if self._closed: raise ClosedResourceError if self._buffer: chunk = bytes(self._buffer[:max_bytes]) del self._buffer[:max_bytes] return chunk elif isinstance(self.receive_stream, ByteReceiveStream): return await self.receive_stream.receive(max_bytes) else: # With a bytes-oriented object stream, we need to handle any surplus bytes # we get from the receive() call chunk = await self.receive_stream.receive() if len(chunk) > max_bytes: # Save the surplus bytes in the buffer self._buffer.extend(chunk[max_bytes:]) return chunk[:max_bytes] else: return chunk async def receive_exactly(self, nbytes: int) -> bytes: """ Read exactly the given amount of bytes from the stream. :param nbytes: the number of bytes to read :return: the bytes read :raises ~anyio.IncompleteRead: if the stream was closed before the requested amount of bytes could be read from the stream """ while True: remaining = nbytes - len(self._buffer) if remaining <= 0: retval = self._buffer[:nbytes] del self._buffer[:nbytes] return bytes(retval) try: if isinstance(self.receive_stream, ByteReceiveStream): chunk = await self.receive_stream.receive(remaining) else: chunk = await self.receive_stream.receive() except EndOfStream as exc: raise IncompleteRead from exc self._buffer.extend(chunk) async def receive_until(self, delimiter: bytes, max_bytes: int) -> bytes: """ Read from the stream until the delimiter is found or max_bytes have been read. :param delimiter: the marker to look for in the stream :param max_bytes: maximum number of bytes that will be read before raising :exc:`~anyio.DelimiterNotFound` :return: the bytes read (not including the delimiter) :raises ~anyio.IncompleteRead: if the stream was closed before the delimiter was found :raises ~anyio.DelimiterNotFound: if the delimiter is not found within the bytes read up to the maximum allowed """ delimiter_size = len(delimiter) offset = 0 while True: # Check if the delimiter can be found in the current buffer index = self._buffer.find(delimiter, offset) if index >= 0: found = self._buffer[:index] del self._buffer[: index + len(delimiter) :] return bytes(found) # Check if the buffer is already at or over the limit if len(self._buffer) >= max_bytes: raise DelimiterNotFound(max_bytes) # Read more data into the buffer from the socket try: data = await self.receive_stream.receive() except EndOfStream as exc: raise IncompleteRead from exc # Move the offset forward and add the new data to the buffer offset = max(len(self._buffer) - delimiter_size + 1, 0) self._buffer.extend(data) anyio-4.4.0/src/anyio/streams/file.py000066400000000000000000000104371462472665400175400ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable, Mapping from io import SEEK_SET, UnsupportedOperation from os import PathLike from pathlib import Path from typing import Any, BinaryIO, cast from .. import ( BrokenResourceError, ClosedResourceError, EndOfStream, TypedAttributeSet, to_thread, typed_attribute, ) from ..abc import ByteReceiveStream, ByteSendStream class FileStreamAttribute(TypedAttributeSet): #: the open file descriptor file: BinaryIO = typed_attribute() #: the path of the file on the file system, if available (file must be a real file) path: Path = typed_attribute() #: the file number, if available (file must be a real file or a TTY) fileno: int = typed_attribute() class _BaseFileStream: def __init__(self, file: BinaryIO): self._file = file async def aclose(self) -> None: await to_thread.run_sync(self._file.close) @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: attributes: dict[Any, Callable[[], Any]] = { FileStreamAttribute.file: lambda: self._file, } if hasattr(self._file, "name"): attributes[FileStreamAttribute.path] = lambda: Path(self._file.name) try: self._file.fileno() except UnsupportedOperation: pass else: attributes[FileStreamAttribute.fileno] = lambda: self._file.fileno() return attributes class FileReadStream(_BaseFileStream, ByteReceiveStream): """ A byte stream that reads from a file in the file system. :param file: a file that has been opened for reading in binary mode .. versionadded:: 3.0 """ @classmethod async def from_path(cls, path: str | PathLike[str]) -> FileReadStream: """ Create a file read stream by opening the given file. :param path: path of the file to read from """ file = await to_thread.run_sync(Path(path).open, "rb") return cls(cast(BinaryIO, file)) async def receive(self, max_bytes: int = 65536) -> bytes: try: data = await to_thread.run_sync(self._file.read, max_bytes) except ValueError: raise ClosedResourceError from None except OSError as exc: raise BrokenResourceError from exc if data: return data else: raise EndOfStream async def seek(self, position: int, whence: int = SEEK_SET) -> int: """ Seek the file to the given position. .. seealso:: :meth:`io.IOBase.seek` .. note:: Not all file descriptors are seekable. :param position: position to seek the file to :param whence: controls how ``position`` is interpreted :return: the new absolute position :raises OSError: if the file is not seekable """ return await to_thread.run_sync(self._file.seek, position, whence) async def tell(self) -> int: """ Return the current stream position. .. note:: Not all file descriptors are seekable. :return: the current absolute position :raises OSError: if the file is not seekable """ return await to_thread.run_sync(self._file.tell) class FileWriteStream(_BaseFileStream, ByteSendStream): """ A byte stream that writes to a file in the file system. :param file: a file that has been opened for writing in binary mode .. versionadded:: 3.0 """ @classmethod async def from_path( cls, path: str | PathLike[str], append: bool = False ) -> FileWriteStream: """ Create a file write stream by opening the given file for writing. :param path: path of the file to write to :param append: if ``True``, open the file for appending; if ``False``, any existing file at the given path will be truncated """ mode = "ab" if append else "wb" file = await to_thread.run_sync(Path(path).open, mode) return cls(cast(BinaryIO, file)) async def send(self, item: bytes) -> None: try: await to_thread.run_sync(self._file.write, item) except ValueError: raise ClosedResourceError from None except OSError as exc: raise BrokenResourceError from exc anyio-4.4.0/src/anyio/streams/memory.py000066400000000000000000000237161462472665400201350ustar00rootroot00000000000000from __future__ import annotations import warnings from collections import OrderedDict, deque from dataclasses import dataclass, field from types import TracebackType from typing import Generic, NamedTuple, TypeVar from .. import ( BrokenResourceError, ClosedResourceError, EndOfStream, WouldBlock, ) from .._core._testing import TaskInfo, get_current_task from ..abc import Event, ObjectReceiveStream, ObjectSendStream from ..lowlevel import checkpoint T_Item = TypeVar("T_Item") T_co = TypeVar("T_co", covariant=True) T_contra = TypeVar("T_contra", contravariant=True) class MemoryObjectStreamStatistics(NamedTuple): current_buffer_used: int #: number of items stored in the buffer #: maximum number of items that can be stored on this stream (or :data:`math.inf`) max_buffer_size: float open_send_streams: int #: number of unclosed clones of the send stream open_receive_streams: int #: number of unclosed clones of the receive stream #: number of tasks blocked on :meth:`MemoryObjectSendStream.send` tasks_waiting_send: int #: number of tasks blocked on :meth:`MemoryObjectReceiveStream.receive` tasks_waiting_receive: int @dataclass(eq=False) class MemoryObjectItemReceiver(Generic[T_Item]): task_info: TaskInfo = field(init=False, default_factory=get_current_task) item: T_Item = field(init=False) @dataclass(eq=False) class MemoryObjectStreamState(Generic[T_Item]): max_buffer_size: float = field() buffer: deque[T_Item] = field(init=False, default_factory=deque) open_send_channels: int = field(init=False, default=0) open_receive_channels: int = field(init=False, default=0) waiting_receivers: OrderedDict[Event, MemoryObjectItemReceiver[T_Item]] = field( init=False, default_factory=OrderedDict ) waiting_senders: OrderedDict[Event, T_Item] = field( init=False, default_factory=OrderedDict ) def statistics(self) -> MemoryObjectStreamStatistics: return MemoryObjectStreamStatistics( len(self.buffer), self.max_buffer_size, self.open_send_channels, self.open_receive_channels, len(self.waiting_senders), len(self.waiting_receivers), ) @dataclass(eq=False) class MemoryObjectReceiveStream(Generic[T_co], ObjectReceiveStream[T_co]): _state: MemoryObjectStreamState[T_co] _closed: bool = field(init=False, default=False) def __post_init__(self) -> None: self._state.open_receive_channels += 1 def receive_nowait(self) -> T_co: """ Receive the next item if it can be done without waiting. :return: the received item :raises ~anyio.ClosedResourceError: if this send stream has been closed :raises ~anyio.EndOfStream: if the buffer is empty and this stream has been closed from the sending end :raises ~anyio.WouldBlock: if there are no items in the buffer and no tasks waiting to send """ if self._closed: raise ClosedResourceError if self._state.waiting_senders: # Get the item from the next sender send_event, item = self._state.waiting_senders.popitem(last=False) self._state.buffer.append(item) send_event.set() if self._state.buffer: return self._state.buffer.popleft() elif not self._state.open_send_channels: raise EndOfStream raise WouldBlock async def receive(self) -> T_co: await checkpoint() try: return self.receive_nowait() except WouldBlock: # Add ourselves in the queue receive_event = Event() receiver = MemoryObjectItemReceiver[T_co]() self._state.waiting_receivers[receive_event] = receiver try: await receive_event.wait() finally: self._state.waiting_receivers.pop(receive_event, None) try: return receiver.item except AttributeError: raise EndOfStream def clone(self) -> MemoryObjectReceiveStream[T_co]: """ Create a clone of this receive stream. Each clone can be closed separately. Only when all clones have been closed will the receiving end of the memory stream be considered closed by the sending ends. :return: the cloned stream """ if self._closed: raise ClosedResourceError return MemoryObjectReceiveStream(_state=self._state) def close(self) -> None: """ Close the stream. This works the exact same way as :meth:`aclose`, but is provided as a special case for the benefit of synchronous callbacks. """ if not self._closed: self._closed = True self._state.open_receive_channels -= 1 if self._state.open_receive_channels == 0: send_events = list(self._state.waiting_senders.keys()) for event in send_events: event.set() async def aclose(self) -> None: self.close() def statistics(self) -> MemoryObjectStreamStatistics: """ Return statistics about the current state of this stream. .. versionadded:: 3.0 """ return self._state.statistics() def __enter__(self) -> MemoryObjectReceiveStream[T_co]: return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.close() def __del__(self) -> None: if not self._closed: warnings.warn( f"Unclosed <{self.__class__.__name__}>", ResourceWarning, source=self, ) @dataclass(eq=False) class MemoryObjectSendStream(Generic[T_contra], ObjectSendStream[T_contra]): _state: MemoryObjectStreamState[T_contra] _closed: bool = field(init=False, default=False) def __post_init__(self) -> None: self._state.open_send_channels += 1 def send_nowait(self, item: T_contra) -> None: """ Send an item immediately if it can be done without waiting. :param item: the item to send :raises ~anyio.ClosedResourceError: if this send stream has been closed :raises ~anyio.BrokenResourceError: if the stream has been closed from the receiving end :raises ~anyio.WouldBlock: if the buffer is full and there are no tasks waiting to receive """ if self._closed: raise ClosedResourceError if not self._state.open_receive_channels: raise BrokenResourceError while self._state.waiting_receivers: receive_event, receiver = self._state.waiting_receivers.popitem(last=False) if not receiver.task_info.has_pending_cancellation(): receiver.item = item receive_event.set() return if len(self._state.buffer) < self._state.max_buffer_size: self._state.buffer.append(item) else: raise WouldBlock async def send(self, item: T_contra) -> None: """ Send an item to the stream. If the buffer is full, this method blocks until there is again room in the buffer or the item can be sent directly to a receiver. :param item: the item to send :raises ~anyio.ClosedResourceError: if this send stream has been closed :raises ~anyio.BrokenResourceError: if the stream has been closed from the receiving end """ await checkpoint() try: self.send_nowait(item) except WouldBlock: # Wait until there's someone on the receiving end send_event = Event() self._state.waiting_senders[send_event] = item try: await send_event.wait() except BaseException: self._state.waiting_senders.pop(send_event, None) raise if send_event in self._state.waiting_senders: del self._state.waiting_senders[send_event] raise BrokenResourceError from None def clone(self) -> MemoryObjectSendStream[T_contra]: """ Create a clone of this send stream. Each clone can be closed separately. Only when all clones have been closed will the sending end of the memory stream be considered closed by the receiving ends. :return: the cloned stream """ if self._closed: raise ClosedResourceError return MemoryObjectSendStream(_state=self._state) def close(self) -> None: """ Close the stream. This works the exact same way as :meth:`aclose`, but is provided as a special case for the benefit of synchronous callbacks. """ if not self._closed: self._closed = True self._state.open_send_channels -= 1 if self._state.open_send_channels == 0: receive_events = list(self._state.waiting_receivers.keys()) self._state.waiting_receivers.clear() for event in receive_events: event.set() async def aclose(self) -> None: self.close() def statistics(self) -> MemoryObjectStreamStatistics: """ Return statistics about the current state of this stream. .. versionadded:: 3.0 """ return self._state.statistics() def __enter__(self) -> MemoryObjectSendStream[T_contra]: return self def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: self.close() def __del__(self) -> None: if not self._closed: warnings.warn( f"Unclosed <{self.__class__.__name__}>", ResourceWarning, source=self, ) anyio-4.4.0/src/anyio/streams/stapled.py000066400000000000000000000103161462472665400202510ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass from typing import Any, Generic, TypeVar from ..abc import ( ByteReceiveStream, ByteSendStream, ByteStream, Listener, ObjectReceiveStream, ObjectSendStream, ObjectStream, TaskGroup, ) T_Item = TypeVar("T_Item") T_Stream = TypeVar("T_Stream") @dataclass(eq=False) class StapledByteStream(ByteStream): """ Combines two byte streams into a single, bidirectional byte stream. Extra attributes will be provided from both streams, with the receive stream providing the values in case of a conflict. :param ByteSendStream send_stream: the sending byte stream :param ByteReceiveStream receive_stream: the receiving byte stream """ send_stream: ByteSendStream receive_stream: ByteReceiveStream async def receive(self, max_bytes: int = 65536) -> bytes: return await self.receive_stream.receive(max_bytes) async def send(self, item: bytes) -> None: await self.send_stream.send(item) async def send_eof(self) -> None: await self.send_stream.aclose() async def aclose(self) -> None: await self.send_stream.aclose() await self.receive_stream.aclose() @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return { **self.send_stream.extra_attributes, **self.receive_stream.extra_attributes, } @dataclass(eq=False) class StapledObjectStream(Generic[T_Item], ObjectStream[T_Item]): """ Combines two object streams into a single, bidirectional object stream. Extra attributes will be provided from both streams, with the receive stream providing the values in case of a conflict. :param ObjectSendStream send_stream: the sending object stream :param ObjectReceiveStream receive_stream: the receiving object stream """ send_stream: ObjectSendStream[T_Item] receive_stream: ObjectReceiveStream[T_Item] async def receive(self) -> T_Item: return await self.receive_stream.receive() async def send(self, item: T_Item) -> None: await self.send_stream.send(item) async def send_eof(self) -> None: await self.send_stream.aclose() async def aclose(self) -> None: await self.send_stream.aclose() await self.receive_stream.aclose() @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return { **self.send_stream.extra_attributes, **self.receive_stream.extra_attributes, } @dataclass(eq=False) class MultiListener(Generic[T_Stream], Listener[T_Stream]): """ Combines multiple listeners into one, serving connections from all of them at once. Any MultiListeners in the given collection of listeners will have their listeners moved into this one. Extra attributes are provided from each listener, with each successive listener overriding any conflicting attributes from the previous one. :param listeners: listeners to serve :type listeners: Sequence[Listener[T_Stream]] """ listeners: Sequence[Listener[T_Stream]] def __post_init__(self) -> None: listeners: list[Listener[T_Stream]] = [] for listener in self.listeners: if isinstance(listener, MultiListener): listeners.extend(listener.listeners) del listener.listeners[:] # type: ignore[attr-defined] else: listeners.append(listener) self.listeners = listeners async def serve( self, handler: Callable[[T_Stream], Any], task_group: TaskGroup | None = None ) -> None: from .. import create_task_group async with create_task_group() as tg: for listener in self.listeners: tg.start_soon(listener.serve, handler, task_group) async def aclose(self) -> None: for listener in self.listeners: await listener.aclose() @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: attributes: dict = {} for listener in self.listeners: attributes.update(listener.extra_attributes) return attributes anyio-4.4.0/src/anyio/streams/text.py000066400000000000000000000117461462472665400176110ustar00rootroot00000000000000from __future__ import annotations import codecs from collections.abc import Callable, Mapping from dataclasses import InitVar, dataclass, field from typing import Any from ..abc import ( AnyByteReceiveStream, AnyByteSendStream, AnyByteStream, ObjectReceiveStream, ObjectSendStream, ObjectStream, ) @dataclass(eq=False) class TextReceiveStream(ObjectReceiveStream[str]): """ Stream wrapper that decodes bytes to strings using the given encoding. Decoding is done using :class:`~codecs.IncrementalDecoder` which returns any completely received unicode characters as soon as they come in. :param transport_stream: any bytes-based receive stream :param encoding: character encoding to use for decoding bytes to strings (defaults to ``utf-8``) :param errors: handling scheme for decoding errors (defaults to ``strict``; see the `codecs module documentation`_ for a comprehensive list of options) .. _codecs module documentation: https://docs.python.org/3/library/codecs.html#codec-objects """ transport_stream: AnyByteReceiveStream encoding: InitVar[str] = "utf-8" errors: InitVar[str] = "strict" _decoder: codecs.IncrementalDecoder = field(init=False) def __post_init__(self, encoding: str, errors: str) -> None: decoder_class = codecs.getincrementaldecoder(encoding) self._decoder = decoder_class(errors=errors) async def receive(self) -> str: while True: chunk = await self.transport_stream.receive() decoded = self._decoder.decode(chunk) if decoded: return decoded async def aclose(self) -> None: await self.transport_stream.aclose() self._decoder.reset() @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return self.transport_stream.extra_attributes @dataclass(eq=False) class TextSendStream(ObjectSendStream[str]): """ Sends strings to the wrapped stream as bytes using the given encoding. :param AnyByteSendStream transport_stream: any bytes-based send stream :param str encoding: character encoding to use for encoding strings to bytes (defaults to ``utf-8``) :param str errors: handling scheme for encoding errors (defaults to ``strict``; see the `codecs module documentation`_ for a comprehensive list of options) .. _codecs module documentation: https://docs.python.org/3/library/codecs.html#codec-objects """ transport_stream: AnyByteSendStream encoding: InitVar[str] = "utf-8" errors: str = "strict" _encoder: Callable[..., tuple[bytes, int]] = field(init=False) def __post_init__(self, encoding: str) -> None: self._encoder = codecs.getencoder(encoding) async def send(self, item: str) -> None: encoded = self._encoder(item, self.errors)[0] await self.transport_stream.send(encoded) async def aclose(self) -> None: await self.transport_stream.aclose() @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return self.transport_stream.extra_attributes @dataclass(eq=False) class TextStream(ObjectStream[str]): """ A bidirectional stream that decodes bytes to strings on receive and encodes strings to bytes on send. Extra attributes will be provided from both streams, with the receive stream providing the values in case of a conflict. :param AnyByteStream transport_stream: any bytes-based stream :param str encoding: character encoding to use for encoding/decoding strings to/from bytes (defaults to ``utf-8``) :param str errors: handling scheme for encoding errors (defaults to ``strict``; see the `codecs module documentation`_ for a comprehensive list of options) .. _codecs module documentation: https://docs.python.org/3/library/codecs.html#codec-objects """ transport_stream: AnyByteStream encoding: InitVar[str] = "utf-8" errors: InitVar[str] = "strict" _receive_stream: TextReceiveStream = field(init=False) _send_stream: TextSendStream = field(init=False) def __post_init__(self, encoding: str, errors: str) -> None: self._receive_stream = TextReceiveStream( self.transport_stream, encoding=encoding, errors=errors ) self._send_stream = TextSendStream( self.transport_stream, encoding=encoding, errors=errors ) async def receive(self) -> str: return await self._receive_stream.receive() async def send(self, item: str) -> None: await self._send_stream.send(item) async def send_eof(self) -> None: await self.transport_stream.send_eof() async def aclose(self) -> None: await self._send_stream.aclose() await self._receive_stream.aclose() @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return { **self._send_stream.extra_attributes, **self._receive_stream.extra_attributes, } anyio-4.4.0/src/anyio/streams/tls.py000066400000000000000000000307201462472665400174200ustar00rootroot00000000000000from __future__ import annotations import logging import re import ssl import sys from collections.abc import Callable, Mapping from dataclasses import dataclass from functools import wraps from typing import Any, Tuple, TypeVar from .. import ( BrokenResourceError, EndOfStream, aclose_forcefully, get_cancelled_exc_class, ) from .._core._typedattr import TypedAttributeSet, typed_attribute from ..abc import AnyByteStream, ByteStream, Listener, TaskGroup if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: from typing_extensions import TypeVarTuple, Unpack T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") _PCTRTT = Tuple[Tuple[str, str], ...] _PCTRTTT = Tuple[_PCTRTT, ...] class TLSAttribute(TypedAttributeSet): """Contains Transport Layer Security related attributes.""" #: the selected ALPN protocol alpn_protocol: str | None = typed_attribute() #: the channel binding for type ``tls-unique`` channel_binding_tls_unique: bytes = typed_attribute() #: the selected cipher cipher: tuple[str, str, int] = typed_attribute() #: the peer certificate in dictionary form (see :meth:`ssl.SSLSocket.getpeercert` # for more information) peer_certificate: None | (dict[str, str | _PCTRTTT | _PCTRTT]) = typed_attribute() #: the peer certificate in binary form peer_certificate_binary: bytes | None = typed_attribute() #: ``True`` if this is the server side of the connection server_side: bool = typed_attribute() #: ciphers shared by the client during the TLS handshake (``None`` if this is the #: client side) shared_ciphers: list[tuple[str, str, int]] | None = typed_attribute() #: the :class:`~ssl.SSLObject` used for encryption ssl_object: ssl.SSLObject = typed_attribute() #: ``True`` if this stream does (and expects) a closing TLS handshake when the #: stream is being closed standard_compatible: bool = typed_attribute() #: the TLS protocol version (e.g. ``TLSv1.2``) tls_version: str = typed_attribute() @dataclass(eq=False) class TLSStream(ByteStream): """ A stream wrapper that encrypts all sent data and decrypts received data. This class has no public initializer; use :meth:`wrap` instead. All extra attributes from :class:`~TLSAttribute` are supported. :var AnyByteStream transport_stream: the wrapped stream """ transport_stream: AnyByteStream standard_compatible: bool _ssl_object: ssl.SSLObject _read_bio: ssl.MemoryBIO _write_bio: ssl.MemoryBIO @classmethod async def wrap( cls, transport_stream: AnyByteStream, *, server_side: bool | None = None, hostname: str | None = None, ssl_context: ssl.SSLContext | None = None, standard_compatible: bool = True, ) -> TLSStream: """ Wrap an existing stream with Transport Layer Security. This performs a TLS handshake with the peer. :param transport_stream: a bytes-transporting stream to wrap :param server_side: ``True`` if this is the server side of the connection, ``False`` if this is the client side (if omitted, will be set to ``False`` if ``hostname`` has been provided, ``False`` otherwise). Used only to create a default context when an explicit context has not been provided. :param hostname: host name of the peer (if host name checking is desired) :param ssl_context: the SSLContext object to use (if not provided, a secure default will be created) :param standard_compatible: if ``False``, skip the closing handshake when closing the connection, and don't raise an exception if the peer does the same :raises ~ssl.SSLError: if the TLS handshake fails """ if server_side is None: server_side = not hostname if not ssl_context: purpose = ( ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH ) ssl_context = ssl.create_default_context(purpose) # Re-enable detection of unexpected EOFs if it was disabled by Python if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF bio_in = ssl.MemoryBIO() bio_out = ssl.MemoryBIO() ssl_object = ssl_context.wrap_bio( bio_in, bio_out, server_side=server_side, server_hostname=hostname ) wrapper = cls( transport_stream=transport_stream, standard_compatible=standard_compatible, _ssl_object=ssl_object, _read_bio=bio_in, _write_bio=bio_out, ) await wrapper._call_sslobject_method(ssl_object.do_handshake) return wrapper async def _call_sslobject_method( self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] ) -> T_Retval: while True: try: result = func(*args) except ssl.SSLWantReadError: try: # Flush any pending writes first if self._write_bio.pending: await self.transport_stream.send(self._write_bio.read()) data = await self.transport_stream.receive() except EndOfStream: self._read_bio.write_eof() except OSError as exc: self._read_bio.write_eof() self._write_bio.write_eof() raise BrokenResourceError from exc else: self._read_bio.write(data) except ssl.SSLWantWriteError: await self.transport_stream.send(self._write_bio.read()) except ssl.SSLSyscallError as exc: self._read_bio.write_eof() self._write_bio.write_eof() raise BrokenResourceError from exc except ssl.SSLError as exc: self._read_bio.write_eof() self._write_bio.write_eof() if ( isinstance(exc, ssl.SSLEOFError) or "UNEXPECTED_EOF_WHILE_READING" in exc.strerror ): if self.standard_compatible: raise BrokenResourceError from exc else: raise EndOfStream from None raise else: # Flush any pending writes first if self._write_bio.pending: await self.transport_stream.send(self._write_bio.read()) return result async def unwrap(self) -> tuple[AnyByteStream, bytes]: """ Does the TLS closing handshake. :return: a tuple of (wrapped byte stream, bytes left in the read buffer) """ await self._call_sslobject_method(self._ssl_object.unwrap) self._read_bio.write_eof() self._write_bio.write_eof() return self.transport_stream, self._read_bio.read() async def aclose(self) -> None: if self.standard_compatible: try: await self.unwrap() except BaseException: await aclose_forcefully(self.transport_stream) raise await self.transport_stream.aclose() async def receive(self, max_bytes: int = 65536) -> bytes: data = await self._call_sslobject_method(self._ssl_object.read, max_bytes) if not data: raise EndOfStream return data async def send(self, item: bytes) -> None: await self._call_sslobject_method(self._ssl_object.write, item) async def send_eof(self) -> None: tls_version = self.extra(TLSAttribute.tls_version) match = re.match(r"TLSv(\d+)(?:\.(\d+))?", tls_version) if match: major, minor = int(match.group(1)), int(match.group(2) or 0) if (major, minor) < (1, 3): raise NotImplementedError( f"send_eof() requires at least TLSv1.3; current " f"session uses {tls_version}" ) raise NotImplementedError( "send_eof() has not yet been implemented for TLS streams" ) @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return { **self.transport_stream.extra_attributes, TLSAttribute.alpn_protocol: self._ssl_object.selected_alpn_protocol, TLSAttribute.channel_binding_tls_unique: ( self._ssl_object.get_channel_binding ), TLSAttribute.cipher: self._ssl_object.cipher, TLSAttribute.peer_certificate: lambda: self._ssl_object.getpeercert(False), TLSAttribute.peer_certificate_binary: lambda: self._ssl_object.getpeercert( True ), TLSAttribute.server_side: lambda: self._ssl_object.server_side, TLSAttribute.shared_ciphers: lambda: self._ssl_object.shared_ciphers() if self._ssl_object.server_side else None, TLSAttribute.standard_compatible: lambda: self.standard_compatible, TLSAttribute.ssl_object: lambda: self._ssl_object, TLSAttribute.tls_version: self._ssl_object.version, } @dataclass(eq=False) class TLSListener(Listener[TLSStream]): """ A convenience listener that wraps another listener and auto-negotiates a TLS session on every accepted connection. If the TLS handshake times out or raises an exception, :meth:`handle_handshake_error` is called to do whatever post-mortem processing is deemed necessary. Supports only the :attr:`~TLSAttribute.standard_compatible` extra attribute. :param Listener listener: the listener to wrap :param ssl_context: the SSL context object :param standard_compatible: a flag passed through to :meth:`TLSStream.wrap` :param handshake_timeout: time limit for the TLS handshake (passed to :func:`~anyio.fail_after`) """ listener: Listener[Any] ssl_context: ssl.SSLContext standard_compatible: bool = True handshake_timeout: float = 30 @staticmethod async def handle_handshake_error(exc: BaseException, stream: AnyByteStream) -> None: """ Handle an exception raised during the TLS handshake. This method does 3 things: #. Forcefully closes the original stream #. Logs the exception (unless it was a cancellation exception) using the ``anyio.streams.tls`` logger #. Reraises the exception if it was a base exception or a cancellation exception :param exc: the exception :param stream: the original stream """ await aclose_forcefully(stream) # Log all except cancellation exceptions if not isinstance(exc, get_cancelled_exc_class()): # CPython (as of 3.11.5) returns incorrect `sys.exc_info()` here when using # any asyncio implementation, so we explicitly pass the exception to log # (https://github.com/python/cpython/issues/108668). Trio does not have this # issue because it works around the CPython bug. logging.getLogger(__name__).exception( "Error during TLS handshake", exc_info=exc ) # Only reraise base exceptions and cancellation exceptions if not isinstance(exc, Exception) or isinstance(exc, get_cancelled_exc_class()): raise async def serve( self, handler: Callable[[TLSStream], Any], task_group: TaskGroup | None = None, ) -> None: @wraps(handler) async def handler_wrapper(stream: AnyByteStream) -> None: from .. import fail_after try: with fail_after(self.handshake_timeout): wrapped_stream = await TLSStream.wrap( stream, ssl_context=self.ssl_context, standard_compatible=self.standard_compatible, ) except BaseException as exc: await self.handle_handshake_error(exc, stream) else: await handler(wrapped_stream) await self.listener.serve(handler_wrapper, task_group) async def aclose(self) -> None: await self.listener.aclose() @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return { TLSAttribute.standard_compatible: lambda: self.standard_compatible, } anyio-4.4.0/src/anyio/to_process.py000066400000000000000000000224771462472665400173320ustar00rootroot00000000000000from __future__ import annotations import os import pickle import subprocess import sys from collections import deque from collections.abc import Callable from importlib.util import module_from_spec, spec_from_file_location from typing import TypeVar, cast from ._core._eventloop import current_time, get_async_backend, get_cancelled_exc_class from ._core._exceptions import BrokenWorkerProcess from ._core._subprocesses import open_process from ._core._synchronization import CapacityLimiter from ._core._tasks import CancelScope, fail_after from .abc import ByteReceiveStream, ByteSendStream, Process from .lowlevel import RunVar, checkpoint_if_cancelled from .streams.buffered import BufferedByteReceiveStream if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: from typing_extensions import TypeVarTuple, Unpack WORKER_MAX_IDLE_TIME = 300 # 5 minutes T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") _process_pool_workers: RunVar[set[Process]] = RunVar("_process_pool_workers") _process_pool_idle_workers: RunVar[deque[tuple[Process, float]]] = RunVar( "_process_pool_idle_workers" ) _default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter") async def run_sync( func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT], cancellable: bool = False, limiter: CapacityLimiter | None = None, ) -> T_Retval: """ Call the given function with the given arguments in a worker process. If the ``cancellable`` option is enabled and the task waiting for its completion is cancelled, the worker process running it will be abruptly terminated using SIGKILL (or ``terminateProcess()`` on Windows). :param func: a callable :param args: positional arguments for the callable :param cancellable: ``True`` to allow cancellation of the operation while it's running :param limiter: capacity limiter to use to limit the total amount of processes running (if omitted, the default limiter is used) :return: an awaitable that yields the return value of the function. """ async def send_raw_command(pickled_cmd: bytes) -> object: try: await stdin.send(pickled_cmd) response = await buffered.receive_until(b"\n", 50) status, length = response.split(b" ") if status not in (b"RETURN", b"EXCEPTION"): raise RuntimeError( f"Worker process returned unexpected response: {response!r}" ) pickled_response = await buffered.receive_exactly(int(length)) except BaseException as exc: workers.discard(process) try: process.kill() with CancelScope(shield=True): await process.aclose() except ProcessLookupError: pass if isinstance(exc, get_cancelled_exc_class()): raise else: raise BrokenWorkerProcess from exc retval = pickle.loads(pickled_response) if status == b"EXCEPTION": assert isinstance(retval, BaseException) raise retval else: return retval # First pickle the request before trying to reserve a worker process await checkpoint_if_cancelled() request = pickle.dumps(("run", func, args), protocol=pickle.HIGHEST_PROTOCOL) # If this is the first run in this event loop thread, set up the necessary variables try: workers = _process_pool_workers.get() idle_workers = _process_pool_idle_workers.get() except LookupError: workers = set() idle_workers = deque() _process_pool_workers.set(workers) _process_pool_idle_workers.set(idle_workers) get_async_backend().setup_process_pool_exit_at_shutdown(workers) async with limiter or current_default_process_limiter(): # Pop processes from the pool (starting from the most recently used) until we # find one that hasn't exited yet process: Process while idle_workers: process, idle_since = idle_workers.pop() if process.returncode is None: stdin = cast(ByteSendStream, process.stdin) buffered = BufferedByteReceiveStream( cast(ByteReceiveStream, process.stdout) ) # Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME # seconds or longer now = current_time() killed_processes: list[Process] = [] while idle_workers: if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME: break process_to_kill, idle_since = idle_workers.popleft() process_to_kill.kill() workers.remove(process_to_kill) killed_processes.append(process_to_kill) with CancelScope(shield=True): for killed_process in killed_processes: await killed_process.aclose() break workers.remove(process) else: command = [sys.executable, "-u", "-m", __name__] process = await open_process( command, stdin=subprocess.PIPE, stdout=subprocess.PIPE ) try: stdin = cast(ByteSendStream, process.stdin) buffered = BufferedByteReceiveStream( cast(ByteReceiveStream, process.stdout) ) with fail_after(20): message = await buffered.receive(6) if message != b"READY\n": raise BrokenWorkerProcess( f"Worker process returned unexpected response: {message!r}" ) main_module_path = getattr(sys.modules["__main__"], "__file__", None) pickled = pickle.dumps( ("init", sys.path, main_module_path), protocol=pickle.HIGHEST_PROTOCOL, ) await send_raw_command(pickled) except (BrokenWorkerProcess, get_cancelled_exc_class()): raise except BaseException as exc: process.kill() raise BrokenWorkerProcess( "Error during worker process initialization" ) from exc workers.add(process) with CancelScope(shield=not cancellable): try: return cast(T_Retval, await send_raw_command(request)) finally: if process in workers: idle_workers.append((process, current_time())) def current_default_process_limiter() -> CapacityLimiter: """ Return the capacity limiter that is used by default to limit the number of worker processes. :return: a capacity limiter object """ try: return _default_process_limiter.get() except LookupError: limiter = CapacityLimiter(os.cpu_count() or 2) _default_process_limiter.set(limiter) return limiter def process_worker() -> None: # Redirect standard streams to os.devnull so that user code won't interfere with the # parent-worker communication stdin = sys.stdin stdout = sys.stdout sys.stdin = open(os.devnull) sys.stdout = open(os.devnull, "w") stdout.buffer.write(b"READY\n") while True: retval = exception = None try: command, *args = pickle.load(stdin.buffer) except EOFError: return except BaseException as exc: exception = exc else: if command == "run": func, args = args try: retval = func(*args) except BaseException as exc: exception = exc elif command == "init": main_module_path: str | None sys.path, main_module_path = args del sys.modules["__main__"] if main_module_path: # Load the parent's main module but as __mp_main__ instead of # __main__ (like multiprocessing does) to avoid infinite recursion try: spec = spec_from_file_location("__mp_main__", main_module_path) if spec and spec.loader: main = module_from_spec(spec) spec.loader.exec_module(main) sys.modules["__main__"] = main except BaseException as exc: exception = exc try: if exception is not None: status = b"EXCEPTION" pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL) else: status = b"RETURN" pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL) except BaseException as exc: exception = exc status = b"EXCEPTION" pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL) stdout.buffer.write(b"%s %d\n" % (status, len(pickled))) stdout.buffer.write(pickled) # Respect SIGTERM if isinstance(exception, SystemExit): raise exception if __name__ == "__main__": process_worker() anyio-4.4.0/src/anyio/to_thread.py000066400000000000000000000045341462472665400171150ustar00rootroot00000000000000from __future__ import annotations import sys from collections.abc import Callable from typing import TypeVar from warnings import warn from ._core._eventloop import get_async_backend from .abc import CapacityLimiter if sys.version_info >= (3, 11): from typing import TypeVarTuple, Unpack else: from typing_extensions import TypeVarTuple, Unpack T_Retval = TypeVar("T_Retval") PosArgsT = TypeVarTuple("PosArgsT") async def run_sync( func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT], abandon_on_cancel: bool = False, cancellable: bool | None = None, limiter: CapacityLimiter | None = None, ) -> T_Retval: """ Call the given function with the given arguments in a worker thread. If the ``cancellable`` option is enabled and the task waiting for its completion is cancelled, the thread will still run its course but its return value (or any raised exception) will be ignored. :param func: a callable :param args: positional arguments for the callable :param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run unchecked on own) if the host task is cancelled, ``False`` to ignore cancellations in the host task until the operation has completed in the worker thread :param cancellable: deprecated alias of ``abandon_on_cancel``; will override ``abandon_on_cancel`` if both parameters are passed :param limiter: capacity limiter to use to limit the total amount of threads running (if omitted, the default limiter is used) :return: an awaitable that yields the return value of the function. """ if cancellable is not None: abandon_on_cancel = cancellable warn( "The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is " "deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead", DeprecationWarning, stacklevel=2, ) return await get_async_backend().run_sync_in_worker_thread( func, args, abandon_on_cancel=abandon_on_cancel, limiter=limiter ) def current_default_thread_limiter() -> CapacityLimiter: """ Return the capacity limiter that is used by default to limit the number of concurrent threads. :return: a capacity limiter object """ return get_async_backend().current_default_thread_limiter() anyio-4.4.0/tests/000077500000000000000000000000001462472665400140205ustar00rootroot00000000000000anyio-4.4.0/tests/__init__.py000066400000000000000000000000001462472665400161170ustar00rootroot00000000000000anyio-4.4.0/tests/conftest.py000066400000000000000000000042701462472665400162220ustar00rootroot00000000000000from __future__ import annotations import asyncio import ssl from collections.abc import Generator from ssl import SSLContext from typing import Any from unittest.mock import Mock import pytest import trustme from _pytest.fixtures import SubRequest from trustme import CA uvloop_marks = [] try: import uvloop except ImportError: uvloop_marks.append(pytest.mark.skip(reason="uvloop not available")) uvloop = Mock() else: if hasattr(asyncio.AbstractEventLoop, "shutdown_default_executor") and not hasattr( uvloop.loop.Loop, "shutdown_default_executor" ): uvloop_marks.append( pytest.mark.skip(reason="uvloop is missing shutdown_default_executor()") ) pytest_plugins = ["pytester", "pytest_mock"] @pytest.fixture( params=[ pytest.param( ("asyncio", {"debug": True, "loop_factory": None}), id="asyncio", ), pytest.param( ("asyncio", {"debug": True, "loop_factory": uvloop.new_event_loop}), marks=uvloop_marks, id="asyncio+uvloop", ), pytest.param("trio"), ] ) def anyio_backend(request: SubRequest) -> tuple[str, dict[str, Any]]: return request.param @pytest.fixture(scope="session") def ca() -> CA: return trustme.CA() @pytest.fixture(scope="session") def server_context(ca: CA) -> SSLContext: server_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): server_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF ca.issue_cert("localhost").configure_cert(server_context) return server_context @pytest.fixture(scope="session") def client_context(ca: CA) -> SSLContext: client_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): client_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF ca.configure_trust(client_context) return client_context @pytest.fixture def asyncio_event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: loop = asyncio.DefaultEventLoopPolicy().new_event_loop() asyncio.set_event_loop(loop) yield loop asyncio.set_event_loop(None) loop.close() anyio-4.4.0/tests/streams/000077500000000000000000000000001462472665400154765ustar00rootroot00000000000000anyio-4.4.0/tests/streams/__init__.py000066400000000000000000000000001462472665400175750ustar00rootroot00000000000000anyio-4.4.0/tests/streams/test_buffered.py000066400000000000000000000037711462472665400207010ustar00rootroot00000000000000from __future__ import annotations import pytest from anyio import IncompleteRead, create_memory_object_stream from anyio.streams.buffered import BufferedByteReceiveStream pytestmark = pytest.mark.anyio async def test_receive_exactly() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](2) buffered_stream = BufferedByteReceiveStream(receive_stream) await send_stream.send(b"abcd") await send_stream.send(b"efgh") result = await buffered_stream.receive_exactly(8) assert result == b"abcdefgh" assert isinstance(result, bytes) send_stream.close() receive_stream.close() async def test_receive_exactly_incomplete() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](1) buffered_stream = BufferedByteReceiveStream(receive_stream) await send_stream.send(b"abcd") await send_stream.aclose() with pytest.raises(IncompleteRead): await buffered_stream.receive_exactly(8) send_stream.close() receive_stream.close() async def test_receive_until() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](2) buffered_stream = BufferedByteReceiveStream(receive_stream) await send_stream.send(b"abcd") await send_stream.send(b"efgh") result = await buffered_stream.receive_until(b"de", 10) assert result == b"abc" assert isinstance(result, bytes) result = await buffered_stream.receive_until(b"h", 10) assert result == b"fg" assert isinstance(result, bytes) send_stream.close() receive_stream.close() async def test_receive_until_incomplete() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](1) buffered_stream = BufferedByteReceiveStream(receive_stream) await send_stream.send(b"abcd") await send_stream.aclose() with pytest.raises(IncompleteRead): assert await buffered_stream.receive_until(b"de", 10) assert buffered_stream.buffer == b"abcd" send_stream.close() receive_stream.close() anyio-4.4.0/tests/streams/test_file.py000066400000000000000000000075251462472665400200370ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path import pytest from _pytest.fixtures import SubRequest from _pytest.tmpdir import TempPathFactory from anyio import ClosedResourceError, EndOfStream from anyio.abc import ByteReceiveStream from anyio.streams.file import FileReadStream, FileStreamAttribute, FileWriteStream pytestmark = pytest.mark.anyio class TestFileReadStream: @pytest.fixture(scope="class") def file_path(self, tmp_path_factory: TempPathFactory) -> Path: path = tmp_path_factory.mktemp("filestream") / "data.txt" path.write_text("Hello") return path @pytest.fixture(params=[False, True], ids=["str", "path"]) def file_path_or_str(self, request: SubRequest, file_path: Path) -> Path | str: return file_path if request.param else str(file_path) async def _run_filestream_test(self, stream: ByteReceiveStream) -> None: assert await stream.receive(3) == b"Hel" assert await stream.receive(3) == b"lo" with pytest.raises(EndOfStream): await stream.receive(1) async def test_read_file_as_path(self, file_path_or_str: Path | str) -> None: async with await FileReadStream.from_path(file_path_or_str) as stream: await self._run_filestream_test(stream) async def test_read_file(self, file_path: Path) -> None: with file_path.open("rb") as file: async with FileReadStream(file) as stream: await self._run_filestream_test(stream) async def test_read_after_close(self, file_path: Path) -> None: async with await FileReadStream.from_path(file_path) as stream: pass with pytest.raises(ClosedResourceError): await stream.receive() async def test_seek(self, file_path: Path) -> None: with file_path.open("rb") as file: async with FileReadStream(file) as stream: await stream.seek(2) assert await stream.tell() == 2 data = await stream.receive() assert data == b"llo" assert await stream.tell() == 5 async def test_extra_attributes(self, file_path: Path) -> None: async with await FileReadStream.from_path(file_path) as stream: path = stream.extra(FileStreamAttribute.path) assert path == file_path fileno = stream.extra(FileStreamAttribute.fileno) assert fileno > 2 file = stream.extra(FileStreamAttribute.file) assert file.fileno() == fileno class TestFileWriteStream: @pytest.fixture def file_path(self, tmp_path: Path) -> Path: return tmp_path / "written_data.txt" async def test_write_file(self, file_path: Path) -> None: async with await FileWriteStream.from_path(file_path) as stream: await stream.send(b"Hel") await stream.send(b"lo") assert file_path.read_text() == "Hello" async def test_append_file(self, file_path: Path) -> None: file_path.write_text("Hello") async with await FileWriteStream.from_path(file_path, True) as stream: await stream.send(b", World!") assert file_path.read_text() == "Hello, World!" async def test_write_after_close(self, file_path: Path) -> None: async with await FileWriteStream.from_path(file_path, True) as stream: pass with pytest.raises(ClosedResourceError): await stream.send(b"foo") async def test_extra_attributes(self, file_path: Path) -> None: async with await FileWriteStream.from_path(file_path) as stream: path = stream.extra(FileStreamAttribute.path) assert path == file_path fileno = stream.extra(FileStreamAttribute.fileno) assert fileno > 2 file = stream.extra(FileStreamAttribute.file) assert file.fileno() == fileno anyio-4.4.0/tests/streams/test_memory.py000066400000000000000000000357621462472665400204340ustar00rootroot00000000000000from __future__ import annotations import gc import sys from typing import NoReturn import pytest from anyio import ( BrokenResourceError, CancelScope, ClosedResourceError, EndOfStream, WouldBlock, create_memory_object_stream, create_task_group, fail_after, wait_all_tasks_blocked, ) from anyio.abc import ObjectReceiveStream, ObjectSendStream, TaskStatus from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup pytestmark = pytest.mark.anyio def test_invalid_max_buffer() -> None: pytest.raises(ValueError, create_memory_object_stream, 1.0).match( "max_buffer_size must be either an integer or math.inf" ) def test_negative_max_buffer() -> None: pytest.raises(ValueError, create_memory_object_stream, -1).match( "max_buffer_size cannot be negative" ) async def test_receive_then_send() -> None: async def receiver() -> None: received_objects.append(await receive.receive()) received_objects.append(await receive.receive()) send, receive = create_memory_object_stream[str](0) received_objects: list[str] = [] async with create_task_group() as tg: tg.start_soon(receiver) await wait_all_tasks_blocked() await send.send("hello") await send.send("anyio") assert received_objects == ["hello", "anyio"] send.close() receive.close() async def test_receive_then_send_nowait() -> None: async def receiver() -> None: received_objects.append(await receive.receive()) send, receive = create_memory_object_stream[str](0) received_objects: list[str] = [] async with create_task_group() as tg: tg.start_soon(receiver) tg.start_soon(receiver) await wait_all_tasks_blocked() send.send_nowait("hello") send.send_nowait("anyio") assert sorted(received_objects, reverse=True) == ["hello", "anyio"] send.close() receive.close() async def test_send_then_receive_nowait() -> None: send, receive = create_memory_object_stream[str](0) async with create_task_group() as tg: tg.start_soon(send.send, "hello") await wait_all_tasks_blocked() assert receive.receive_nowait() == "hello" send.close() receive.close() async def test_send_is_unblocked_after_receive_nowait() -> None: send, receive = create_memory_object_stream[str](1) send.send_nowait("hello") with fail_after(1): async with create_task_group() as tg: tg.start_soon(send.send, "anyio") await wait_all_tasks_blocked() assert receive.receive_nowait() == "hello" assert receive.receive_nowait() == "anyio" send.close() receive.close() async def test_send_nowait_then_receive_nowait() -> None: send, receive = create_memory_object_stream[str](2) send.send_nowait("hello") send.send_nowait("anyio") assert receive.receive_nowait() == "hello" assert receive.receive_nowait() == "anyio" send.close() receive.close() async def test_iterate() -> None: async def receiver() -> None: async for item in receive: received_objects.append(item) send, receive = create_memory_object_stream[str]() received_objects: list[str] = [] async with create_task_group() as tg: tg.start_soon(receiver) await send.send("hello") await send.send("anyio") await send.aclose() assert received_objects == ["hello", "anyio"] send.close() receive.close() async def test_receive_send_closed_send_stream() -> None: send, receive = create_memory_object_stream[None]() await send.aclose() with pytest.raises(EndOfStream): receive.receive_nowait() with pytest.raises(ClosedResourceError): await send.send(None) receive.close() async def test_receive_send_closed_receive_stream() -> None: send, receive = create_memory_object_stream[None]() await receive.aclose() with pytest.raises(ClosedResourceError): receive.receive_nowait() with pytest.raises(BrokenResourceError): await send.send(None) send.close() async def test_cancel_receive() -> None: send, receive = create_memory_object_stream[str]() async with create_task_group() as tg: tg.start_soon(receive.receive) await wait_all_tasks_blocked() tg.cancel_scope.cancel() with pytest.raises(WouldBlock): send.send_nowait("hello") send.close() receive.close() async def test_cancel_send() -> None: send, receive = create_memory_object_stream[str]() async with create_task_group() as tg: tg.start_soon(send.send, "hello") await wait_all_tasks_blocked() tg.cancel_scope.cancel() with pytest.raises(WouldBlock): receive.receive_nowait() send.close() receive.close() async def test_clone() -> None: send1, receive1 = create_memory_object_stream[str](1) send2 = send1.clone() receive2 = receive1.clone() await send1.aclose() await receive1.aclose() send2.send_nowait("hello") assert receive2.receive_nowait() == "hello" send1.close() receive1.close() send2.close() receive2.close() async def test_clone_closed() -> None: send, receive = create_memory_object_stream[NoReturn](1) await send.aclose() await receive.aclose() pytest.raises(ClosedResourceError, send.clone) pytest.raises(ClosedResourceError, receive.clone) async def test_close_send_while_receiving() -> None: send, receive = create_memory_object_stream[NoReturn](1) with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(receive.receive) await wait_all_tasks_blocked() await send.aclose() assert len(exc.value.exceptions) == 1 assert isinstance(exc.value.exceptions[0], EndOfStream) send.close() receive.close() async def test_close_receive_while_sending() -> None: # We send None here as a regression test for #731 send, receive = create_memory_object_stream[None](0) with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(send.send, None) await wait_all_tasks_blocked() await receive.aclose() assert len(exc.value.exceptions) == 1 assert isinstance(exc.value.exceptions[0], BrokenResourceError) send.close() receive.close() async def test_receive_after_send_closed() -> None: send, receive = create_memory_object_stream[str](1) await send.send("hello") await send.aclose() assert await receive.receive() == "hello" send.close() receive.close() async def test_receive_when_cancelled() -> None: """ Test that calling receive() in a cancelled scope prevents it from going through with the operation. """ send, receive = create_memory_object_stream[str]() async with create_task_group() as tg: tg.start_soon(send.send, "hello") await wait_all_tasks_blocked() tg.start_soon(send.send, "world") await wait_all_tasks_blocked() with CancelScope() as scope: scope.cancel() await receive.receive() assert await receive.receive() == "hello" assert await receive.receive() == "world" send.close() receive.close() async def test_send_when_cancelled() -> None: """ Test that calling send() in a cancelled scope prevents it from going through with the operation. """ async def receiver() -> None: received.append(await receive.receive()) received: list[str] = [] send, receive = create_memory_object_stream[str]() async with create_task_group() as tg: tg.start_soon(receiver) with CancelScope() as scope: scope.cancel() await send.send("hello") await send.send("world") assert received == ["world"] send.close() receive.close() async def test_cancel_during_receive() -> None: """ Test that cancelling a pending receive() operation does not cause an item in the stream to be lost. """ async def scoped_receiver(task_status: TaskStatus[CancelScope]) -> None: with CancelScope() as cancel_scope: task_status.started(cancel_scope) received.append(await receive.receive()) assert cancel_scope.cancel_called received: list[str] = [] send, receive = create_memory_object_stream[str]() with send, receive: async with create_task_group() as tg: receiver_scope = await tg.start(scoped_receiver) await wait_all_tasks_blocked() send.send_nowait("hello") receiver_scope.cancel() assert received == ["hello"] async def test_cancel_during_receive_buffered() -> None: """ Test that sending an item to a memory object stream when the receiver that is next in line has been cancelled will not result in the item being lost. """ async def scoped_receiver( receive: MemoryObjectReceiveStream[str], task_status: TaskStatus[CancelScope] ) -> None: with CancelScope() as cancel_scope: task_status.started(cancel_scope) await receive.receive() send, receive = create_memory_object_stream[str](1) with send, receive: async with create_task_group() as tg: cancel_scope = await tg.start(scoped_receiver, receive) await wait_all_tasks_blocked() cancel_scope.cancel() send.send_nowait("item") # Since the item was not sent to the cancelled task, it should be available here assert receive.receive_nowait() == "item" async def test_close_receive_after_send() -> None: async def send() -> None: async with send_stream: await send_stream.send("test") async def receive() -> None: async with receive_stream: assert await receive_stream.receive() == "test" send_stream, receive_stream = create_memory_object_stream[str]() async with create_task_group() as tg: tg.start_soon(send) tg.start_soon(receive) send_stream.close() receive_stream.close() async def test_statistics() -> None: send_stream, receive_stream = create_memory_object_stream[None](1) streams: list[MemoryObjectReceiveStream[None] | MemoryObjectSendStream[None]] = [ send_stream, receive_stream, ] for stream in streams: statistics = stream.statistics() assert statistics.max_buffer_size == 1 assert statistics.current_buffer_used == 0 assert statistics.open_send_streams == 1 assert statistics.open_receive_streams == 1 assert statistics.tasks_waiting_send == 0 assert statistics.tasks_waiting_receive == 0 for stream in streams: async with create_task_group() as tg: # Test tasks_waiting_send send_stream.send_nowait(None) assert stream.statistics().current_buffer_used == 1 tg.start_soon(send_stream.send, None) await wait_all_tasks_blocked() assert stream.statistics().current_buffer_used == 1 assert stream.statistics().tasks_waiting_send == 1 receive_stream.receive_nowait() assert stream.statistics().current_buffer_used == 1 assert stream.statistics().tasks_waiting_send == 0 receive_stream.receive_nowait() assert stream.statistics().current_buffer_used == 0 # Test tasks_waiting_receive tg.start_soon(receive_stream.receive) await wait_all_tasks_blocked() assert stream.statistics().tasks_waiting_receive == 1 send_stream.send_nowait(None) assert stream.statistics().tasks_waiting_receive == 0 async with create_task_group() as tg: # Test tasks_waiting_send send_stream.send_nowait(None) assert stream.statistics().tasks_waiting_send == 0 for _ in range(3): tg.start_soon(send_stream.send, None) await wait_all_tasks_blocked() assert stream.statistics().tasks_waiting_send == 3 for i in range(2, -1, -1): receive_stream.receive_nowait() assert stream.statistics().tasks_waiting_send == i receive_stream.receive_nowait() assert stream.statistics().current_buffer_used == 0 assert stream.statistics().tasks_waiting_send == 0 assert stream.statistics().tasks_waiting_receive == 0 send_stream.close() receive_stream.close() async def test_sync_close() -> None: send_stream, receive_stream = create_memory_object_stream[None](1) with send_stream, receive_stream: pass with pytest.raises(ClosedResourceError): send_stream.send_nowait(None) with pytest.raises(ClosedResourceError): receive_stream.receive_nowait() async def test_type_variance() -> None: """ This test does not do anything at run time, but since the test suite is also checked with a static type checker, it ensures that the memory object stream co/contravariance works as intended. If it doesn't, one or both of the following reassignments will trip the type checker. """ send, receive = create_memory_object_stream[float]() receive1: MemoryObjectReceiveStream[complex] = receive # noqa: F841 receive2: ObjectReceiveStream[complex] = receive # noqa: F841 send1: MemoryObjectSendStream[int] = send # noqa: F841 send2: ObjectSendStream[int] = send # noqa: F841 send.close() receive.close() async def test_deprecated_item_type_parameter() -> None: with pytest.warns(DeprecationWarning, match="item_type argument has been "): send, receive = create_memory_object_stream(item_type=int) # type: ignore[var-annotated] send.close() receive.close() async def test_not_closed_warning() -> None: send, receive = create_memory_object_stream[int]() with pytest.warns(ResourceWarning, match="Unclosed "): del send gc.collect() with pytest.warns(ResourceWarning, match="Unclosed "): del receive gc.collect() @pytest.mark.parametrize("anyio_backend", ["asyncio"], indirect=True) async def test_send_to_natively_cancelled_receiver() -> None: """ Test that if a task waiting on receive.receive() is cancelled and then another task sends an item, said item is not delivered to the task with a pending cancellation, but rather to the next one in line. """ from asyncio import CancelledError, create_task send, receive = create_memory_object_stream[str](1) with send, receive: receive_task = create_task(receive.receive()) await wait_all_tasks_blocked() # ensure that the task is waiting to receive receive_task.cancel() send.send_nowait("hello") with pytest.raises(CancelledError): await receive_task assert receive.receive_nowait() == "hello" anyio-4.4.0/tests/streams/test_stapled.py000066400000000000000000000127521462472665400205520ustar00rootroot00000000000000from __future__ import annotations from collections import deque from dataclasses import InitVar, dataclass, field from typing import Iterable, TypeVar import pytest from anyio import ClosedResourceError, EndOfStream from anyio.abc import ( ByteReceiveStream, ByteSendStream, ObjectReceiveStream, ObjectSendStream, ) from anyio.streams.stapled import StapledByteStream, StapledObjectStream pytestmark = pytest.mark.anyio @dataclass class DummyByteReceiveStream(ByteReceiveStream): data: InitVar[bytes] buffer: bytearray = field(init=False) _closed: bool = field(init=False, default=False) def __post_init__(self, data: bytes) -> None: self.buffer = bytearray(data) async def receive(self, max_bytes: int = 65536) -> bytes: if self._closed: raise ClosedResourceError data = bytes(self.buffer[:max_bytes]) del self.buffer[:max_bytes] return data async def aclose(self) -> None: self._closed = True @dataclass class DummyByteSendStream(ByteSendStream): buffer: bytearray = field(init=False, default_factory=bytearray) _closed: bool = field(init=False, default=False) async def send(self, item: bytes) -> None: if self._closed: raise ClosedResourceError self.buffer.extend(item) async def aclose(self) -> None: self._closed = True class TestStapledByteStream: @pytest.fixture def send_stream(self) -> DummyByteSendStream: return DummyByteSendStream() @pytest.fixture def receive_stream(self) -> DummyByteReceiveStream: return DummyByteReceiveStream(b"hello, world") @pytest.fixture def stapled( self, send_stream: DummyByteSendStream, receive_stream: DummyByteReceiveStream ) -> StapledByteStream: return StapledByteStream(send_stream, receive_stream) async def test_receive_send( self, stapled: StapledByteStream, send_stream: DummyByteSendStream ) -> None: assert await stapled.receive(3) == b"hel" assert await stapled.receive() == b"lo, world" assert await stapled.receive() == b"" await stapled.send(b"how are you ") await stapled.send(b"today?") assert stapled.send_stream is send_stream assert bytes(send_stream.buffer) == b"how are you today?" async def test_send_eof(self, stapled: StapledByteStream) -> None: await stapled.send_eof() await stapled.send_eof() with pytest.raises(ClosedResourceError): await stapled.send(b"world") assert await stapled.receive() == b"hello, world" async def test_aclose(self, stapled: StapledByteStream) -> None: await stapled.aclose() with pytest.raises(ClosedResourceError): await stapled.receive() with pytest.raises(ClosedResourceError): await stapled.send(b"") T_Item = TypeVar("T_Item") @dataclass class DummyObjectReceiveStream(ObjectReceiveStream[T_Item]): data: InitVar[Iterable[T_Item]] buffer: deque[T_Item] = field(init=False) _closed: bool = field(init=False, default=False) def __post_init__(self, data: Iterable[T_Item]) -> None: self.buffer = deque(data) async def receive(self) -> T_Item: if self._closed: raise ClosedResourceError if not self.buffer: raise EndOfStream return self.buffer.popleft() async def aclose(self) -> None: self._closed = True @dataclass class DummyObjectSendStream(ObjectSendStream[T_Item]): buffer: list[T_Item] = field(init=False, default_factory=list) _closed: bool = field(init=False, default=False) async def send(self, item: T_Item) -> None: if self._closed: raise ClosedResourceError self.buffer.append(item) async def aclose(self) -> None: self._closed = True class TestStapledObjectStream: @pytest.fixture def receive_stream(self) -> DummyObjectReceiveStream[str]: return DummyObjectReceiveStream(["hello", "world"]) @pytest.fixture def send_stream(self) -> DummyObjectSendStream[str]: return DummyObjectSendStream[str]() @pytest.fixture def stapled( self, receive_stream: DummyObjectReceiveStream[str], send_stream: DummyObjectSendStream[str], ) -> StapledObjectStream[str]: return StapledObjectStream(send_stream, receive_stream) async def test_receive_send( self, stapled: StapledObjectStream[str], send_stream: DummyObjectSendStream[str] ) -> None: assert await stapled.receive() == "hello" assert await stapled.receive() == "world" with pytest.raises(EndOfStream): await stapled.receive() await stapled.send("how are you ") await stapled.send("today?") assert stapled.send_stream is send_stream assert send_stream.buffer == ["how are you ", "today?"] async def test_send_eof(self, stapled: StapledObjectStream[str]) -> None: await stapled.send_eof() await stapled.send_eof() with pytest.raises(ClosedResourceError): await stapled.send("world") assert await stapled.receive() == "hello" assert await stapled.receive() == "world" async def test_aclose(self, stapled: StapledObjectStream[str]) -> None: await stapled.aclose() with pytest.raises(ClosedResourceError): await stapled.receive() with pytest.raises(ClosedResourceError): await stapled.send(b"") # type: ignore[arg-type] anyio-4.4.0/tests/streams/test_text.py000066400000000000000000000047441462472665400201040ustar00rootroot00000000000000from __future__ import annotations import platform import sys import pytest from anyio import create_memory_object_stream from anyio.streams.stapled import StapledObjectStream from anyio.streams.text import TextReceiveStream, TextSendStream, TextStream pytestmark = pytest.mark.anyio async def test_receive() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](1) text_stream = TextReceiveStream(receive_stream) await send_stream.send(b"\xc3\xa5\xc3\xa4\xc3") # ends with half of the "ö" letter assert await text_stream.receive() == "åä" # Send the missing byte for "ö" await send_stream.send(b"\xb6") assert await text_stream.receive() == "ö" send_stream.close() receive_stream.close() async def test_send() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](1) text_stream = TextSendStream(send_stream) await text_stream.send("åäö") assert await receive_stream.receive() == b"\xc3\xa5\xc3\xa4\xc3\xb6" send_stream.close() receive_stream.close() @pytest.mark.xfail( platform.python_implementation() == "PyPy" and sys.pypy_version_info < (7, 3, 2), # type: ignore[attr-defined] reason="PyPy has a bug in its incremental UTF-8 decoder (#3274)", ) async def test_receive_encoding_error() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](1) text_stream = TextReceiveStream(receive_stream, errors="replace") await send_stream.send(b"\xe5\xe4\xf6") # "åäö" in latin-1 assert await text_stream.receive() == "���" send_stream.close() receive_stream.close() async def test_send_encoding_error() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](1) text_stream = TextSendStream(send_stream, encoding="iso-8859-1", errors="replace") await text_stream.send("€") assert await receive_stream.receive() == b"?" send_stream.close() receive_stream.close() async def test_bidirectional_stream() -> None: send_stream, receive_stream = create_memory_object_stream[bytes](1) stapled_stream = StapledObjectStream(send_stream, receive_stream) text_stream = TextStream(stapled_stream) await text_stream.send("åäö") assert await receive_stream.receive() == b"\xc3\xa5\xc3\xa4\xc3\xb6" await send_stream.send(b"\xc3\xa6\xc3\xb8") assert await text_stream.receive() == "æø" assert text_stream.extra_attributes == {} send_stream.close() receive_stream.close() anyio-4.4.0/tests/streams/test_tls.py000066400000000000000000000445461462472665400177260ustar00rootroot00000000000000from __future__ import annotations import socket import ssl from contextlib import ExitStack from threading import Thread from typing import ContextManager, NoReturn import pytest from pytest_mock import MockerFixture from trustme import CA from anyio import ( BrokenResourceError, EndOfStream, Event, connect_tcp, create_memory_object_stream, create_task_group, create_tcp_listener, to_thread, ) from anyio.abc import AnyByteStream, SocketAttribute, SocketStream from anyio.streams.stapled import StapledObjectStream from anyio.streams.tls import TLSAttribute, TLSListener, TLSStream pytestmark = pytest.mark.anyio class TestTLSStream: async def test_send_receive( self, server_context: ssl.SSLContext, client_context: ssl.SSLContext ) -> None: def serve_sync() -> None: conn, addr = server_sock.accept() conn.settimeout(1) data = conn.recv(10) conn.send(data[::-1]) conn.close() server_sock = server_context.wrap_socket( socket.socket(), server_side=True, suppress_ragged_eofs=False ) server_sock.settimeout(1) server_sock.bind(("127.0.0.1", 0)) server_sock.listen() server_thread = Thread(target=serve_sync) server_thread.start() async with await connect_tcp(*server_sock.getsockname()) as stream: wrapper = await TLSStream.wrap( stream, hostname="localhost", ssl_context=client_context ) await wrapper.send(b"hello") response = await wrapper.receive() server_thread.join() server_sock.close() assert response == b"olleh" async def test_extra_attributes( self, server_context: ssl.SSLContext, client_context: ssl.SSLContext ) -> None: def serve_sync() -> None: conn, addr = server_sock.accept() with conn: conn.settimeout(1) conn.recv(1) server_context.set_alpn_protocols(["h2"]) client_context.set_alpn_protocols(["h2"]) server_sock = server_context.wrap_socket( socket.socket(), server_side=True, suppress_ragged_eofs=True ) server_sock.settimeout(1) server_sock.bind(("127.0.0.1", 0)) server_sock.listen() server_thread = Thread(target=serve_sync) server_thread.start() async with await connect_tcp(*server_sock.getsockname()) as stream: wrapper = await TLSStream.wrap( stream, hostname="localhost", ssl_context=client_context, standard_compatible=False, ) async with wrapper: for name, attribute in SocketAttribute.__dict__.items(): if not name.startswith("_"): assert wrapper.extra(attribute) == stream.extra(attribute) assert wrapper.extra(TLSAttribute.alpn_protocol) == "h2" assert isinstance( wrapper.extra(TLSAttribute.channel_binding_tls_unique), bytes ) assert isinstance(wrapper.extra(TLSAttribute.cipher), tuple) assert isinstance(wrapper.extra(TLSAttribute.peer_certificate), dict) assert isinstance( wrapper.extra(TLSAttribute.peer_certificate_binary), bytes ) assert wrapper.extra(TLSAttribute.server_side) is False assert wrapper.extra(TLSAttribute.shared_ciphers) is None assert isinstance(wrapper.extra(TLSAttribute.ssl_object), ssl.SSLObject) assert wrapper.extra(TLSAttribute.standard_compatible) is False assert wrapper.extra(TLSAttribute.tls_version).startswith("TLSv") await wrapper.send(b"\x00") server_thread.join() server_sock.close() async def test_unwrap( self, server_context: ssl.SSLContext, client_context: ssl.SSLContext ) -> None: def serve_sync() -> None: conn, addr = server_sock.accept() conn.settimeout(1) conn.send(b"encrypted") unencrypted = conn.unwrap() unencrypted.send(b"unencrypted") unencrypted.close() server_sock = server_context.wrap_socket( socket.socket(), server_side=True, suppress_ragged_eofs=False ) server_sock.settimeout(1) server_sock.bind(("127.0.0.1", 0)) server_sock.listen() server_thread = Thread(target=serve_sync) server_thread.start() async with await connect_tcp(*server_sock.getsockname()) as stream: wrapper = await TLSStream.wrap( stream, hostname="localhost", ssl_context=client_context ) msg1 = await wrapper.receive() unwrapped_stream, msg2 = await wrapper.unwrap() if msg2 != b"unencrypted": msg2 += await unwrapped_stream.receive() server_thread.join() server_sock.close() assert msg1 == b"encrypted" assert msg2 == b"unencrypted" @pytest.mark.skipif(not ssl.HAS_ALPN, reason="ALPN support not available") async def test_alpn_negotiation( self, server_context: ssl.SSLContext, client_context: ssl.SSLContext ) -> None: def serve_sync() -> None: conn, addr = server_sock.accept() conn.settimeout(1) selected_alpn_protocol = conn.selected_alpn_protocol() assert selected_alpn_protocol is not None conn.send(selected_alpn_protocol.encode()) conn.close() server_context.set_alpn_protocols(["dummy1", "dummy2"]) client_context.set_alpn_protocols(["dummy2", "dummy3"]) server_sock = server_context.wrap_socket( socket.socket(), server_side=True, suppress_ragged_eofs=False ) server_sock.settimeout(1) server_sock.bind(("127.0.0.1", 0)) server_sock.listen() server_thread = Thread(target=serve_sync) server_thread.start() async with await connect_tcp(*server_sock.getsockname()) as stream: wrapper = await TLSStream.wrap( stream, hostname="localhost", ssl_context=client_context ) assert wrapper.extra(TLSAttribute.alpn_protocol) == "dummy2" server_alpn_protocol = await wrapper.receive() server_thread.join() server_sock.close() assert server_alpn_protocol == b"dummy2" @pytest.mark.parametrize( "server_compatible, client_compatible", [ pytest.param(True, True, id="both_standard"), pytest.param(True, False, id="server_standard"), pytest.param(False, True, id="client_standard"), pytest.param(False, False, id="neither_standard"), ], ) async def test_ragged_eofs( self, server_context: ssl.SSLContext, client_context: ssl.SSLContext, server_compatible: bool, client_compatible: bool, ) -> None: server_exc = None def serve_sync() -> None: nonlocal server_exc conn, addr = server_sock.accept() try: conn.settimeout(1) conn.sendall(b"hello") if server_compatible: conn.unwrap() except BaseException as exc: server_exc = exc finally: conn.close() client_cm: ContextManager = ExitStack() if client_compatible and not server_compatible: client_cm = pytest.raises(BrokenResourceError) server_sock = server_context.wrap_socket( socket.socket(), server_side=True, suppress_ragged_eofs=not server_compatible, ) server_sock.settimeout(1) server_sock.bind(("127.0.0.1", 0)) server_sock.listen() server_thread = Thread(target=serve_sync, daemon=True) server_thread.start() async with await connect_tcp(*server_sock.getsockname()) as stream: wrapper = await TLSStream.wrap( stream, hostname="localhost", ssl_context=client_context, standard_compatible=client_compatible, ) with client_cm: assert await wrapper.receive() == b"hello" await wrapper.aclose() server_thread.join() server_sock.close() if not client_compatible and server_compatible: assert isinstance(server_exc, OSError) assert not isinstance(server_exc, socket.timeout) else: assert server_exc is None async def test_ragged_eof_on_receive( self, server_context: ssl.SSLContext, client_context: ssl.SSLContext ) -> None: server_exc = None def serve_sync() -> None: nonlocal server_exc conn, addr = server_sock.accept() try: conn.settimeout(1) conn.sendall(b"hello") except BaseException as exc: server_exc = exc finally: conn.close() server_sock = server_context.wrap_socket( socket.socket(), server_side=True, suppress_ragged_eofs=True ) server_sock.settimeout(1) server_sock.bind(("127.0.0.1", 0)) server_sock.listen() server_thread = Thread(target=serve_sync, daemon=True) server_thread.start() try: async with await connect_tcp(*server_sock.getsockname()) as stream: wrapper = await TLSStream.wrap( stream, hostname="localhost", ssl_context=client_context, standard_compatible=False, ) assert await wrapper.receive() == b"hello" with pytest.raises(EndOfStream): await wrapper.receive() finally: server_thread.join() server_sock.close() assert server_exc is None async def test_receive_send_after_eof( self, server_context: ssl.SSLContext, client_context: ssl.SSLContext ) -> None: def serve_sync() -> None: conn, addr = server_sock.accept() conn.sendall(b"hello") conn.unwrap() conn.close() server_sock = server_context.wrap_socket( socket.socket(), server_side=True, suppress_ragged_eofs=False ) server_sock.settimeout(1) server_sock.bind(("127.0.0.1", 0)) server_sock.listen() server_thread = Thread(target=serve_sync, daemon=True) server_thread.start() stream = await connect_tcp(*server_sock.getsockname()) async with await TLSStream.wrap( stream, hostname="localhost", ssl_context=client_context ) as wrapper: assert await wrapper.receive() == b"hello" with pytest.raises(EndOfStream): await wrapper.receive() server_thread.join() server_sock.close() @pytest.mark.parametrize( "force_tlsv12", [ pytest.param( False, marks=[ pytest.mark.skipif( not getattr(ssl, "HAS_TLSv1_3", False), reason="No TLS 1.3 support", ) ], ), pytest.param(True), ], ids=["tlsv13", "tlsv12"], ) async def test_send_eof_not_implemented( self, server_context: ssl.SSLContext, ca: CA, force_tlsv12: bool ) -> None: def serve_sync() -> None: conn, addr = server_sock.accept() conn.sendall(b"hello") conn.unwrap() conn.close() client_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) ca.configure_trust(client_context) if force_tlsv12: expected_pattern = r"send_eof\(\) requires at least TLSv1.3" client_context.maximum_version = ssl.TLSVersion.TLSv1_2 else: expected_pattern = ( r"send_eof\(\) has not yet been implemented for TLS streams" ) server_sock = server_context.wrap_socket( socket.socket(), server_side=True, suppress_ragged_eofs=False ) server_sock.settimeout(1) server_sock.bind(("127.0.0.1", 0)) server_sock.listen() server_thread = Thread(target=serve_sync, daemon=True) server_thread.start() stream = await connect_tcp(*server_sock.getsockname()) async with await TLSStream.wrap( stream, hostname="localhost", ssl_context=client_context ) as wrapper: assert await wrapper.receive() == b"hello" with pytest.raises(NotImplementedError) as exc: await wrapper.send_eof() exc.match(expected_pattern) server_thread.join() server_sock.close() @pytest.mark.skipif( not hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"), reason="The ssl module does not have the OP_IGNORE_UNEXPECTED_EOF attribute", ) async def test_default_context_ignore_unexpected_eof_flag_off( self, mocker: MockerFixture ) -> None: send1, receive1 = create_memory_object_stream[bytes]() client_stream = StapledObjectStream(send1, receive1) mocker.patch.object(TLSStream, "_call_sslobject_method") tls_stream = await TLSStream.wrap(client_stream) ssl_context = tls_stream.extra(TLSAttribute.ssl_object).context assert not ssl_context.options & ssl.OP_IGNORE_UNEXPECTED_EOF send1.close() receive1.close() class TestTLSListener: async def test_handshake_fail( self, server_context: ssl.SSLContext, caplog: pytest.LogCaptureFixture ) -> None: def handler(stream: object) -> NoReturn: pytest.fail("This function should never be called in this scenario") exception = None class CustomTLSListener(TLSListener): @staticmethod async def handle_handshake_error( exc: BaseException, stream: AnyByteStream ) -> None: nonlocal exception await TLSListener.handle_handshake_error(exc, stream) # Regression test for #608 assert len(caplog.records) == 1 logged_exc_info = caplog.records[0].exc_info logged_exc = logged_exc_info[1] if logged_exc_info is not None else None assert logged_exc is exc assert isinstance(stream, SocketStream) exception = exc event.set() event = Event() listener = await create_tcp_listener(local_host="127.0.0.1") tls_listener = CustomTLSListener(listener, server_context) async with tls_listener, create_task_group() as tg: tg.start_soon(tls_listener.serve, handler) sock = socket.socket() sock.connect(listener.extra(SocketAttribute.local_address)) sock.close() await event.wait() tg.cancel_scope.cancel() assert isinstance(exception, BrokenResourceError) async def test_extra_attributes( self, client_context: ssl.SSLContext, server_context: ssl.SSLContext, ca: CA ) -> None: def connect_sync(addr: tuple[str, int]) -> None: with socket.create_connection(addr) as plain_sock: plain_sock.settimeout(2) with client_context.wrap_socket( plain_sock, server_side=False, server_hostname="localhost", suppress_ragged_eofs=False, ) as conn: conn.recv(1) conn.unwrap() class CustomTLSListener(TLSListener): @staticmethod async def handle_handshake_error( exc: BaseException, stream: AnyByteStream ) -> None: await TLSListener.handle_handshake_error(exc, stream) pytest.fail("TLS handshake failed") async def handler(stream: TLSStream) -> None: async with stream: try: assert stream.extra(TLSAttribute.alpn_protocol) == "h2" assert isinstance( stream.extra(TLSAttribute.channel_binding_tls_unique), bytes ) assert isinstance(stream.extra(TLSAttribute.cipher), tuple) assert isinstance(stream.extra(TLSAttribute.peer_certificate), dict) assert isinstance( stream.extra(TLSAttribute.peer_certificate_binary), bytes ) assert stream.extra(TLSAttribute.server_side) is True shared_ciphers = stream.extra(TLSAttribute.shared_ciphers) assert isinstance(shared_ciphers, list) assert len(shared_ciphers) > 1 assert isinstance( stream.extra(TLSAttribute.ssl_object), ssl.SSLObject ) assert stream.extra(TLSAttribute.standard_compatible) is True assert stream.extra(TLSAttribute.tls_version).startswith("TLSv") finally: event.set() await stream.send(b"\x00") # Issue a client certificate and make the server trust it client_cert = ca.issue_cert("dummy-client") client_cert.configure_cert(client_context) ca.configure_trust(server_context) server_context.verify_mode = ssl.CERT_REQUIRED event = Event() server_context.set_alpn_protocols(["h2"]) client_context.set_alpn_protocols(["h2"]) listener = await create_tcp_listener(local_host="127.0.0.1") tls_listener = CustomTLSListener(listener, server_context) async with tls_listener, create_task_group() as tg: assert tls_listener.extra(TLSAttribute.standard_compatible) is True tg.start_soon(tls_listener.serve, handler) client_thread = Thread( target=connect_sync, args=[listener.extra(SocketAttribute.local_address)], ) client_thread.start() await event.wait() await to_thread.run_sync(client_thread.join) tg.cancel_scope.cancel() anyio-4.4.0/tests/test_debugging.py000066400000000000000000000110631462472665400173650ustar00rootroot00000000000000from __future__ import annotations import asyncio import sys from collections.abc import AsyncGenerator, Coroutine, Generator from typing import Any, cast import pytest import anyio from anyio import ( Event, TaskInfo, create_task_group, get_current_task, get_running_tasks, move_on_after, wait_all_tasks_blocked, ) from anyio.abc import TaskStatus pytestmark = pytest.mark.anyio get_coro = asyncio.Task.get_coro def test_main_task_name( anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: task_name = None async def main() -> None: nonlocal task_name task_name = get_current_task().name anyio.run(main, backend=anyio_backend_name, backend_options=anyio_backend_options) assert task_name == "tests.test_debugging.test_main_task_name..main" # Work around sniffio/asyncio bug that leaves behind an unclosed event loop if anyio_backend_name == "asyncio": import asyncio import gc for loop in [ obj for obj in gc.get_objects() if isinstance(obj, asyncio.AbstractEventLoop) ]: loop.close() @pytest.mark.parametrize( "name_input,expected", [ (None, "tests.test_debugging.test_non_main_task_name..non_main"), (b"name", "b'name'"), ("name", "name"), ("", ""), ], ) async def test_non_main_task_name( name_input: bytes | str | None, expected: str ) -> None: async def non_main(*, task_status: TaskStatus) -> None: task_status.started(anyio.get_current_task().name) async with anyio.create_task_group() as tg: name = await tg.start(non_main, name=name_input) assert name == expected async def test_get_running_tasks() -> None: async def inspect() -> None: await wait_all_tasks_blocked() new_tasks = set(get_running_tasks()) - existing_tasks task_infos[:] = sorted(new_tasks, key=lambda info: info.name or "") event.set() event = Event() task_infos: list[TaskInfo] = [] host_task = get_current_task() async with create_task_group() as tg: existing_tasks = set(get_running_tasks()) tg.start_soon(event.wait, name="task1") tg.start_soon(event.wait, name="task2") tg.start_soon(inspect) assert len(task_infos) == 3 expected_names = [ "task1", "task2", "tests.test_debugging.test_get_running_tasks..inspect", ] for task, expected_name in zip(task_infos, expected_names): assert task.parent_id == host_task.id assert task.name == expected_name assert repr(task).endswith(f"TaskInfo(id={task.id}, name={expected_name!r})") @pytest.mark.skipif( sys.version_info >= (3, 11), reason="Generator based coroutines have been removed in Python 3.11", ) @pytest.mark.filterwarnings( 'ignore:"@coroutine" decorator is deprecated:DeprecationWarning' ) def test_wait_generator_based_task_blocked( asyncio_event_loop: asyncio.AbstractEventLoop, ) -> None: async def native_coro_part() -> None: await wait_all_tasks_blocked() gen = cast(Generator, get_coro(gen_task)) assert not gen.gi_running coro = cast(Coroutine, gen.gi_yieldfrom) assert coro.cr_code.co_name == "wait" event.set() @asyncio.coroutine # type: ignore[attr-defined] def generator_part() -> Generator[object, BaseException, None]: yield from event.wait() # type: ignore[misc] event = asyncio.Event() gen_task: asyncio.Task[None] = asyncio_event_loop.create_task(generator_part()) asyncio_event_loop.run_until_complete(native_coro_part()) @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_wait_all_tasks_blocked_asend(anyio_backend: str) -> None: """Test that wait_all_tasks_blocked() does not crash on an `asend()` object.""" async def agen_func() -> AsyncGenerator[None, None]: yield agen = agen_func() coro = agen.asend(None) loop = asyncio.get_running_loop() task = loop.create_task(cast("Coroutine[Any, Any, Any]", coro)) await wait_all_tasks_blocked() await task await agen.aclose() async def test_wait_all_tasks_blocked_cancelled_task() -> None: done = False async def self_cancel(*, task_status: TaskStatus) -> None: nonlocal done task_status.started() with move_on_after(-1): await Event().wait() done = True async with create_task_group() as tg: await tg.start(self_cancel) await wait_all_tasks_blocked() assert done anyio-4.4.0/tests/test_eventloop.py000066400000000000000000000051551462472665400174520ustar00rootroot00000000000000from __future__ import annotations import asyncio import math from asyncio import get_running_loop from unittest.mock import AsyncMock import pytest from pytest import MonkeyPatch from pytest_mock.plugin import MockerFixture from anyio import run, sleep_forever, sleep_until pytestmark = pytest.mark.anyio fake_current_time = 1620581544.0 @pytest.fixture def fake_sleep(mocker: MockerFixture) -> AsyncMock: mocker.patch("anyio._core._eventloop.current_time", return_value=fake_current_time) return mocker.patch("anyio._core._eventloop.sleep", AsyncMock()) async def test_sleep_until(fake_sleep: AsyncMock) -> None: deadline = fake_current_time + 500.102352 await sleep_until(deadline) fake_sleep.assert_called_once_with(deadline - fake_current_time) async def test_sleep_until_in_past(fake_sleep: AsyncMock) -> None: deadline = fake_current_time - 500.102352 await sleep_until(deadline) fake_sleep.assert_called_once_with(0) async def test_sleep_forever(fake_sleep: AsyncMock) -> None: await sleep_forever() fake_sleep.assert_called_once_with(math.inf) def test_run_task() -> None: """Test that anyio.run() on asyncio will work with a callable returning a Future.""" async def async_add(x: int, y: int) -> int: return x + y result = run(asyncio.create_task, async_add(1, 2), backend="asyncio") assert result == 3 class TestAsyncioOptions: def test_debug(self) -> None: async def main() -> bool: return get_running_loop().get_debug() debug = run(main, backend="asyncio", backend_options={"debug": True}) assert debug is True def test_debug_via_env(self, monkeypatch: MonkeyPatch) -> None: async def main() -> bool: return get_running_loop().get_debug() monkeypatch.setenv("PYTHONASYNCIODEBUG", "1") debug = run(main, backend="asyncio") assert debug is True def test_loop_factory(self) -> None: async def main() -> type: return type(get_running_loop()) uvloop = pytest.importorskip("uvloop", reason="uvloop not installed") loop_class = run( main, backend="asyncio", backend_options={"loop_factory": uvloop.new_event_loop}, ) assert issubclass(loop_class, uvloop.Loop) def test_use_uvloop(self) -> None: async def main() -> type: return type(get_running_loop()) uvloop = pytest.importorskip("uvloop", reason="uvloop not installed") loop_class = run(main, backend="asyncio", backend_options={"use_uvloop": True}) assert issubclass(loop_class, uvloop.Loop) anyio-4.4.0/tests/test_fileio.py000066400000000000000000000501111462472665400166760ustar00rootroot00000000000000from __future__ import annotations import os import pathlib import platform import socket import stat import sys import pytest from _pytest.tmpdir import TempPathFactory from anyio import AsyncFile, Path, open_file, wrap_file pytestmark = pytest.mark.anyio class TestAsyncFile: @pytest.fixture(scope="class") def testdata(cls) -> bytes: return b"".join(bytes([i] * 1000) for i in range(10)) @pytest.fixture def testdatafile( self, tmp_path_factory: TempPathFactory, testdata: bytes ) -> pathlib.Path: file = tmp_path_factory.mktemp("file").joinpath("testdata") file.write_bytes(testdata) return file async def test_open_close(self, testdatafile: pathlib.Path) -> None: f = await open_file(testdatafile) await f.aclose() async def test_read(self, testdatafile: pathlib.Path, testdata: bytes) -> None: async with await open_file(testdatafile, "rb") as f: data = await f.read() assert f.closed assert data == testdata async def test_write(self, testdatafile: pathlib.Path, testdata: bytes) -> None: async with await open_file(testdatafile, "ab") as f: await f.write(b"f" * 1000) assert testdatafile.stat().st_size == len(testdata) + 1000 async def test_async_iteration(self, tmp_path: pathlib.Path) -> None: lines = ["blah blah\n", "foo foo\n", "bar bar"] testpath = tmp_path.joinpath("testfile") testpath.write_text("".join(lines), "ascii") async with await open_file(str(testpath)) as f: lines_i = iter(lines) async for line in f: assert line == next(lines_i) async def test_wrap_file(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testdata" with path.open("w") as fp: wrapped = wrap_file(fp) await wrapped.write("dummydata") assert path.read_text() == "dummydata" class TestPath: @pytest.fixture def populated_tmpdir(self, tmp_path: pathlib.Path) -> pathlib.Path: tmp_path.joinpath("testfile").touch() tmp_path.joinpath("testfile2").touch() subdir = tmp_path / "subdir" sibdir = tmp_path / "sibdir" for subpath in (subdir, sibdir): subpath.mkdir() subpath.joinpath("dummyfile1.txt").touch() subpath.joinpath("dummyfile2.txt").touch() return tmp_path async def test_properties(self) -> None: """ Ensure that all public properties and methods are available on the async Path class. """ path = pathlib.Path("/test/path/another/part") stdlib_properties = { p for p in dir(path) if p.startswith("__") or not p.startswith("_") } stdlib_properties.discard("link_to") stdlib_properties.discard("__class_getitem__") stdlib_properties.discard("__enter__") stdlib_properties.discard("__exit__") async_path = Path(path) anyio_properties = { p for p in dir(async_path) if p.startswith("__") or not p.startswith("_") } missing = stdlib_properties - anyio_properties assert not missing def test_repr(self) -> None: assert repr(Path("/foo")) == "Path('/foo')" def test_bytes(self) -> None: assert bytes(Path("/foo-åäö")) == os.fsencode(f"{os.path.sep}foo-åäö") def test_hash(self) -> None: assert hash(Path("/foo")) == hash(pathlib.Path("/foo")) def test_comparison(self) -> None: path1 = Path("/foo1") path2 = Path("/foo2") assert path1 < path2 assert path1 <= path2 assert path2 > path1 assert path2 >= path1 def test_truediv(self) -> None: result = Path("/foo") / "bar" assert isinstance(result, Path) assert result == pathlib.Path("/foo/bar") def test_rtruediv(self) -> None: result = "/foo" / Path("bar") assert isinstance(result, Path) assert result == pathlib.Path("/foo/bar") def test_parts_property(self) -> None: assert Path("/abc/xyz/foo.txt").parts == (os.path.sep, "abc", "xyz", "foo.txt") @pytest.mark.skipif( platform.system() != "Windows", reason="Drive only makes sense on Windows" ) def test_drive_property(self) -> None: assert Path("c:\\abc\\xyz").drive == "c:" def test_root_property(self) -> None: assert Path("/abc/xyz/foo.txt").root == os.path.sep def test_anchor_property(self) -> None: assert Path("/abc/xyz/foo.txt.zip").anchor == os.path.sep def test_parents_property(self) -> None: parents = Path("/abc/xyz/foo.txt").parents assert len(parents) == 3 assert all(isinstance(parent, Path) for parent in parents) assert str(parents[0]) == f"{os.path.sep}abc{os.path.sep}xyz" assert str(parents[1]) == f"{os.path.sep}abc" assert str(parents[2]) == os.path.sep def test_parent_property(self) -> None: parent = Path("/abc/xyz/foo.txt").parent assert isinstance(parent, Path) assert str(parent) == f"{os.path.sep}abc{os.path.sep}xyz" def test_name_property(self) -> None: assert Path("/abc/xyz/foo.txt.zip").name == "foo.txt.zip" def test_suffix_property(self) -> None: assert Path("/abc/xyz/foo.txt.zip").suffix == ".zip" def test_suffixes_property(self) -> None: assert Path("/abc/xyz/foo.tar.gz").suffixes == [".tar", ".gz"] def test_stem_property(self) -> None: assert Path("/abc/xyz/foo.txt.zip").stem == "foo.txt" async def test_absolute(self) -> None: result = await Path("../foo/bar").absolute() assert isinstance(result, Path) assert result == pathlib.Path.cwd() / "../foo/bar" @pytest.mark.skipif( platform.system() != "Windows", reason="Only makes sense on Windows" ) def test_as_posix(self) -> None: assert Path("c:\\foo\\bar").as_posix() == "c:/foo/bar" def test_as_uri(self) -> None: if platform.system() == "Windows": assert Path("c:\\foo\\bar").as_uri() == "file:///c:/foo/bar" else: assert Path("/foo/bar").as_uri() == "file:///foo/bar" async def test_cwd(self) -> None: result = await Path.cwd() assert isinstance(result, Path) assert result == pathlib.Path.cwd() async def test_exists(self, tmp_path: pathlib.Path) -> None: assert not await Path("~/btelkbee").exists() assert await Path(tmp_path).exists() async def test_expanduser(self) -> None: result = await Path("~/btelkbee").expanduser() assert isinstance(result, Path) assert str(result) == os.path.expanduser(f"~{os.path.sep}btelkbee") async def test_home(self) -> None: result = await Path.home() assert isinstance(result, Path) assert result == pathlib.Path.home() @pytest.mark.parametrize( "arg, result", [ ("c:/xyz" if platform.system() == "Windows" else "/xyz", True), ("../xyz", False), ], ) def test_is_absolute(self, arg: str, result: bool) -> None: assert Path(arg).is_absolute() == result @pytest.mark.skipif( platform.system() == "Windows", reason="Block devices are not available on Windows", ) async def test_is_block_device(self) -> None: assert not await Path("/btelkbee").is_block_device() with os.scandir("/dev") as iterator: for entry in iterator: if stat.S_ISBLK(entry.stat(follow_symlinks=False).st_mode): assert await Path(entry.path).is_block_device() break else: pytest.skip("Could not find a suitable block device") @pytest.mark.skipif( platform.system() == "Windows", reason="Character devices are not available on Windows", ) async def test_is_char_device(self) -> None: assert not await Path("/btelkbee").is_char_device() assert await Path("/dev/random").is_char_device() async def test_is_dir(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "somedir" assert not await Path(path).is_dir() path.mkdir() assert await Path(path).is_dir() @pytest.mark.skipif( platform.system() == "Windows", reason="mkfifo() is not available on Windows" ) async def test_is_fifo(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "somefifo" assert not await Path(path).is_fifo() os.mkfifo(path) assert await Path(path).is_fifo() async def test_is_file(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "somefile" assert not await Path(path).is_file() path.touch() assert await Path(path).is_file() @pytest.mark.skipif( sys.version_info < (3, 12), reason="Path.is_junction() is only available on Python 3.12+", ) async def test_is_junction(self, tmp_path: pathlib.Path) -> None: assert not await Path(tmp_path).is_junction() async def test_is_mount(self) -> None: assert not await Path("/gfobj4ewiotj").is_mount() assert await Path("/").is_mount() def test_is_reserved(self) -> None: expected_result = platform.system() == "Windows" assert Path("nul").is_reserved() == expected_result @pytest.mark.skipif( platform.system() == "Windows", reason="UNIX sockets are not available on Windows", ) async def test_is_socket(self, tmp_path_factory: TempPathFactory) -> None: path = tmp_path_factory.mktemp("unix").joinpath("socket") assert not await Path(path).is_socket() with socket.socket(socket.AF_UNIX) as sock: sock.bind(str(path)) assert await Path(path).is_socket() @pytest.mark.skipif( platform.system() == "Windows", reason="symbolic links are not supported on Windows", ) async def test_is_symlink(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" assert not await Path(path).is_symlink() path.symlink_to("/foo") assert await Path(path).is_symlink() @pytest.mark.parametrize("arg, result", [("/xyz/abc", True), ("/xyz/baz", False)]) def test_is_relative_to(self, arg: str, result: bool) -> None: assert Path("/xyz/abc/foo").is_relative_to(arg) == result async def test_glob(self, populated_tmpdir: pathlib.Path) -> None: all_paths = [] async for path in Path(populated_tmpdir).glob("**/*.txt"): assert isinstance(path, Path) all_paths.append(path.relative_to(populated_tmpdir)) all_paths.sort() assert all_paths == [ Path("sibdir") / "dummyfile1.txt", Path("sibdir") / "dummyfile2.txt", Path("subdir") / "dummyfile1.txt", Path("subdir") / "dummyfile2.txt", ] async def test_rglob(self, populated_tmpdir: pathlib.Path) -> None: all_paths = [] async for path in Path(populated_tmpdir).rglob("*.txt"): assert isinstance(path, Path) all_paths.append(path.relative_to(populated_tmpdir)) all_paths.sort() assert all_paths == [ Path("sibdir") / "dummyfile1.txt", Path("sibdir") / "dummyfile2.txt", Path("subdir") / "dummyfile1.txt", Path("subdir") / "dummyfile2.txt", ] async def test_iterdir(self, populated_tmpdir: pathlib.Path) -> None: all_paths = [] async for path in Path(populated_tmpdir).iterdir(): assert isinstance(path, Path) all_paths.append(path.name) all_paths.sort() assert all_paths == ["sibdir", "subdir", "testfile", "testfile2"] def test_joinpath(self) -> None: path = Path("/foo").joinpath("bar") assert path == Path("/foo/bar") def test_match(self) -> None: assert Path("/foo/bar").match("/foo/*") assert not Path("/foo/bar").match("/baz/*") @pytest.mark.skipif( platform.system() == "Windows", reason="chmod() is not available on Windows" ) async def test_chmod(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" path.touch(0o666) await Path(path).chmod(0o444) assert path.stat().st_mode & 0o777 == 0o444 @pytest.mark.skipif( platform.system() == "Windows", reason="hard links are not supported on Windows" ) async def test_hardlink_to(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" target = tmp_path / "link" target.touch() await Path(path).hardlink_to(Path(target)) assert path.stat().st_nlink == 2 assert target.stat().st_nlink == 2 @pytest.mark.skipif( not hasattr(os, "lchmod"), reason="os.lchmod() is not available" ) async def test_lchmod(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" path.symlink_to("/foo/bar/baz") await Path(path).lchmod(0o600) assert path.lstat().st_mode & 0o777 == 0o600 @pytest.mark.skipif( platform.system() == "Windows", reason="symbolic links are not supported on Windows", ) async def test_lstat(self, tmp_path: pathlib.Path) -> None: path = tmp_path.joinpath("testfile") path.symlink_to("/foo/bar/baz") result = await Path(path).lstat() assert isinstance(result, os.stat_result) @pytest.mark.skipif( platform.system() == "Windows", reason="owner and group are not supported on Windows", ) async def test_group(self, tmp_path: pathlib.Path) -> None: import grp group_name = grp.getgrgid(os.getegid()).gr_name assert await Path(tmp_path).group() == group_name async def test_mkdir(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testdir" await Path(path).mkdir() assert path.is_dir() async def test_open(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" path.write_bytes(b"bibbitibobbitiboo") fp = await Path(path).open("rb") assert isinstance(fp, AsyncFile) assert fp.name == str(path) await fp.aclose() @pytest.mark.skipif( platform.system() == "Windows", reason="owner and group are not supported on Windows", ) async def test_owner(self, tmp_path: pathlib.Path) -> None: import pwd user_name = pwd.getpwuid(os.geteuid()).pw_name assert await Path(tmp_path).owner() == user_name @pytest.mark.skipif( platform.system() == "Windows", reason="symbolic links are not supported on Windows", ) async def test_readlink(self, tmp_path: pathlib.Path) -> None: path = tmp_path.joinpath("testfile") path.symlink_to("/foo/bar/baz") link_target = await Path(path).readlink() assert isinstance(link_target, Path) assert str(link_target) == "/foo/bar/baz" async def test_read_bytes(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" path.write_bytes(b"bibbitibobbitiboo") assert await Path(path).read_bytes() == b"bibbitibobbitiboo" async def test_read_text(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" path.write_text("some text åäö", encoding="utf-8") assert await Path(path).read_text(encoding="utf-8") == "some text åäö" async def test_relative_to_subpath(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "subdir" assert path.relative_to(tmp_path) == Path("subdir") @pytest.mark.skipif( sys.version_info < (3, 12), reason="Path.relative_to(walk_up=) is only available on Python 3.12+", ) async def test_relative_to_sibling( self, populated_tmpdir: pathlib.Path, monkeypatch: pytest.MonkeyPatch, ) -> None: subdir = Path(populated_tmpdir / "subdir") sibdir = Path(populated_tmpdir / "sibdir") with pytest.raises(ValueError): subdir.relative_to(sibdir, walk_up=False) monkeypatch.chdir(sibdir) relpath = subdir.relative_to(sibdir, walk_up=True) / "dummyfile1.txt" assert os.access(relpath, os.R_OK) async def test_rename(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "somefile" path.touch() target = tmp_path / "anotherfile" result = await Path(path).rename(Path(target)) assert isinstance(result, Path) assert result == target async def test_replace(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "somefile" path.write_text("hello") target = tmp_path / "anotherfile" target.write_text("world") result = await Path(path).replace(Path(target)) assert isinstance(result, Path) assert result == target assert target.read_text() == "hello" async def test_resolve(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "somedir" / ".." / "somefile" result = await Path(path).resolve() assert result == tmp_path / "somefile" async def test_rmdir(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "somedir" path.mkdir() await Path(path).rmdir() assert not path.exists() async def test_samefile(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "somefile" path.touch() assert await Path(tmp_path / "somefile").samefile(Path(path)) async def test_stat(self, tmp_path: pathlib.Path) -> None: result = await Path(tmp_path).stat() assert isinstance(result, os.stat_result) async def test_touch(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" await Path(path).touch() assert path.is_file() @pytest.mark.skipif( platform.system() == "Windows", reason="symbolic links are not supported on Windows", ) async def test_symlink_to(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" target = tmp_path / "link" await Path(path).symlink_to(Path(target)) assert path.is_symlink() async def test_unlink(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" path.touch() await Path(path).unlink() assert not path.exists() async def test_unlink_missing_file(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" await Path(path).unlink(missing_ok=True) with pytest.raises(FileNotFoundError): await Path(path).unlink(missing_ok=False) @pytest.mark.skipif( sys.version_info < (3, 12), reason="Path.walk() is only available on Python 3.12+", ) async def test_walk(self, tmp_path: pathlib.Path) -> None: subdir = tmp_path / "subdir" subdir.mkdir() subdir.joinpath("file1").touch() subdir.joinpath("file2").touch() path = Path(tmp_path) iterator = Path(tmp_path).walk().__aiter__() root, dirs, files = await iterator.__anext__() assert root == path assert dirs == ["subdir"] assert files == [] root, dirs, files = await iterator.__anext__() assert root == path / "subdir" assert dirs == [] assert sorted(files) == ["file1", "file2"] with pytest.raises(StopAsyncIteration): await iterator.__anext__() def test_with_name(self) -> None: assert Path("/xyz/foo.txt").with_name("bar").name == "bar" def test_with_stem(self) -> None: assert Path("/xyz/foo.txt").with_stem("bar").name == "bar.txt" def test_with_suffix(self) -> None: assert Path("/xyz/foo.txt.gz").with_suffix(".zip").name == "foo.txt.zip" async def test_write_bytes(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" await Path(path).write_bytes(b"bibbitibobbitiboo") assert path.read_bytes() == b"bibbitibobbitiboo" async def test_write_text(self, tmp_path: pathlib.Path) -> None: path = tmp_path / "testfile" await Path(path).write_text("some text åäö", encoding="utf-8") assert path.read_text(encoding="utf-8") == "some text åäö" anyio-4.4.0/tests/test_from_thread.py000066400000000000000000000550061462472665400177310ustar00rootroot00000000000000from __future__ import annotations import math import sys import threading import time from collections.abc import Awaitable, Callable from concurrent import futures from concurrent.futures import CancelledError, Future from contextlib import asynccontextmanager, suppress from contextvars import ContextVar from typing import Any, AsyncGenerator, Literal, NoReturn, TypeVar import pytest import sniffio from _pytest.logging import LogCaptureFixture from anyio import ( CancelScope, Event, create_task_group, fail_after, from_thread, get_all_backends, get_cancelled_exc_class, get_current_task, run, sleep, to_thread, wait_all_tasks_blocked, ) from anyio.abc import TaskStatus from anyio.from_thread import BlockingPortal, start_blocking_portal from anyio.lowlevel import checkpoint if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup, ExceptionGroup pytestmark = pytest.mark.anyio T_Retval = TypeVar("T_Retval") async def async_add(a: int, b: int) -> int: assert threading.current_thread() is threading.main_thread() return a + b async def asyncgen_add(a: int, b: int) -> AsyncGenerator[int, Any]: yield a + b def sync_add(a: int, b: int) -> int: assert threading.current_thread() is threading.main_thread() return a + b def thread_worker_async( func: Callable[..., Awaitable[T_Retval]], *args: Any ) -> T_Retval: assert threading.current_thread() is not threading.main_thread() return from_thread.run(func, *args) def thread_worker_sync(func: Callable[..., T_Retval], *args: Any) -> T_Retval: assert threading.current_thread() is not threading.main_thread() return from_thread.run_sync(func, *args) @pytest.mark.parametrize("cancel", [True, False]) async def test_thread_cancelled(cancel: bool) -> None: event = threading.Event() thread_finished_future: Future[None] = Future() def sync_function() -> None: event.wait(3) try: from_thread.check_cancelled() except BaseException as exc: thread_finished_future.set_exception(exc) else: thread_finished_future.set_result(None) async with create_task_group() as tg: tg.start_soon(to_thread.run_sync, sync_function) await wait_all_tasks_blocked() if cancel: tg.cancel_scope.cancel() event.set() if cancel: with pytest.raises(get_cancelled_exc_class()): thread_finished_future.result(3) else: thread_finished_future.result(3) async def test_thread_cancelled_and_abandoned() -> None: event = threading.Event() thread_finished_future: Future[None] = Future() def sync_function() -> None: event.wait(3) try: from_thread.check_cancelled() except BaseException as exc: thread_finished_future.set_exception(exc) else: thread_finished_future.set_result(None) async with create_task_group() as tg: tg.start_soon(lambda: to_thread.run_sync(sync_function, abandon_on_cancel=True)) await wait_all_tasks_blocked() tg.cancel_scope.cancel() event.set() with pytest.raises(get_cancelled_exc_class()): thread_finished_future.result(3) async def test_cancelscope_propagation() -> None: async def async_time_bomb() -> None: cancel_scope.cancel() with fail_after(1): await sleep(3) with CancelScope() as cancel_scope: await to_thread.run_sync(from_thread.run, async_time_bomb) assert cancel_scope.cancelled_caught async def test_cancelscope_propagation_when_abandoned() -> None: host_cancelled_event = Event() completed_event = Event() async def async_time_bomb() -> None: cancel_scope.cancel() with fail_after(3): await host_cancelled_event.wait() completed_event.set() with CancelScope() as cancel_scope: await to_thread.run_sync( from_thread.run, async_time_bomb, abandon_on_cancel=True ) assert cancel_scope.cancelled_caught host_cancelled_event.set() with fail_after(3): await completed_event.wait() class TestRunAsyncFromThread: async def test_run_corofunc_from_thread(self) -> None: result = await to_thread.run_sync(thread_worker_async, async_add, 1, 2) assert result == 3 async def test_run_asyncgen_from_thread(self) -> None: gen = asyncgen_add(1, 2) try: result = await to_thread.run_sync(thread_worker_async, gen.__anext__) assert result == 3 finally: await gen.aclose() async def test_run_sync_from_thread(self) -> None: result = await to_thread.run_sync(thread_worker_sync, sync_add, 1, 2) assert result == 3 def test_run_sync_from_thread_pooling(self) -> None: async def main() -> None: thread_ids = set() for _ in range(5): thread_ids.add(await to_thread.run_sync(threading.get_ident)) # Expects that all the work has been done in the same worker thread assert len(thread_ids) == 1 assert thread_ids.pop() != threading.get_ident() assert threading.active_count() == initial_count + 1 # The thread should not exist after the event loop has been closed initial_count = threading.active_count() run(main, backend="asyncio") for _ in range(10): if threading.active_count() == initial_count: return time.sleep(0.1) pytest.fail("Worker thread did not exit within 1 second") async def test_run_async_from_thread_exception(self) -> None: with pytest.raises(TypeError) as exc: await to_thread.run_sync(thread_worker_async, async_add, 1, "foo") exc.match("unsupported operand type") async def test_run_sync_from_thread_exception(self) -> None: with pytest.raises(TypeError) as exc: await to_thread.run_sync(thread_worker_sync, sync_add, 1, "foo") exc.match("unsupported operand type") async def test_run_anyio_async_func_from_thread(self) -> None: def worker(delay: float) -> Literal[True]: from_thread.run(sleep, delay) return True assert await to_thread.run_sync(worker, 0) def test_run_async_from_unclaimed_thread(self) -> None: async def foo() -> None: pass exc = pytest.raises(RuntimeError, from_thread.run, foo) exc.match("This function can only be run from an AnyIO worker thread") async def test_contextvar_propagation(self, anyio_backend_name: str) -> None: var = ContextVar("var", default=1) async def async_func() -> int: await checkpoint() return var.get() def worker() -> int: var.set(6) return from_thread.run(async_func) assert await to_thread.run_sync(worker) == 6 async def test_sniffio(self, anyio_backend_name: str) -> None: async def async_func() -> str: return sniffio.current_async_library() def worker() -> str: sniffio.current_async_library_cvar.set("something invalid for async_func") return from_thread.run(async_func) assert await to_thread.run_sync(worker) == anyio_backend_name class TestRunSyncFromThread: def test_run_sync_from_unclaimed_thread(self) -> None: def foo() -> None: pass exc = pytest.raises(RuntimeError, from_thread.run_sync, foo) exc.match("This function can only be run from an AnyIO worker thread") async def test_contextvar_propagation(self) -> None: var = ContextVar("var", default=1) def worker() -> int: var.set(6) return from_thread.run_sync(var.get) assert await to_thread.run_sync(worker) == 6 async def test_sniffio(self, anyio_backend_name: str) -> None: def worker() -> str: sniffio.current_async_library_cvar.set("something invalid for async_func") return from_thread.run_sync(sniffio.current_async_library) assert await to_thread.run_sync(worker) == anyio_backend_name class TestBlockingPortal: class AsyncCM: def __init__(self, ignore_error: bool): self.ignore_error = ignore_error async def __aenter__(self) -> Literal["test"]: return "test" async def __aexit__( self, exc_type: object, exc_val: object, exc_tb: object ) -> bool: return self.ignore_error async def test_call_corofunc(self) -> None: async with BlockingPortal() as portal: result = await to_thread.run_sync(portal.call, async_add, 1, 2) assert result == 3 async def test_call_anext(self) -> None: gen = asyncgen_add(1, 2) try: async with BlockingPortal() as portal: result = await to_thread.run_sync(portal.call, gen.__anext__) assert result == 3 finally: await gen.aclose() async def test_aexit_with_exception(self) -> None: """ Test that when the portal exits with an exception, all tasks are cancelled. """ def external_thread() -> None: try: portal.call(sleep, 3) except BaseException as exc: results.append(exc) else: results.append(None) results: list[BaseException | None] = [] with suppress(Exception): async with BlockingPortal() as portal: thread1 = threading.Thread(target=external_thread) thread1.start() thread2 = threading.Thread(target=external_thread) thread2.start() await sleep(0.1) assert not results raise Exception await to_thread.run_sync(thread1.join) await to_thread.run_sync(thread2.join) assert len(results) == 2 assert isinstance(results[0], CancelledError) assert isinstance(results[1], CancelledError) async def test_aexit_without_exception(self) -> None: """Test that when the portal exits, it waits for all tasks to finish.""" def external_thread() -> None: try: portal.call(sleep, 0.2) except BaseException as exc: results.append(exc) else: results.append(None) results: list[BaseException | None] = [] async with BlockingPortal() as portal: thread1 = threading.Thread(target=external_thread) thread1.start() thread2 = threading.Thread(target=external_thread) thread2.start() await sleep(0.1) assert not results await to_thread.run_sync(thread1.join) await to_thread.run_sync(thread2.join) assert results == [None, None] async def test_call_portal_from_event_loop_thread(self) -> None: async with BlockingPortal() as portal: exc = pytest.raises(RuntimeError, portal.call, threading.get_ident) exc.match("This method cannot be called from the event loop thread") def test_start_with_new_event_loop( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def async_get_thread_id() -> int: return threading.get_ident() with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: thread_id = portal.call(async_get_thread_id) assert isinstance(thread_id, int) assert thread_id != threading.get_ident() def test_start_with_nonexistent_backend(self) -> None: with pytest.raises(LookupError) as exc: with start_blocking_portal("foo"): pass exc.match("No such backend: foo") def test_call_stopped_portal( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: pass pytest.raises(RuntimeError, portal.call, threading.get_ident).match( "This portal is not running" ) def test_start_task_soon( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def event_waiter() -> Literal["test"]: await event1.wait() event2.set() return "test" with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: event1 = portal.call(Event) event2 = portal.call(Event) future = portal.start_task_soon(event_waiter) portal.call(event1.set) portal.call(event2.wait) assert future.result() == "test" def test_start_task_soon_cancel_later( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def noop() -> None: await sleep(2) with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: future = portal.start_task_soon(noop) portal.call(wait_all_tasks_blocked) future.cancel() assert future.cancelled() def test_start_task_soon_cancel_immediately( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: cancelled = False async def event_waiter() -> None: nonlocal cancelled try: await sleep(3) except get_cancelled_exc_class(): cancelled = True with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: future = portal.start_task_soon(event_waiter) future.cancel() assert cancelled def test_start_task_soon_with_name( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: task_name = None async def taskfunc() -> None: nonlocal task_name task_name = get_current_task().name with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: portal.start_task_soon(taskfunc, name="testname") assert task_name == "testname" def test_async_context_manager_success( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: with portal.wrap_async_context_manager( TestBlockingPortal.AsyncCM(False) ) as cm: assert cm == "test" def test_async_context_manager_error( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: with pytest.raises(Exception) as exc: with portal.wrap_async_context_manager( TestBlockingPortal.AsyncCM(False) ) as cm: assert cm == "test" raise Exception("should NOT be ignored") exc.match("should NOT be ignored") def test_async_context_manager_error_ignore( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: with portal.wrap_async_context_manager( TestBlockingPortal.AsyncCM(True) ) as cm: assert cm == "test" raise Exception("should be ignored") def test_async_context_manager_exception_in_task_group( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: """Regression test for #381.""" async def failing_func() -> None: 0 / 0 @asynccontextmanager async def run_in_context() -> AsyncGenerator[None, None]: async with create_task_group() as tg: tg.start_soon(failing_func) yield with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: with pytest.raises(ExceptionGroup) as exc: with portal.wrap_async_context_manager(run_in_context()): pass assert len(exc.value.exceptions) == 1 assert isinstance(exc.value.exceptions[0], ZeroDivisionError) def test_start_no_value( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def taskfunc(*, task_status: TaskStatus) -> None: task_status.started() with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: future, value = portal.start_task(taskfunc) assert value is None assert future.result() is None def test_start_with_value( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def taskfunc(*, task_status: TaskStatus) -> None: task_status.started("foo") with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: future, value = portal.start_task(taskfunc) assert value == "foo" assert future.result() is None def test_start_crash_before_started_call( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def taskfunc(*, task_status: object) -> NoReturn: raise Exception("foo") with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: with pytest.raises(Exception, match="foo"): portal.start_task(taskfunc) def test_start_crash_after_started_call( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def taskfunc(*, task_status: TaskStatus) -> NoReturn: task_status.started(2) raise Exception("foo") with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: future, value = portal.start_task(taskfunc) assert value == 2 with pytest.raises(Exception, match="foo"): future.result() def test_start_no_started_call( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def taskfunc(*, task_status: TaskStatus) -> None: pass with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: with pytest.raises(RuntimeError, match="Task exited"): portal.start_task(taskfunc) def test_start_with_name( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def taskfunc(*, task_status: TaskStatus) -> None: task_status.started(get_current_task().name) with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: future, start_value = portal.start_task(taskfunc, name="testname") assert start_value == "testname" def test_contextvar_propagation_sync( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: var = ContextVar("var", default=1) var.set(6) with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: propagated_value = portal.call(var.get) assert propagated_value == 6 def test_contextvar_propagation_async( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: var = ContextVar("var", default=1) var.set(6) async def get_var() -> int: await checkpoint() return var.get() with start_blocking_portal(anyio_backend_name, anyio_backend_options) as portal: propagated_value = portal.call(get_var) assert propagated_value == 6 @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_asyncio_run_sync_called(self, caplog: LogCaptureFixture) -> None: """Regression test for #357.""" async def in_loop() -> None: raise CancelledError async with BlockingPortal() as portal: await to_thread.run_sync(portal.start_task_soon, in_loop) assert not caplog.text def test_raise_baseexception_from_task( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: """ Test that when a task raises a BaseException, it does not trigger additional exceptions when trying to close the portal. """ async def raise_baseexception() -> None: raise BaseException("fatal error") with pytest.raises(BaseExceptionGroup) as outer_exc: with start_blocking_portal( anyio_backend_name, anyio_backend_options ) as portal: with pytest.raises(BaseException, match="fatal error") as exc: portal.call(raise_baseexception) assert exc.value.__context__ is None assert len(outer_exc.value.exceptions) == 1 assert str(outer_exc.value.exceptions[0]) == "fatal error" @pytest.mark.parametrize("portal_backend_name", get_all_backends()) async def test_from_async( self, anyio_backend_name: str, portal_backend_name: str ) -> None: """ Test that portals don't deadlock when started/used from async code. Note: This test will deadlock if there is a regression. A deadlock should be treated as a failure. See also https://github.com/agronholm/anyio/pull/524#discussion_r1183080886. """ if anyio_backend_name == "trio" and portal_backend_name == "trio": pytest.xfail("known bug (#525)") with start_blocking_portal(portal_backend_name) as portal: portal.call(checkpoint) async def test_cancel_portal_future(self) -> None: """Regression test for #575.""" event = Event() def sync_thread() -> None: fs = [portal.start_task_soon(sleep, math.inf)] from_thread.run_sync(event.set) done, not_done = futures.wait( fs, timeout=1, return_when=futures.FIRST_COMPLETED ) assert not not_done async with from_thread.BlockingPortal() as portal: async with create_task_group() as tg: tg.start_soon(to_thread.run_sync, sync_thread) # Ensure thread has time to start the task await event.wait() await portal.stop(cancel_remaining=True) anyio-4.4.0/tests/test_lowlevel.py000066400000000000000000000065011462472665400172640ustar00rootroot00000000000000from __future__ import annotations from typing import Any import pytest from anyio import create_task_group, run from anyio.lowlevel import ( RunVar, cancel_shielded_checkpoint, checkpoint, checkpoint_if_cancelled, ) pytestmark = pytest.mark.anyio @pytest.mark.parametrize("cancel", [False, True]) async def test_checkpoint_if_cancelled(cancel: bool) -> None: finished = second_finished = False async def func() -> None: nonlocal finished tg.start_soon(second_func) if cancel: tg.cancel_scope.cancel() await checkpoint_if_cancelled() finished = True async def second_func() -> None: nonlocal second_finished assert finished != cancel second_finished = True async with create_task_group() as tg: tg.start_soon(func) assert finished != cancel assert second_finished @pytest.mark.parametrize("cancel", [False, True]) async def test_cancel_shielded_checkpoint(cancel: bool) -> None: finished = second_finished = False async def func() -> None: nonlocal finished await cancel_shielded_checkpoint() finished = True async def second_func() -> None: nonlocal second_finished assert not finished second_finished = True async with create_task_group() as tg: tg.start_soon(func) tg.start_soon(second_func) if cancel: tg.cancel_scope.cancel() assert finished assert second_finished @pytest.mark.parametrize("cancel", [False, True]) async def test_checkpoint(cancel: bool) -> None: finished = second_finished = False async def func() -> None: nonlocal finished await checkpoint() finished = True async def second_func() -> None: nonlocal second_finished assert not finished second_finished = True async with create_task_group() as tg: tg.start_soon(func) tg.start_soon(second_func) if cancel: tg.cancel_scope.cancel() assert finished != cancel assert second_finished class TestRunVar: def test_get_set( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any], ) -> None: async def taskfunc(index: int) -> None: assert var.get() == index var.set(index + 1) async def main() -> None: pytest.raises(LookupError, var.get) for i in range(2): var.set(i) async with create_task_group() as tg: tg.start_soon(taskfunc, i) assert var.get() == i + 1 var = RunVar[int]("var") for _ in range(2): run(main, backend=anyio_backend_name, backend_options=anyio_backend_options) async def test_reset_token_used_on_wrong_runvar(self) -> None: var1 = RunVar[str]("var1") var2 = RunVar[str]("var2") token = var1.set("blah") with pytest.raises( ValueError, match="This token does not belong to this RunVar" ): var2.reset(token) async def test_reset_token_used_twice(self) -> None: var = RunVar[str]("var") token = var.set("blah") var.reset(token) with pytest.raises(ValueError, match="This token has already been used"): var.reset(token) anyio-4.4.0/tests/test_pytest_plugin.py000066400000000000000000000256401462472665400203460ustar00rootroot00000000000000from __future__ import annotations import pytest from _pytest.logging import LogCaptureFixture from _pytest.pytester import Pytester from anyio import get_all_backends pytestmark = pytest.mark.filterwarnings( "ignore:The TerminalReporter.writer attribute is deprecated" ":pytest.PytestDeprecationWarning:" ) pytest_args = "-v", "-p", "anyio", "-p", "no:asyncio", "-p", "no:trio" def test_plugin(testdir: Pytester) -> None: testdir.makeconftest( """ from contextvars import ContextVar import sniffio import pytest from anyio import sleep var = ContextVar("var") @pytest.fixture async def async_fixture(): await sleep(0) return sniffio.current_async_library() @pytest.fixture async def context_variable(): token = var.set("testvalue") yield var var.reset(token) @pytest.fixture async def some_feature(): yield None await sleep(0) """ ) testdir.makepyfile( """ import pytest import sniffio from hypothesis import strategies, given from anyio import get_all_backends, sleep @pytest.mark.anyio async def test_marked_test() -> None: # Test that tests marked with @pytest.mark.anyio are run pass @pytest.mark.anyio async def test_async_fixture_from_marked_test(async_fixture): # Test that async functions can use async fixtures assert async_fixture in get_all_backends() def test_async_fixture_from_sync_test(anyio_backend_name, async_fixture): # Test that regular functions can use async fixtures too assert async_fixture == anyio_backend_name @pytest.mark.anyio async def test_skip_inline(some_feature): # Test for github #214 pytest.skip("Test that skipping works") @pytest.mark.anyio async def test_contextvar(context_variable): # Test that a contextvar set in an async fixture is visible to the test assert context_variable.get() == "testvalue" """ ) result = testdir.runpytest(*pytest_args) result.assert_outcomes( passed=4 * len(get_all_backends()), skipped=len(get_all_backends()) ) def test_asyncio(testdir: Pytester, caplog: LogCaptureFixture) -> None: testdir.makeconftest( """ import asyncio import pytest import threading @pytest.fixture(scope='class') def anyio_backend(): return 'asyncio' @pytest.fixture async def setup_fail_fixture(): def callback(): raise RuntimeError('failing fixture setup') asyncio.get_running_loop().call_soon(callback) await asyncio.sleep(0) yield None @pytest.fixture async def teardown_fail_fixture(): def callback(): raise RuntimeError('failing fixture teardown') yield None asyncio.get_running_loop().call_soon(callback) await asyncio.sleep(0) @pytest.fixture def no_thread_leaks_fixture(): # this has to be non-async fixture so that it wraps up # after the event loop gets closed threads_before = threading.enumerate() yield threads_after = threading.enumerate() leaked_threads = set(threads_after) - set(threads_before) assert not leaked_threads """ ) testdir.makepyfile( """ import asyncio import pytest pytestmark = pytest.mark.anyio class TestClassFixtures: @pytest.fixture(scope='class') async def async_class_fixture(self, anyio_backend): await asyncio.sleep(0) return anyio_backend def test_class_fixture_in_test_method( self, async_class_fixture, anyio_backend_name ): assert anyio_backend_name == 'asyncio' assert async_class_fixture == 'asyncio' async def test_callback_exception_during_test() -> None: def callback(): nonlocal started started = True raise Exception('foo') started = False asyncio.get_running_loop().call_soon(callback) await asyncio.sleep(0) assert started async def test_callback_exception_during_setup(setup_fail_fixture): pass async def test_callback_exception_during_teardown(teardown_fail_fixture): pass async def test_exception_handler_no_exception(): asyncio.get_event_loop().call_exception_handler( {"message": "bogus error"} ) await asyncio.sleep(0.1) async def test_shutdown_default_executor(no_thread_leaks_fixture): # Test for github #503 asyncio.get_event_loop().run_in_executor(None, lambda: 1) """ ) result = testdir.runpytest(*pytest_args) result.assert_outcomes(passed=4, failed=1, errors=2) assert len(caplog.messages) == 1 assert caplog.messages[0] == "bogus error" def test_autouse_async_fixture(testdir: Pytester) -> None: testdir.makeconftest( """ import pytest autouse_backend = None @pytest.fixture(autouse=True) async def autouse_async_fixture(anyio_backend_name): global autouse_backend autouse_backend = anyio_backend_name @pytest.fixture def autouse_backend_name(): return autouse_backend """ ) testdir.makepyfile( """ import pytest import sniffio from anyio import get_all_backends, sleep def test_autouse_backend(autouse_backend_name): # Test that async autouse fixtures are triggered assert autouse_backend_name in get_all_backends() """ ) result = testdir.runpytest_subprocess(*pytest_args) result.assert_outcomes(passed=len(get_all_backends())) def test_cancel_scope_in_asyncgen_fixture(testdir: Pytester) -> None: testdir.makepyfile( """ import pytest from anyio import create_task_group, sleep @pytest.fixture async def asyncgen_fixture(): async with create_task_group() as tg: tg.cancel_scope.cancel() await sleep(1) yield 1 @pytest.mark.anyio async def test_cancel_in_asyncgen_fixture(asyncgen_fixture): assert asyncgen_fixture == 1 """ ) result = testdir.runpytest_subprocess(*pytest_args) result.assert_outcomes(passed=len(get_all_backends())) def test_module_scoped_task_group_fixture(testdir: Pytester) -> None: testdir.makeconftest( """ import pytest from anyio import ( CancelScope, create_memory_object_stream, create_task_group, get_all_backends, ) @pytest.fixture(scope="module", params=get_all_backends()) def anyio_backend(): return 'asyncio' @pytest.fixture(scope="module") async def task_group(): async with create_task_group() as tg: yield tg @pytest.fixture async def streams(task_group): async def echo_messages(*, task_status): with CancelScope() as cancel_scope: task_status.started(cancel_scope) async for obj in receive1: await send2.send(obj) send1, receive1 = create_memory_object_stream() send2, receive2 = create_memory_object_stream() cancel_scope = await task_group.start(echo_messages) yield send1, receive2 cancel_scope.cancel() """ ) testdir.makepyfile( """ import pytest @pytest.mark.anyio async def test_task_group(streams): send1, receive2 = streams await send1.send("hello") assert await receive2.receive() == "hello" """ ) result = testdir.runpytest_subprocess(*pytest_args) result.assert_outcomes(passed=len(get_all_backends())) def test_async_fixture_teardown_after_sync_test(testdir: Pytester) -> None: # Regression test for #619 testdir.makepyfile( """ import pytest from anyio import create_task_group, sleep @pytest.fixture(scope="session") def anyio_backend(): return "asyncio" @pytest.fixture(scope="module") async def bbbbbb(): yield "" @pytest.fixture(scope="module") async def aaaaaa(): yield "" @pytest.mark.anyio async def test_1(bbbbbb): pass @pytest.mark.anyio async def test_2(aaaaaa, bbbbbb): pass """ ) result = testdir.runpytest_subprocess(*pytest_args) result.assert_outcomes(passed=2) def test_hypothesis_module_mark(testdir: Pytester) -> None: testdir.makepyfile( """ import pytest from hypothesis import given from hypothesis.strategies import just pytestmark = pytest.mark.anyio @given(x=just(1)) async def test_hypothesis_wrapper(x): assert isinstance(x, int) @given(x=just(1)) def test_hypothesis_wrapper_regular(x): assert isinstance(x, int) @pytest.mark.xfail(strict=True) @given(x=just(1)) async def test_hypothesis_wrapper_failing(x): pytest.fail('This test failed successfully') """ ) result = testdir.runpytest(*pytest_args) result.assert_outcomes( passed=len(get_all_backends()) + 1, xfailed=len(get_all_backends()) ) def test_hypothesis_function_mark(testdir: Pytester) -> None: testdir.makepyfile( """ import pytest from hypothesis import given from hypothesis.strategies import just @pytest.mark.anyio @given(x=just(1)) async def test_anyio_mark_first(x): assert isinstance(x, int) @given(x=just(1)) @pytest.mark.anyio async def test_anyio_mark_last(x): assert isinstance(x, int) @pytest.mark.xfail(strict=True) @pytest.mark.anyio @given(x=just(1)) async def test_anyio_mark_first_fail(x): pytest.fail('This test failed successfully') @given(x=just(1)) @pytest.mark.xfail(strict=True) @pytest.mark.anyio async def test_anyio_mark_last_fail(x): pytest.fail('This test failed successfully') """ ) result = testdir.runpytest(*pytest_args) result.assert_outcomes( passed=2 * len(get_all_backends()), xfailed=2 * len(get_all_backends()) ) anyio-4.4.0/tests/test_signals.py000066400000000000000000000036171462472665400171000ustar00rootroot00000000000000from __future__ import annotations import os import signal import sys from typing import AsyncIterable import pytest from anyio import create_task_group, fail_after, open_signal_receiver, to_thread pytestmark = [ pytest.mark.anyio, pytest.mark.skipif( sys.platform == "win32", reason="Signal delivery cannot be tested on Windows", ), ] async def test_receive_signals() -> None: with open_signal_receiver(signal.SIGUSR1, signal.SIGUSR2) as sigiter: await to_thread.run_sync(os.kill, os.getpid(), signal.SIGUSR1) await to_thread.run_sync(os.kill, os.getpid(), signal.SIGUSR2) with fail_after(1): sigusr1 = await sigiter.__anext__() assert isinstance(sigusr1, signal.Signals) assert sigusr1 == signal.Signals.SIGUSR1 sigusr2 = await sigiter.__anext__() assert isinstance(sigusr2, signal.Signals) assert sigusr2 == signal.Signals.SIGUSR2 async def test_task_group_cancellation_open() -> None: async def signal_handler() -> None: with open_signal_receiver(signal.SIGUSR1) as sigiter: async for v in sigiter: pytest.fail("SIGUSR1 should not be sent") pytest.fail("signal_handler should have been cancelled") pytest.fail("open_signal_receiver should not suppress cancellation") async with create_task_group() as tg: tg.start_soon(signal_handler) tg.cancel_scope.cancel() async def test_task_group_cancellation_consume() -> None: async def consume(sigiter: AsyncIterable[int]) -> None: async for v in sigiter: pytest.fail("SIGUSR1 should not be sent") pytest.fail("consume should have been cancelled") with open_signal_receiver(signal.SIGUSR1) as sigiter: async with create_task_group() as tg: tg.start_soon(consume, sigiter) tg.cancel_scope.cancel() anyio-4.4.0/tests/test_sockets.py000066400000000000000000002020051462472665400171030ustar00rootroot00000000000000from __future__ import annotations import array import gc import io import os import platform import socket import sys import tempfile import threading import time from contextlib import suppress from pathlib import Path from socket import AddressFamily from ssl import SSLContext, SSLError from threading import Thread from typing import Any, Generator, Iterable, Iterator, NoReturn, TypeVar, cast import psutil import pytest from _pytest.fixtures import SubRequest from _pytest.logging import LogCaptureFixture from _pytest.monkeypatch import MonkeyPatch from _pytest.tmpdir import TempPathFactory from anyio import ( BrokenResourceError, BusyResourceError, ClosedResourceError, EndOfStream, Event, TypedAttributeLookupError, connect_tcp, connect_unix, create_connected_udp_socket, create_connected_unix_datagram_socket, create_task_group, create_tcp_listener, create_udp_socket, create_unix_datagram_socket, create_unix_listener, fail_after, getaddrinfo, getnameinfo, move_on_after, sleep, wait_all_tasks_blocked, ) from anyio.abc import ( IPSockAddrType, Listener, SocketAttribute, SocketListener, SocketStream, ) from anyio.streams.stapled import MultiListener if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup from typing import Literal AnyIPAddressFamily = Literal[ AddressFamily.AF_UNSPEC, AddressFamily.AF_INET, AddressFamily.AF_INET6 ] pytestmark = pytest.mark.anyio # If a socket can bind to ::1, the current environment has IPv6 properly configured has_ipv6 = False if socket.has_ipv6: try: s = socket.socket(AddressFamily.AF_INET6) try: s.bind(("::1", 0)) finally: s.close() del s except OSError: pass else: has_ipv6 = True skip_ipv6_mark = pytest.mark.skipif(not has_ipv6, reason="IPv6 is not available") @pytest.fixture def fake_localhost_dns(monkeypatch: MonkeyPatch) -> None: def fake_getaddrinfo(*args: Any, **kwargs: Any) -> object: # Make it return IPv4 addresses first so we can test the IPv6 preference results = real_getaddrinfo(*args, **kwargs) return sorted(results, key=lambda item: item[0]) real_getaddrinfo = socket.getaddrinfo monkeypatch.setattr("socket.getaddrinfo", fake_getaddrinfo) @pytest.fixture( params=[ pytest.param(AddressFamily.AF_INET, id="ipv4"), pytest.param(AddressFamily.AF_INET6, id="ipv6", marks=[skip_ipv6_mark]), ] ) def family(request: SubRequest) -> AnyIPAddressFamily: return request.param @pytest.fixture def check_asyncio_bug(anyio_backend_name: str, family: AnyIPAddressFamily) -> None: if ( anyio_backend_name == "asyncio" and sys.platform == "win32" and family == AddressFamily.AF_INET6 ): import asyncio policy = asyncio.get_event_loop_policy() if policy.__class__.__name__ == "WindowsProactorEventLoopPolicy": pytest.skip("Does not work due to a known bug (39148)") _T = TypeVar("_T") def _identity(v: _T) -> _T: return v # _ProactorBasePipeTransport.abort() after _ProactorBasePipeTransport.close() # does not cancel writes: https://bugs.python.org/issue44428 _ignore_win32_resource_warnings = ( pytest.mark.filterwarnings( "ignore:unclosed Iterator[socket.socket]: sock = socket.socket(family, socket.SOCK_STREAM) sock.settimeout(1) sock.bind(("localhost", 0)) sock.listen() yield sock sock.close() @pytest.fixture def server_addr(self, server_sock: socket.socket) -> tuple[str, int]: return server_sock.getsockname()[:2] async def test_extra_attributes( self, server_sock: socket.socket, server_addr: tuple[str, int], family: AnyIPAddressFamily, ) -> None: async with await connect_tcp(*server_addr) as stream: raw_socket = stream.extra(SocketAttribute.raw_socket) assert stream.extra(SocketAttribute.family) == family assert ( stream.extra(SocketAttribute.local_address) == raw_socket.getsockname()[:2] ) assert ( stream.extra(SocketAttribute.local_port) == raw_socket.getsockname()[1] ) assert stream.extra(SocketAttribute.remote_address) == server_addr assert stream.extra(SocketAttribute.remote_port) == server_addr[1] async def test_send_receive( self, server_sock: socket.socket, server_addr: tuple[str, int] ) -> None: async with await connect_tcp(*server_addr) as stream: client, _ = server_sock.accept() await stream.send(b"blah") request = client.recv(100) client.sendall(request[::-1]) response = await stream.receive() client.close() assert response == b"halb" async def test_send_large_buffer( self, server_sock: socket.socket, server_addr: tuple[str, int] ) -> None: def serve() -> None: client, _ = server_sock.accept() client.sendall(buffer) client.close() buffer = ( b"\xff" * 1024 * 1024 ) # should exceed the maximum kernel send buffer size async with await connect_tcp(*server_addr) as stream: thread = Thread(target=serve, daemon=True) thread.start() response = b"" while len(response) < len(buffer): response += await stream.receive() thread.join() assert response == buffer async def test_send_eof( self, server_sock: socket.socket, server_addr: tuple[str, int] ) -> None: def serve() -> None: client, _ = server_sock.accept() request = b"" while True: data = client.recv(100) request += data if not data: break client.sendall(request[::-1]) client.close() async with await connect_tcp(*server_addr) as stream: thread = Thread(target=serve, daemon=True) thread.start() await stream.send(b"hello, ") await stream.send(b"world\n") await stream.send_eof() response = await stream.receive() thread.join() assert response == b"\ndlrow ,olleh" async def test_iterate( self, server_sock: socket.socket, server_addr: tuple[str, int] ) -> None: def serve() -> None: client, _ = server_sock.accept() client.sendall(b"bl") event.wait(1) client.sendall(b"ah") client.close() event = threading.Event() thread = Thread(target=serve, daemon=True) thread.start() chunks = [] async with await connect_tcp(*server_addr) as stream: async for chunk in stream: chunks.append(chunk) event.set() thread.join() assert chunks == [b"bl", b"ah"] async def test_socket_options( self, family: AnyIPAddressFamily, server_addr: tuple[str, int] ) -> None: async with await connect_tcp(*server_addr) as stream: raw_socket = stream.extra(SocketAttribute.raw_socket) assert raw_socket.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY) != 0 @skip_ipv6_mark @pytest.mark.parametrize( "local_addr, expected_client_addr", [ pytest.param("", "::1", id="dualstack"), pytest.param("127.0.0.1", "127.0.0.1", id="ipv4"), pytest.param("::1", "::1", id="ipv6"), ], ) async def test_happy_eyeballs( self, local_addr: str, expected_client_addr: str, fake_localhost_dns: None ) -> None: client_addr = None, None def serve() -> None: nonlocal client_addr client, client_addr = server_sock.accept() client.close() family = ( AddressFamily.AF_INET if local_addr == "127.0.0.1" else AddressFamily.AF_INET6 ) server_sock = socket.socket(family) server_sock.bind((local_addr, 0)) server_sock.listen() port = server_sock.getsockname()[1] thread = Thread(target=serve, daemon=True) thread.start() async with await connect_tcp("localhost", port): pass thread.join() server_sock.close() assert client_addr[0] == expected_client_addr @pytest.mark.parametrize( "target, exception_class", [ pytest.param( "localhost", ExceptionGroup, id="multi", marks=[skip_ipv6_mark] ), pytest.param("127.0.0.1", ConnectionRefusedError, id="single"), ], ) async def test_connection_refused( self, target: str, exception_class: type[ExceptionGroup] | type[ConnectionRefusedError], fake_localhost_dns: None, ) -> None: dummy_socket = socket.socket(AddressFamily.AF_INET6) dummy_socket.bind(("::", 0)) free_port = dummy_socket.getsockname()[1] dummy_socket.close() with pytest.raises(OSError) as exc: await connect_tcp(target, free_port) assert exc.match("All connection attempts failed") assert isinstance(exc.value.__cause__, exception_class) if isinstance(exc.value.__cause__, ExceptionGroup): for exception in exc.value.__cause__.exceptions: assert isinstance(exception, ConnectionRefusedError) async def test_receive_timeout( self, server_sock: socket.socket, server_addr: tuple[str, int] ) -> None: def serve() -> None: conn, _ = server_sock.accept() time.sleep(1) conn.close() thread = Thread(target=serve, daemon=True) thread.start() async with await connect_tcp(*server_addr) as stream: start_time = time.monotonic() with move_on_after(0.1): while time.monotonic() - start_time < 0.3: await stream.receive(1) pytest.fail("The timeout was not respected") async def test_concurrent_send(self, server_addr: tuple[str, int]) -> None: async def send_data() -> NoReturn: while True: await stream.send(b"\x00" * 4096) async with await connect_tcp(*server_addr) as stream: async with create_task_group() as tg: tg.start_soon(send_data) await wait_all_tasks_blocked() with pytest.raises(BusyResourceError) as exc: await stream.send(b"foo") exc.match("already writing to") tg.cancel_scope.cancel() async def test_concurrent_receive(self, server_addr: tuple[str, int]) -> None: async with await connect_tcp(*server_addr) as client: async with create_task_group() as tg: tg.start_soon(client.receive) await wait_all_tasks_blocked() try: with pytest.raises(BusyResourceError) as exc: await client.receive() exc.match("already reading from") finally: tg.cancel_scope.cancel() async def test_close_during_receive(self, server_addr: tuple[str, int]) -> None: async def interrupt() -> None: await wait_all_tasks_blocked() await stream.aclose() async with await connect_tcp(*server_addr) as stream: async with create_task_group() as tg: tg.start_soon(interrupt) with pytest.raises(ClosedResourceError): await stream.receive() async def test_receive_after_close(self, server_addr: tuple[str, int]) -> None: stream = await connect_tcp(*server_addr) await stream.aclose() with pytest.raises(ClosedResourceError): await stream.receive() async def test_send_after_close(self, server_addr: tuple[str, int]) -> None: stream = await connect_tcp(*server_addr) await stream.aclose() with pytest.raises(ClosedResourceError): await stream.send(b"foo") async def test_send_after_peer_closed(self, family: AnyIPAddressFamily) -> None: def serve_once() -> None: client_sock, _ = server_sock.accept() client_sock.close() server_sock.close() server_sock = socket.socket(family, socket.SOCK_STREAM) server_sock.settimeout(1) server_sock.bind(("localhost", 0)) server_addr = server_sock.getsockname()[:2] server_sock.listen() thread = Thread(target=serve_once, daemon=True) thread.start() with pytest.raises(BrokenResourceError): async with await connect_tcp(*server_addr) as stream: for _ in range(1000): await stream.send(b"foo") thread.join() async def test_connect_tcp_with_tls( self, server_context: SSLContext, client_context: SSLContext, server_sock: socket.socket, server_addr: tuple[str, int], ) -> None: def serve() -> None: with suppress(socket.timeout): client, addr = server_sock.accept() client.settimeout(1) client = server_context.wrap_socket(client, server_side=True) data = client.recv(100) client.sendall(data[::-1]) client.unwrap() client.close() # The TLSStream tests are more comprehensive than this one! thread = Thread(target=serve, daemon=True) thread.start() async with await connect_tcp( *server_addr, tls_hostname="localhost", ssl_context=client_context ) as stream: await stream.send(b"hello") response = await stream.receive() assert response == b"olleh" thread.join() async def test_connect_tcp_with_tls_cert_check_fail( self, server_context: SSLContext, server_sock: socket.socket, server_addr: tuple[str, int], ) -> None: thread_exception = None def serve() -> None: nonlocal thread_exception client, addr = server_sock.accept() with client: client.settimeout(1) try: server_context.wrap_socket(client, server_side=True) except OSError: pass except BaseException as exc: thread_exception = exc thread = Thread(target=serve, daemon=True) thread.start() with pytest.raises(SSLError): await connect_tcp(*server_addr, tls_hostname="localhost") thread.join() assert thread_exception is None @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_unretrieved_future_exception_server_crash( self, family: AnyIPAddressFamily, caplog: LogCaptureFixture ) -> None: """ Test that there won't be any leftover Futures that don't get their exceptions retrieved. See https://github.com/encode/httpcore/issues/382 for details. """ def serve() -> None: sock, addr = server_sock.accept() event.wait(3) del sock gc.collect() server_sock = socket.socket(family, socket.SOCK_STREAM) server_sock.settimeout(1) server_sock.bind(("localhost", 0)) server_sock.listen() server_addr = server_sock.getsockname()[:2] event = threading.Event() thread = Thread(target=serve) thread.start() async with await connect_tcp(*server_addr) as stream: await stream.send(b"GET") event.set() with pytest.raises(BrokenResourceError): await stream.receive() thread.join() gc.collect() assert not caplog.text @pytest.mark.network class TestTCPListener: async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None: async with await create_tcp_listener( local_host="localhost", family=family ) as multi: assert multi.extra(SocketAttribute.family) == family for listener in multi.listeners: raw_socket = listener.extra(SocketAttribute.raw_socket) assert listener.extra(SocketAttribute.family) == family assert ( listener.extra(SocketAttribute.local_address) == raw_socket.getsockname()[:2] ) assert ( listener.extra(SocketAttribute.local_port) == raw_socket.getsockname()[1] ) pytest.raises( TypedAttributeLookupError, listener.extra, SocketAttribute.remote_address, ) pytest.raises( TypedAttributeLookupError, listener.extra, SocketAttribute.remote_port, ) @pytest.mark.parametrize( "family", [ pytest.param(AddressFamily.AF_INET, id="ipv4"), pytest.param(AddressFamily.AF_INET6, id="ipv6", marks=[skip_ipv6_mark]), pytest.param(socket.AF_UNSPEC, id="both", marks=[skip_ipv6_mark]), ], ) async def test_accept(self, family: AnyIPAddressFamily) -> None: async with await create_tcp_listener( local_host="localhost", family=family ) as multi: for listener in multi.listeners: client = socket.socket(listener.extra(SocketAttribute.family)) client.settimeout(1) client.connect(listener.extra(SocketAttribute.local_address)) assert isinstance(listener, SocketListener) stream = await listener.accept() client.sendall(b"blah") request = await stream.receive() await stream.send(request[::-1]) assert client.recv(100) == b"halb" client.close() await stream.aclose() async def test_accept_after_close(self, family: AnyIPAddressFamily) -> None: async with await create_tcp_listener( local_host="localhost", family=family ) as multi: for listener in multi.listeners: await listener.aclose() assert isinstance(listener, SocketListener) with pytest.raises(ClosedResourceError): await listener.accept() async def test_socket_options(self, family: AnyIPAddressFamily) -> None: async with await create_tcp_listener( local_host="localhost", family=family ) as multi: for listener in multi.listeners: raw_socket = listener.extra(SocketAttribute.raw_socket) if sys.platform == "win32": assert ( raw_socket.getsockopt( socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE ) != 0 ) else: assert ( raw_socket.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) != 0 ) raw_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 80000) assert raw_socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) in ( 80000, 160000, ) client = socket.socket(raw_socket.family) client.settimeout(1) client.connect(raw_socket.getsockname()) assert isinstance(listener, SocketListener) async with await listener.accept() as stream: raw_socket = stream.extra(SocketAttribute.raw_socket) assert raw_socket.gettimeout() == 0 assert raw_socket.family == listener.extra(SocketAttribute.family) assert ( raw_socket.getsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY) != 0 ) client.close() @pytest.mark.skipif( not hasattr(socket, "SO_REUSEPORT"), reason="SO_REUSEPORT option not supported" ) async def test_reuse_port(self, family: AnyIPAddressFamily) -> None: multi1 = await create_tcp_listener( local_host="localhost", family=family, reuse_port=True ) assert len(multi1.listeners) == 1 multi2 = await create_tcp_listener( local_host="localhost", local_port=multi1.listeners[0].extra(SocketAttribute.local_port), family=family, reuse_port=True, ) assert len(multi2.listeners) == 1 assert multi1.listeners[0].extra( SocketAttribute.local_address ) == multi2.listeners[0].extra(SocketAttribute.local_address) await multi1.aclose() await multi2.aclose() async def test_close_from_other_task(self, family: AnyIPAddressFamily) -> None: listener = await create_tcp_listener(local_host="localhost", family=family) with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(listener.serve, lambda stream: None) await wait_all_tasks_blocked() await listener.aclose() tg.cancel_scope.cancel() assert len(exc.value.exceptions) == 1 assert isinstance(exc.value.exceptions[0], ExceptionGroup) nested_grp = exc.value.exceptions[0] assert len(nested_grp.exceptions) == 1 assert isinstance(nested_grp.exceptions[0], ExceptionGroup) async def test_send_after_eof(self, family: AnyIPAddressFamily) -> None: async def handle(stream: SocketStream) -> None: async with stream: await stream.send(b"Hello\n") multi = await create_tcp_listener(family=family, local_host="localhost") async with multi, create_task_group() as tg: tg.start_soon(multi.serve, handle) await wait_all_tasks_blocked() with socket.socket(family) as client: client.connect(multi.extra(SocketAttribute.local_address)) client.shutdown(socket.SHUT_WR) client.setblocking(False) with fail_after(1): while True: try: message = client.recv(10) except BlockingIOError: await sleep(0) else: assert message == b"Hello\n" break tg.cancel_scope.cancel() async def test_eof_after_send(self, family: AnyIPAddressFamily) -> None: """Regression test for #701.""" received_bytes = b"" async def handle(stream: SocketStream) -> None: nonlocal received_bytes async with stream: received_bytes = await stream.receive() with pytest.raises(EndOfStream), fail_after(1): await stream.receive() tg.cancel_scope.cancel() multi = await create_tcp_listener(family=family, local_host="localhost") async with multi, create_task_group() as tg: with socket.socket(family) as client: client.connect(multi.extra(SocketAttribute.local_address)) client.send(b"Hello") client.shutdown(socket.SHUT_WR) await multi.serve(handle) assert received_bytes == b"Hello" @skip_ipv6_mark @pytest.mark.skipif( sys.platform == "win32", reason="Windows does not support interface name suffixes", ) async def test_bind_link_local(self) -> None: # Regression test for #554 link_local_ipv6_address = next( ( addr.address for addresses in psutil.net_if_addrs().values() for addr in addresses if addr.address.startswith("fe80::") and "%" in addr.address ), None, ) if link_local_ipv6_address is None: pytest.fail("Could not find a link-local IPv6 interface") async with await create_tcp_listener(local_host=link_local_ipv6_address): pass @pytest.mark.skipif( sys.platform == "win32", reason="UNIX sockets are not available on Windows" ) class TestUNIXStream: @pytest.fixture def socket_path(self) -> Generator[Path, None, None]: # Use stdlib tempdir generation # Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path with tempfile.TemporaryDirectory() as path: yield Path(path) / "socket" @pytest.fixture(params=[False, True], ids=["str", "path"]) def socket_path_or_str(self, request: SubRequest, socket_path: Path) -> Path | str: return socket_path if request.param else str(socket_path) @pytest.fixture def server_sock(self, socket_path: Path) -> Iterable[socket.socket]: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(1) sock.bind(str(socket_path)) sock.listen() yield sock sock.close() async def test_extra_attributes( self, server_sock: socket.socket, socket_path: Path ) -> None: async with await connect_unix(socket_path) as stream: raw_socket = stream.extra(SocketAttribute.raw_socket) assert stream.extra(SocketAttribute.family) == socket.AF_UNIX assert ( stream.extra(SocketAttribute.local_address) == raw_socket.getsockname() ) assert stream.extra(SocketAttribute.remote_address) == str(socket_path) pytest.raises( TypedAttributeLookupError, stream.extra, SocketAttribute.local_port ) pytest.raises( TypedAttributeLookupError, stream.extra, SocketAttribute.remote_port ) async def test_send_receive( self, server_sock: socket.socket, socket_path_or_str: Path | str ) -> None: async with await connect_unix(socket_path_or_str) as stream: client, _ = server_sock.accept() await stream.send(b"blah") request = client.recv(100) client.sendall(request[::-1]) response = await stream.receive() client.close() assert response == b"halb" async def test_receive_large_buffer( self, server_sock: socket.socket, socket_path: Path ) -> None: def serve() -> None: client, _ = server_sock.accept() client.sendall(buffer) client.close() buffer = ( b"\xff" * 1024 * 512 + b"\x00" * 1024 * 512 ) # should exceed the maximum kernel send buffer size async with await connect_unix(socket_path) as stream: thread = Thread(target=serve, daemon=True) thread.start() response = b"" while len(response) < len(buffer): response += await stream.receive() thread.join() assert response == buffer async def test_send_large_buffer( self, server_sock: socket.socket, socket_path: Path ) -> None: response = b"" def serve() -> None: nonlocal response client, _ = server_sock.accept() while True: data = client.recv(1024) if not data: break response += data client.close() buffer = ( b"\xff" * 1024 * 512 + b"\x00" * 1024 * 512 ) # should exceed the maximum kernel send buffer size async with await connect_unix(socket_path) as stream: thread = Thread(target=serve, daemon=True) thread.start() await stream.send(buffer) thread.join() assert response == buffer async def test_receive_fds( self, server_sock: socket.socket, socket_path: Path, tmp_path: Path ) -> None: def serve() -> None: path1 = tmp_path / "file1" path2 = tmp_path / "file2" path1.write_text("Hello, ") path2.write_text("World!") with path1.open() as file1, path2.open() as file2: fdarray = array.array("i", [file1.fileno(), file2.fileno()]) client, _ = server_sock.accept() cmsg = (socket.SOL_SOCKET, socket.SCM_RIGHTS, fdarray) with client: client.sendmsg([b"test"], [cmsg]) async with await connect_unix(socket_path) as stream: thread = Thread(target=serve, daemon=True) thread.start() message, fds = await stream.receive_fds(10, 2) thread.join() text = "" for fd in fds: with os.fdopen(fd) as file: text += file.read() assert message == b"test" assert text == "Hello, World!" async def test_receive_fds_bad_args( self, server_sock: socket.socket, socket_path: Path ) -> None: async with await connect_unix(socket_path) as stream: for msglen in (-1, "foo"): with pytest.raises( ValueError, match="msglen must be a non-negative integer" ): await stream.receive_fds(msglen, 0) # type: ignore[arg-type] for maxfds in (0, "foo"): with pytest.raises( ValueError, match="maxfds must be a positive integer" ): await stream.receive_fds(0, maxfds) # type: ignore[arg-type] async def test_send_fds( self, server_sock: socket.socket, socket_path: Path, tmp_path: Path ) -> None: def serve() -> None: fds = array.array("i") client, _ = server_sock.accept() msg, ancdata, *_ = client.recvmsg(10, socket.CMSG_LEN(2 * fds.itemsize)) client.close() assert msg == b"test" for cmsg_level, cmsg_type, cmsg_data in ancdata: assert cmsg_level == socket.SOL_SOCKET assert cmsg_type == socket.SCM_RIGHTS fds.frombytes( cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)] ) text = "" for fd in fds: with os.fdopen(fd) as file: text += file.read() assert text == "Hello, World!" path1 = tmp_path / "file1" path2 = tmp_path / "file2" path1.write_text("Hello, ") path2.write_text("World!") with path1.open() as file1, path2.open() as file2, fail_after(2): assert isinstance(file1, io.TextIOWrapper) assert isinstance(file2, io.TextIOWrapper) async with await connect_unix(socket_path) as stream: thread = Thread(target=serve, daemon=True) thread.start() await stream.send_fds(b"test", [file1, file2]) thread.join() async def test_send_eof( self, server_sock: socket.socket, socket_path: Path ) -> None: def serve() -> None: client, _ = server_sock.accept() request = b"" while True: data = client.recv(100) request += data if not data: break client.sendall(request[::-1]) client.close() async with await connect_unix(socket_path) as stream: thread = Thread(target=serve, daemon=True) thread.start() await stream.send(b"hello, ") await stream.send(b"world\n") await stream.send_eof() response = await stream.receive() thread.join() assert response == b"\ndlrow ,olleh" async def test_iterate(self, server_sock: socket.socket, socket_path: Path) -> None: def serve() -> None: client, _ = server_sock.accept() client.sendall(b"bl") time.sleep(0.05) client.sendall(b"ah") client.close() thread = Thread(target=serve, daemon=True) thread.start() chunks = [] async with await connect_unix(socket_path) as stream: async for chunk in stream: chunks.append(chunk) thread.join() assert chunks == [b"bl", b"ah"] async def test_send_fds_bad_args( self, server_sock: socket.socket, socket_path: Path ) -> None: async with await connect_unix(socket_path) as stream: with pytest.raises(ValueError, match="message must not be empty"): await stream.send_fds(b"", [0]) with pytest.raises(ValueError, match="fds must not be empty"): await stream.send_fds(b"test", []) async def test_concurrent_send( self, server_sock: socket.socket, socket_path: Path ) -> None: async def send_data() -> NoReturn: while True: await client.send(b"\x00" * 4096) async with await connect_unix(socket_path) as client: async with create_task_group() as tg: tg.start_soon(send_data) await wait_all_tasks_blocked() with pytest.raises(BusyResourceError) as exc: await client.send(b"foo") exc.match("already writing to") tg.cancel_scope.cancel() async def test_concurrent_receive( self, server_sock: socket.socket, socket_path: Path ) -> None: async with await connect_unix(socket_path) as client: async with create_task_group() as tg: tg.start_soon(client.receive) await wait_all_tasks_blocked() try: with pytest.raises(BusyResourceError) as exc: await client.receive() exc.match("already reading from") finally: tg.cancel_scope.cancel() async def test_close_during_receive( self, server_sock: socket.socket, socket_path: Path ) -> None: async def interrupt() -> None: await wait_all_tasks_blocked() await stream.aclose() async with await connect_unix(socket_path) as stream: async with create_task_group() as tg: tg.start_soon(interrupt) with pytest.raises(ClosedResourceError): await stream.receive() async def test_receive_after_close( self, server_sock: socket.socket, socket_path: Path ) -> None: stream = await connect_unix(socket_path) await stream.aclose() with pytest.raises(ClosedResourceError): await stream.receive() async def test_send_after_close( self, server_sock: socket.socket, socket_path: Path ) -> None: stream = await connect_unix(socket_path) await stream.aclose() with pytest.raises(ClosedResourceError): await stream.send(b"foo") async def test_cannot_connect(self, socket_path: Path) -> None: with pytest.raises(FileNotFoundError): await connect_unix(socket_path) async def test_connecting_using_bytes( self, server_sock: socket.socket, socket_path: Path ) -> None: async with await connect_unix(str(socket_path).encode()): pass @pytest.mark.skipif( platform.system() == "Darwin", reason="macOS requires valid UTF-8 paths" ) async def test_connecting_with_non_utf8(self, socket_path: Path) -> None: actual_path = str(socket_path).encode() + b"\xf0" server = socket.socket(socket.AF_UNIX) server.bind(actual_path) server.listen(1) async with await connect_unix(actual_path): pass @pytest.mark.skipif( sys.platform == "win32", reason="UNIX sockets are not available on Windows" ) class TestUNIXListener: @pytest.fixture def socket_path(self) -> Generator[Path, None, None]: # Use stdlib tempdir generation # Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path with tempfile.TemporaryDirectory() as path: yield Path(path) / "socket" @pytest.fixture(params=[False, True], ids=["str", "path"]) def socket_path_or_str(self, request: SubRequest, socket_path: Path) -> Path | str: return socket_path if request.param else str(socket_path) async def test_extra_attributes(self, socket_path: Path) -> None: async with await create_unix_listener(socket_path) as listener: raw_socket = listener.extra(SocketAttribute.raw_socket) assert listener.extra(SocketAttribute.family) == socket.AF_UNIX assert ( listener.extra(SocketAttribute.local_address) == raw_socket.getsockname() ) pytest.raises( TypedAttributeLookupError, listener.extra, SocketAttribute.local_port ) pytest.raises( TypedAttributeLookupError, listener.extra, SocketAttribute.remote_address, ) pytest.raises( TypedAttributeLookupError, listener.extra, SocketAttribute.remote_port ) async def test_accept(self, socket_path_or_str: Path | str) -> None: async with await create_unix_listener(socket_path_or_str) as listener: client = socket.socket(socket.AF_UNIX) client.settimeout(1) client.connect(str(socket_path_or_str)) stream = await listener.accept() client.sendall(b"blah") request = await stream.receive() await stream.send(request[::-1]) assert client.recv(100) == b"halb" client.close() await stream.aclose() async def test_socket_options(self, socket_path: Path) -> None: async with await create_unix_listener(socket_path) as listener: listener_socket = listener.extra(SocketAttribute.raw_socket) assert listener_socket.family == socket.AddressFamily.AF_UNIX listener_socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 80000) assert listener_socket.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) in ( 80000, 160000, ) client = socket.socket(listener_socket.family) client.settimeout(1) client.connect(listener_socket.getsockname()) async with await listener.accept() as stream: assert stream.extra(SocketAttribute.raw_socket).gettimeout() == 0 assert stream.extra(SocketAttribute.family) == listener_socket.family client.close() async def test_send_after_eof(self, socket_path: Path) -> None: async def handle(stream: SocketStream) -> None: async with stream: await stream.send(b"Hello\n") async with await create_unix_listener( socket_path ) as listener, create_task_group() as tg: tg.start_soon(listener.serve, handle) await wait_all_tasks_blocked() with socket.socket(socket.AF_UNIX) as client: client.connect(str(socket_path)) client.shutdown(socket.SHUT_WR) client.setblocking(False) with fail_after(1): while True: try: message = client.recv(10) except BlockingIOError: await sleep(0) else: assert message == b"Hello\n" break tg.cancel_scope.cancel() async def test_bind_twice(self, socket_path: Path) -> None: """Test that the previous socket is removed before binding to the path.""" for _ in range(2): async with await create_unix_listener(socket_path): pass async def test_listening_bytes_path(self, socket_path: Path) -> None: async with await create_unix_listener(str(socket_path).encode()): pass @pytest.mark.skipif( platform.system() == "Darwin", reason="macOS requires valid UTF-8 paths" ) async def test_listening_invalid_ascii(self, socket_path: Path) -> None: real_path = str(socket_path).encode() + b"\xf0" async with await create_unix_listener(real_path): pass async def test_multi_listener(tmp_path_factory: TempPathFactory) -> None: async def handle(stream: SocketStream) -> None: client_addresses.append(stream.extra(SocketAttribute.remote_address)) event.set() await stream.aclose() client_addresses: list[str | IPSockAddrType] = [] listeners: list[Listener] = [await create_tcp_listener(local_host="localhost")] with tempfile.TemporaryDirectory() as path: if sys.platform != "win32": listeners.append(await create_unix_listener(Path(path) / "socket")) expected_addresses: list[str | IPSockAddrType] = [] async with MultiListener(listeners) as multi_listener: async with create_task_group() as tg: tg.start_soon(multi_listener.serve, handle) for listener in multi_listener.listeners: event = Event() local_address = listener.extra(SocketAttribute.local_address) if ( sys.platform != "win32" and listener.extra(SocketAttribute.family) == socket.AddressFamily.AF_UNIX ): assert isinstance(local_address, str) stream: SocketStream = await connect_unix(local_address) else: assert isinstance(local_address, tuple) stream = await connect_tcp(*local_address) expected_addresses.append( stream.extra(SocketAttribute.local_address) ) await event.wait() await stream.aclose() tg.cancel_scope.cancel() assert client_addresses == expected_addresses @pytest.mark.network @pytest.mark.usefixtures("check_asyncio_bug") class TestUDPSocket: async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None: async with await create_udp_socket( family=family, local_host="localhost" ) as udp: raw_socket = udp.extra(SocketAttribute.raw_socket) assert raw_socket.gettimeout() == 0 assert udp.extra(SocketAttribute.family) == family assert ( udp.extra(SocketAttribute.local_address) == raw_socket.getsockname()[:2] ) assert udp.extra(SocketAttribute.local_port) == raw_socket.getsockname()[1] pytest.raises( TypedAttributeLookupError, udp.extra, SocketAttribute.remote_address ) pytest.raises( TypedAttributeLookupError, udp.extra, SocketAttribute.remote_port ) async def test_send_receive(self, family: AnyIPAddressFamily) -> None: async with await create_udp_socket( local_host="localhost", family=family ) as sock: host, port = sock.extra(SocketAttribute.local_address) # type: ignore[misc] await sock.sendto(b"blah", host, port) request, addr = await sock.receive() assert request == b"blah" assert addr == sock.extra(SocketAttribute.local_address) await sock.sendto(b"halb", host, port) response, addr = await sock.receive() assert response == b"halb" assert addr == (host, port) async def test_iterate(self, family: AnyIPAddressFamily) -> None: async def serve() -> None: async for packet, addr in server: await server.send((packet[::-1], addr)) async with await create_udp_socket( family=family, local_host="localhost" ) as server: host, port = server.extra( # type: ignore[misc] SocketAttribute.local_address ) async with await create_udp_socket( family=family, local_host="localhost" ) as client: async with create_task_group() as tg: tg.start_soon(serve) await client.sendto(b"FOOBAR", host, port) assert await client.receive() == (b"RABOOF", (host, port)) await client.sendto(b"123456", host, port) assert await client.receive() == (b"654321", (host, port)) tg.cancel_scope.cancel() @pytest.mark.skipif( not hasattr(socket, "SO_REUSEPORT"), reason="SO_REUSEPORT option not supported" ) async def test_reuse_port(self, family: AnyIPAddressFamily) -> None: async with await create_udp_socket( family=family, local_host="localhost", reuse_port=True ) as udp: port = udp.extra(SocketAttribute.local_port) assert port != 0 async with await create_udp_socket( family=family, local_host="localhost", local_port=port, reuse_port=True ) as udp2: assert port == udp2.extra(SocketAttribute.local_port) async def test_concurrent_receive(self) -> None: async with await create_udp_socket( family=AddressFamily.AF_INET, local_host="localhost" ) as udp: async with create_task_group() as tg: tg.start_soon(udp.receive) await wait_all_tasks_blocked() try: with pytest.raises(BusyResourceError) as exc: await udp.receive() exc.match("already reading from") finally: tg.cancel_scope.cancel() async def test_close_during_receive(self) -> None: async def close_when_blocked() -> None: await wait_all_tasks_blocked() await udp.aclose() async with await create_udp_socket( family=AddressFamily.AF_INET, local_host="localhost" ) as udp: async with create_task_group() as tg: tg.start_soon(close_when_blocked) with pytest.raises(ClosedResourceError): await udp.receive() async def test_receive_after_close(self) -> None: udp = await create_udp_socket( family=AddressFamily.AF_INET, local_host="localhost" ) await udp.aclose() with pytest.raises(ClosedResourceError): await udp.receive() async def test_send_after_close(self) -> None: udp = await create_udp_socket( family=AddressFamily.AF_INET, local_host="localhost" ) host, port = udp.extra(SocketAttribute.local_address) # type: ignore[misc] await udp.aclose() with pytest.raises(ClosedResourceError): await udp.sendto(b"foo", host, port) async def test_create_unbound_socket(self, family: AnyIPAddressFamily) -> None: """Regression test for #360.""" async with await create_udp_socket(family=family) as udp: local_address = cast( IPSockAddrType, udp.extra(SocketAttribute.local_address) ) assert local_address[1] > 0 @pytest.mark.network @pytest.mark.usefixtures("check_asyncio_bug") class TestConnectedUDPSocket: async def test_extra_attributes(self, family: AnyIPAddressFamily) -> None: async with await create_connected_udp_socket( "localhost", 5000, family=family ) as udp: raw_socket = udp.extra(SocketAttribute.raw_socket) assert udp.extra(SocketAttribute.family) == family assert ( udp.extra(SocketAttribute.local_address) == raw_socket.getsockname()[:2] ) assert udp.extra(SocketAttribute.local_port) == raw_socket.getsockname()[1] assert ( udp.extra(SocketAttribute.remote_address) == raw_socket.getpeername()[:2] ) assert udp.extra(SocketAttribute.remote_port) == 5000 async def test_send_receive(self, family: AnyIPAddressFamily) -> None: async with await create_udp_socket( family=family, local_host="localhost" ) as udp1: host, port = udp1.extra(SocketAttribute.local_address) # type: ignore[misc] async with await create_connected_udp_socket( host, port, local_host="localhost", family=family ) as udp2: host, port = udp2.extra( SocketAttribute.local_address # type: ignore[misc] ) await udp2.send(b"blah") request = await udp1.receive() assert request == (b"blah", (host, port)) await udp1.sendto(b"halb", host, port) response = await udp2.receive() assert response == b"halb" async def test_iterate(self, family: AnyIPAddressFamily) -> None: async def serve() -> None: async for packet in udp2: await udp2.send(packet[::-1]) async with await create_udp_socket( family=family, local_host="localhost" ) as udp1: host, port = udp1.extra(SocketAttribute.local_address) # type: ignore[misc] async with await create_connected_udp_socket(host, port) as udp2: host, port = udp2.extra( # type: ignore[misc] SocketAttribute.local_address ) async with create_task_group() as tg: tg.start_soon(serve) await udp1.sendto(b"FOOBAR", host, port) assert await udp1.receive() == (b"RABOOF", (host, port)) await udp1.sendto(b"123456", host, port) assert await udp1.receive() == (b"654321", (host, port)) tg.cancel_scope.cancel() @pytest.mark.skipif( not hasattr(socket, "SO_REUSEPORT"), reason="SO_REUSEPORT option not supported" ) async def test_reuse_port(self, family: AnyIPAddressFamily) -> None: async with await create_connected_udp_socket( "localhost", 6000, family=family, local_host="localhost", reuse_port=True ) as udp: port = udp.extra(SocketAttribute.local_port) assert port != 0 async with await create_connected_udp_socket( "localhost", 6001, family=family, local_host="localhost", local_port=port, reuse_port=True, ) as udp2: assert port == udp2.extra(SocketAttribute.local_port) async def test_concurrent_receive(self) -> None: async with await create_connected_udp_socket( "localhost", 5000, local_host="localhost", family=AddressFamily.AF_INET ) as udp: async with create_task_group() as tg: tg.start_soon(udp.receive) await wait_all_tasks_blocked() try: with pytest.raises(BusyResourceError) as exc: await udp.receive() exc.match("already reading from") finally: tg.cancel_scope.cancel() async def test_close_during_receive(self) -> None: async def close_when_blocked() -> None: await wait_all_tasks_blocked() await udp.aclose() async with await create_connected_udp_socket( "localhost", 5000, local_host="localhost", family=AddressFamily.AF_INET ) as udp: async with create_task_group() as tg: tg.start_soon(close_when_blocked) with pytest.raises(ClosedResourceError): await udp.receive() async def test_receive_after_close(self, family: AnyIPAddressFamily) -> None: udp = await create_connected_udp_socket( "localhost", 5000, local_host="localhost", family=family ) await udp.aclose() with pytest.raises(ClosedResourceError): await udp.receive() async def test_send_after_close(self, family: AnyIPAddressFamily) -> None: udp = await create_connected_udp_socket( "localhost", 5000, local_host="localhost", family=family ) await udp.aclose() with pytest.raises(ClosedResourceError): await udp.send(b"foo") @pytest.mark.skipif( sys.platform == "win32", reason="UNIX sockets are not available on Windows" ) class TestUNIXDatagramSocket: @pytest.fixture def socket_path(self) -> Generator[Path, None, None]: # Use stdlib tempdir generation # Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path with tempfile.TemporaryDirectory() as path: yield Path(path) / "socket" @pytest.fixture(params=[False, True], ids=["str", "path"]) def socket_path_or_str(self, request: SubRequest, socket_path: Path) -> Path | str: return socket_path if request.param else str(socket_path) @pytest.fixture def peer_socket_path(self) -> Generator[Path, None, None]: # Use stdlib tempdir generation # Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path with tempfile.TemporaryDirectory() as path: yield Path(path) / "peer_socket" async def test_extra_attributes(self, socket_path: Path) -> None: async with await create_unix_datagram_socket(local_path=socket_path) as unix_dg: raw_socket = unix_dg.extra(SocketAttribute.raw_socket) assert raw_socket.gettimeout() == 0 assert unix_dg.extra(SocketAttribute.family) == socket.AF_UNIX assert ( unix_dg.extra(SocketAttribute.local_address) == raw_socket.getsockname() ) pytest.raises( TypedAttributeLookupError, unix_dg.extra, SocketAttribute.local_port ) pytest.raises( TypedAttributeLookupError, unix_dg.extra, SocketAttribute.remote_address ) pytest.raises( TypedAttributeLookupError, unix_dg.extra, SocketAttribute.remote_port ) async def test_send_receive(self, socket_path_or_str: Path | str) -> None: async with await create_unix_datagram_socket( local_path=socket_path_or_str, ) as sock: path = str(socket_path_or_str) await sock.sendto(b"blah", path) request, addr = await sock.receive() assert request == b"blah" assert addr == path await sock.sendto(b"halb", path) response, addr = await sock.receive() assert response == b"halb" assert addr == path async def test_iterate(self, peer_socket_path: Path, socket_path: Path) -> None: async def serve() -> None: async for packet, addr in server: await server.send((packet[::-1], addr)) async with await create_unix_datagram_socket( local_path=peer_socket_path, ) as server: peer_path = str(peer_socket_path) async with await create_unix_datagram_socket( local_path=socket_path ) as client: async with create_task_group() as tg: tg.start_soon(serve) await client.sendto(b"FOOBAR", peer_path) assert await client.receive() == (b"RABOOF", peer_path) await client.sendto(b"123456", peer_path) assert await client.receive() == (b"654321", peer_path) tg.cancel_scope.cancel() async def test_concurrent_receive(self) -> None: async with await create_unix_datagram_socket() as unix_dg: async with create_task_group() as tg: tg.start_soon(unix_dg.receive) await wait_all_tasks_blocked() try: with pytest.raises(BusyResourceError) as exc: await unix_dg.receive() exc.match("already reading from") finally: tg.cancel_scope.cancel() async def test_close_during_receive(self) -> None: async def close_when_blocked() -> None: await wait_all_tasks_blocked() await unix_dg.aclose() async with await create_unix_datagram_socket() as unix_dg: async with create_task_group() as tg: tg.start_soon(close_when_blocked) with pytest.raises(ClosedResourceError): await unix_dg.receive() async def test_receive_after_close(self) -> None: unix_dg = await create_unix_datagram_socket() await unix_dg.aclose() with pytest.raises(ClosedResourceError): await unix_dg.receive() async def test_send_after_close(self, socket_path: Path) -> None: unix_dg = await create_unix_datagram_socket(local_path=socket_path) path = str(socket_path) await unix_dg.aclose() with pytest.raises(ClosedResourceError): await unix_dg.sendto(b"foo", path) async def test_local_path_bytes(self, socket_path: Path) -> None: async with await create_unix_datagram_socket( local_path=str(socket_path).encode() ): pass @pytest.mark.skipif( platform.system() == "Darwin", reason="macOS requires valid UTF-8 paths" ) async def test_local_path_invalid_ascii(self, socket_path: Path) -> None: real_path = str(socket_path).encode() + b"\xf0" async with await create_unix_datagram_socket(local_path=real_path): pass @pytest.mark.skipif( sys.platform == "win32", reason="UNIX sockets are not available on Windows" ) class TestConnectedUNIXDatagramSocket: @pytest.fixture def socket_path(self) -> Generator[Path, None, None]: # Use stdlib tempdir generation # Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path with tempfile.TemporaryDirectory() as path: yield Path(path) / "socket" @pytest.fixture(params=[False, True], ids=["str", "path"]) def socket_path_or_str(self, request: SubRequest, socket_path: Path) -> Path | str: return socket_path if request.param else str(socket_path) @pytest.fixture def peer_socket_path(self) -> Generator[Path, None, None]: # Use stdlib tempdir generation # Fixes `OSError: AF_UNIX path too long` from pytest generated temp_path with tempfile.TemporaryDirectory() as path: yield Path(path) / "peer_socket" @pytest.fixture(params=[False, True], ids=["peer_str", "peer_path"]) def peer_socket_path_or_str( self, request: SubRequest, peer_socket_path: Path ) -> Path | str: return peer_socket_path if request.param else str(peer_socket_path) @pytest.fixture def peer_sock(self, peer_socket_path: Path) -> Iterable[socket.socket]: sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) sock.settimeout(1) sock.bind(str(peer_socket_path)) yield sock sock.close() async def test_extra_attributes( self, socket_path: Path, peer_socket_path: Path, peer_sock: socket.socket, ) -> None: async with await create_connected_unix_datagram_socket( remote_path=peer_socket_path, local_path=socket_path, ) as unix_dg: raw_socket = unix_dg.extra(SocketAttribute.raw_socket) assert raw_socket is not None assert unix_dg.extra(SocketAttribute.family) == AddressFamily.AF_UNIX assert unix_dg.extra(SocketAttribute.local_address) == str(socket_path) assert unix_dg.extra(SocketAttribute.remote_address) == str( peer_socket_path ) pytest.raises( TypedAttributeLookupError, unix_dg.extra, SocketAttribute.local_port ) pytest.raises( TypedAttributeLookupError, unix_dg.extra, SocketAttribute.remote_port ) async def test_send_receive( self, socket_path_or_str: Path | str, peer_socket_path_or_str: Path | str, ) -> None: async with await create_unix_datagram_socket( local_path=peer_socket_path_or_str, ) as unix_dg1: async with await create_connected_unix_datagram_socket( peer_socket_path_or_str, local_path=socket_path_or_str, ) as unix_dg2: socket_path = str(socket_path_or_str) await unix_dg2.send(b"blah") request = await unix_dg1.receive() assert request == (b"blah", socket_path) await unix_dg1.sendto(b"halb", socket_path) response = await unix_dg2.receive() assert response == b"halb" async def test_iterate( self, socket_path: Path, peer_socket_path: Path, ) -> None: async def serve() -> None: async for packet in unix_dg2: await unix_dg2.send(packet[::-1]) async with await create_unix_datagram_socket( local_path=peer_socket_path, ) as unix_dg1: async with await create_connected_unix_datagram_socket( peer_socket_path, local_path=socket_path ) as unix_dg2: path = str(socket_path) async with create_task_group() as tg: tg.start_soon(serve) await unix_dg1.sendto(b"FOOBAR", path) assert await unix_dg1.receive() == (b"RABOOF", path) await unix_dg1.sendto(b"123456", path) assert await unix_dg1.receive() == (b"654321", path) tg.cancel_scope.cancel() async def test_concurrent_receive( self, peer_socket_path: Path, peer_sock: socket.socket ) -> None: async with await create_connected_unix_datagram_socket( peer_socket_path ) as unix_dg: async with create_task_group() as tg: tg.start_soon(unix_dg.receive) await wait_all_tasks_blocked() try: with pytest.raises(BusyResourceError) as exc: await unix_dg.receive() exc.match("already reading from") finally: tg.cancel_scope.cancel() async def test_close_during_receive( self, peer_socket_path_or_str: Path | str, peer_sock: socket.socket ) -> None: async def close_when_blocked() -> None: await wait_all_tasks_blocked() await udp.aclose() async with await create_connected_unix_datagram_socket( peer_socket_path_or_str ) as udp: async with create_task_group() as tg: tg.start_soon(close_when_blocked) with pytest.raises(ClosedResourceError): await udp.receive() async def test_receive_after_close( self, peer_socket_path_or_str: Path | str, peer_sock: socket.socket ) -> None: udp = await create_connected_unix_datagram_socket(peer_socket_path_or_str) await udp.aclose() with pytest.raises(ClosedResourceError): await udp.receive() async def test_send_after_close( self, peer_socket_path_or_str: Path | str, peer_sock: socket.socket ) -> None: udp = await create_connected_unix_datagram_socket(peer_socket_path_or_str) await udp.aclose() with pytest.raises(ClosedResourceError): await udp.send(b"foo") @pytest.mark.network async def test_getaddrinfo() -> None: # IDNA 2003 gets this wrong correct = await getaddrinfo("faß.de", 0) wrong = await getaddrinfo("fass.de", 0) assert correct != wrong @pytest.mark.parametrize( "sock_type", [socket.SOCK_STREAM, socket.SocketKind.SOCK_STREAM] ) async def test_getaddrinfo_ipv6addr( sock_type: Literal[socket.SocketKind.SOCK_STREAM], ) -> None: # IDNA trips up over raw IPv6 addresses proto = 0 if platform.system() == "Windows" else 6 assert await getaddrinfo("::1", 0, type=sock_type) == [ ( socket.AddressFamily.AF_INET6, socket.SocketKind.SOCK_STREAM, proto, "", ("::1", 0), ) ] async def test_getnameinfo() -> None: expected_result = socket.getnameinfo(("127.0.0.1", 6666), 0) result = await getnameinfo(("127.0.0.1", 6666)) assert result == expected_result anyio-4.4.0/tests/test_subprocesses.py000066400000000000000000000153221462472665400201540ustar00rootroot00000000000000from __future__ import annotations import os import platform import sys from pathlib import Path from subprocess import CalledProcessError from textwrap import dedent import pytest from _pytest.fixtures import FixtureRequest from anyio import CancelScope, ClosedResourceError, open_process, run_process from anyio.streams.buffered import BufferedByteReceiveStream pytestmark = pytest.mark.anyio @pytest.mark.parametrize( "shell, command", [ pytest.param( True, f'{sys.executable} -c "import sys; print(sys.stdin.read()[::-1])"', id="shell", ), pytest.param( False, [sys.executable, "-c", "import sys; print(sys.stdin.read()[::-1])"], id="exec", ), ], ) async def test_run_process( shell: bool, command: str | list[str], anyio_backend_name: str ) -> None: process = await run_process(command, input=b"abc") assert process.returncode == 0 assert process.stdout.rstrip() == b"cba" async def test_run_process_checked() -> None: with pytest.raises(CalledProcessError) as exc: await run_process( [ sys.executable, "-c", 'import sys; print("stderr-text", file=sys.stderr); ' 'print("stdout-text"); sys.exit(1)', ], check=True, ) assert exc.value.returncode == 1 assert exc.value.stdout.rstrip() == b"stdout-text" assert exc.value.stderr.rstrip() == b"stderr-text" @pytest.mark.skipif( platform.system() == "Windows", reason="process.terminate() kills the process instantly on Windows", ) async def test_terminate(tmp_path: Path) -> None: script_path = tmp_path / "script.py" script_path.write_text( dedent( """\ import signal, sys, time def terminate(signum, frame): sys.exit(2) signal.signal(signal.SIGTERM, terminate) print('ready', flush=True) time.sleep(5) """ ) ) async with await open_process([sys.executable, str(script_path)]) as process: stdout = process.stdout assert stdout is not None buffered_stdout = BufferedByteReceiveStream(stdout) line = await buffered_stdout.receive_until(b"\n", 100) assert line.rstrip() == b"ready" process.terminate() assert await process.wait() == 2 async def test_process_cwd(tmp_path: Path) -> None: """Test that `cwd` is successfully passed to the subprocess implementation""" cmd = [sys.executable, "-c", "import os; print(os.getcwd())"] result = await run_process(cmd, cwd=tmp_path) assert result.stdout.decode().strip() == str(tmp_path) async def test_process_env() -> None: """Test that `env` is successfully passed to the subprocess implementation""" env = os.environ.copy() env.update({"foo": "bar"}) cmd = [sys.executable, "-c", "import os; print(os.environ['foo'])"] result = await run_process(cmd, env=env) assert result.stdout.decode().strip() == env["foo"] @pytest.mark.skipif( platform.system() == "Windows", reason="Windows does not have os.getsid()" ) async def test_process_new_session_sid() -> None: """ Test that start_new_session is successfully passed to the subprocess implementation. """ sid = os.getsid(os.getpid()) cmd = [sys.executable, "-c", "import os; print(os.getsid(os.getpid()))"] result = await run_process(cmd) assert result.stdout.decode().strip() == str(sid) result = await run_process(cmd, start_new_session=True) assert result.stdout.decode().strip() != str(sid) async def test_run_process_connect_to_file(tmp_path: Path) -> None: stdinfile = tmp_path / "stdin" stdinfile.write_text("Hello, process!\n") stdoutfile = tmp_path / "stdout" stderrfile = tmp_path / "stderr" with stdinfile.open("rb") as fin, stdoutfile.open("wb") as fout, stderrfile.open( "wb" ) as ferr: async with await open_process( [ sys.executable, "-c", "import sys; txt = sys.stdin.read().strip(); " 'print("stdin says", repr(txt), "but stderr says NO!", ' "file=sys.stderr); " 'print("stdin says", repr(txt), "and stdout says YES!")', ], stdin=fin, stdout=fout, stderr=ferr, ) as p: assert await p.wait() == 0 assert ( stdoutfile.read_text() == "stdin says 'Hello, process!' and stdout says YES!\n" ) assert ( stderrfile.read_text() == "stdin says 'Hello, process!' but stderr says NO!\n" ) async def test_run_process_inherit_stdout(capfd: pytest.CaptureFixture[str]) -> None: await run_process( [ sys.executable, "-c", 'import sys; print("stderr-text", file=sys.stderr); ' 'print("stdout-text")', ], check=True, stdout=None, stderr=None, ) out, err = capfd.readouterr() assert out == "stdout-text" + os.linesep assert err == "stderr-text" + os.linesep async def test_process_aexit_cancellation_doesnt_orphan_process() -> None: """ Regression test for #669. Ensures that open_process.__aexit__() doesn't leave behind an orphan process when cancelled. """ with CancelScope() as scope: async with await open_process( [sys.executable, "-c", "import time; time.sleep(1)"] ) as process: scope.cancel() assert process.returncode is not None assert process.returncode != 0 async def test_process_aexit_cancellation_closes_standard_streams( request: FixtureRequest, anyio_backend_name: str, ) -> None: """ Regression test for #669. Ensures that open_process.__aexit__() closes standard streams when cancelled. Also ensures that process.std{in.send,{out,err}.receive}() raise ClosedResourceError on a closed stream. """ if anyio_backend_name == "asyncio": # Avoid pytest.xfail here due to https://github.com/pytest-dev/pytest/issues/9027 request.node.add_marker( pytest.mark.xfail(reason="#671 needs to be resolved first") ) with CancelScope() as scope: async with await open_process( [sys.executable, "-c", "import time; time.sleep(1)"] ) as process: scope.cancel() assert process.stdin is not None with pytest.raises(ClosedResourceError): await process.stdin.send(b"foo") assert process.stdout is not None with pytest.raises(ClosedResourceError): await process.stdout.receive(1) assert process.stderr is not None with pytest.raises(ClosedResourceError): await process.stderr.receive(1) anyio-4.4.0/tests/test_synchronization.py000066400000000000000000000532651462472665400207050ustar00rootroot00000000000000from __future__ import annotations import asyncio from typing import Any import pytest from anyio import ( CancelScope, Condition, Event, Lock, Semaphore, WouldBlock, create_task_group, fail_after, run, to_thread, wait_all_tasks_blocked, ) from anyio.abc import CapacityLimiter, TaskStatus pytestmark = pytest.mark.anyio class TestLock: async def test_contextmanager(self) -> None: async def task() -> None: assert lock.locked() async with lock: results.append("2") results = [] lock = Lock() async with create_task_group() as tg: async with lock: tg.start_soon(task) await wait_all_tasks_blocked() results.append("1") assert not lock.locked() assert results == ["1", "2"] async def test_manual_acquire(self) -> None: async def task() -> None: assert lock.locked() await lock.acquire() try: results.append("2") finally: lock.release() results = [] lock = Lock() async with create_task_group() as tg: await lock.acquire() try: tg.start_soon(task) await wait_all_tasks_blocked() results.append("1") finally: lock.release() assert not lock.locked() assert results == ["1", "2"] async def test_acquire_nowait(self) -> None: lock = Lock() lock.acquire_nowait() assert lock.locked() async def test_acquire_nowait_wouldblock(self) -> None: async def try_lock() -> None: pytest.raises(WouldBlock, lock.acquire_nowait) lock = Lock() async with lock, create_task_group() as tg: assert lock.locked() tg.start_soon(try_lock) @pytest.mark.parametrize( "release_first", [pytest.param(False, id="releaselast"), pytest.param(True, id="releasefirst")], ) async def test_cancel_during_acquire(self, release_first: bool) -> None: acquired = False async def task(*, task_status: TaskStatus) -> None: nonlocal acquired task_status.started() async with lock: acquired = True lock = Lock() async with create_task_group() as tg: await lock.acquire() await tg.start(task) tg.cancel_scope.cancel() with CancelScope(shield=True): if release_first: lock.release() await wait_all_tasks_blocked() else: await wait_all_tasks_blocked() lock.release() assert not acquired assert not lock.locked() async def test_statistics(self) -> None: async def waiter() -> None: async with lock: pass lock = Lock() async with create_task_group() as tg: assert not lock.statistics().locked assert lock.statistics().tasks_waiting == 0 async with lock: assert lock.statistics().locked assert lock.statistics().tasks_waiting == 0 for i in range(1, 3): tg.start_soon(waiter) await wait_all_tasks_blocked() assert lock.statistics().tasks_waiting == i assert not lock.statistics().locked assert lock.statistics().tasks_waiting == 0 @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_asyncio_deadlock(self) -> None: """Regression test for #398.""" lock = Lock() async def acquire() -> None: async with lock: await asyncio.sleep(0) loop = asyncio.get_running_loop() task1 = loop.create_task(acquire()) task2 = loop.create_task(acquire()) await asyncio.sleep(0) task1.cancel() await asyncio.wait_for(task2, 1) def test_instantiate_outside_event_loop( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def use_lock() -> None: async with lock: pass lock = Lock() statistics = lock.statistics() assert not statistics.locked assert statistics.owner is None assert statistics.tasks_waiting == 0 run(use_lock, backend=anyio_backend_name, backend_options=anyio_backend_options) class TestEvent: async def test_event(self) -> None: async def setter() -> None: assert not event.is_set() event.set() event = Event() async with create_task_group() as tg: tg.start_soon(setter) await event.wait() assert event.is_set() async def test_event_cancel(self) -> None: task_started = event_set = False async def task() -> None: nonlocal task_started, event_set task_started = True await event.wait() event_set = True event = Event() async with create_task_group() as tg: tg.start_soon(task) tg.cancel_scope.cancel() event.set() assert task_started assert not event_set async def test_event_wait_before_set_before_cancel(self) -> None: setter_started = waiter_woke = False async def setter() -> None: nonlocal setter_started setter_started = True assert not event.is_set() event.set() tg.cancel_scope.cancel() event = Event() async with create_task_group() as tg: tg.start_soon(setter) await event.wait() waiter_woke = True assert setter_started assert waiter_woke async def test_statistics(self) -> None: async def waiter() -> None: await event.wait() event = Event() async with create_task_group() as tg: assert event.statistics().tasks_waiting == 0 for i in range(1, 3): tg.start_soon(waiter) await wait_all_tasks_blocked() assert event.statistics().tasks_waiting == i event.set() assert event.statistics().tasks_waiting == 0 def test_instantiate_outside_event_loop( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def use_event() -> None: event.set() await event.wait() event = Event() assert not event.is_set() assert event.statistics().tasks_waiting == 0 run( use_event, backend=anyio_backend_name, backend_options=anyio_backend_options ) class TestCondition: async def test_contextmanager(self) -> None: async def notifier() -> None: async with condition: condition.notify_all() condition = Condition() async with create_task_group() as tg: async with condition: assert condition.locked() tg.start_soon(notifier) await condition.wait() async def test_manual_acquire(self) -> None: async def notifier() -> None: await condition.acquire() try: condition.notify_all() finally: condition.release() condition = Condition() async with create_task_group() as tg: await condition.acquire() try: assert condition.locked() tg.start_soon(notifier) await condition.wait() finally: condition.release() async def test_acquire_nowait(self) -> None: condition = Condition() condition.acquire_nowait() assert condition.locked() async def test_acquire_nowait_wouldblock(self) -> None: async def try_lock() -> None: pytest.raises(WouldBlock, condition.acquire_nowait) condition = Condition() async with condition, create_task_group() as tg: assert condition.locked() tg.start_soon(try_lock) async def test_wait_cancel(self) -> None: task_started = notified = False async def task() -> None: nonlocal task_started, notified task_started = True async with condition: event.set() await condition.wait() notified = True event = Event() condition = Condition() async with create_task_group() as tg: tg.start_soon(task) await event.wait() await wait_all_tasks_blocked() tg.cancel_scope.cancel() assert task_started assert not notified async def test_statistics(self) -> None: async def waiter() -> None: async with condition: await condition.wait() condition = Condition() async with create_task_group() as tg: assert not condition.statistics().lock_statistics.locked assert condition.statistics().tasks_waiting == 0 async with condition: assert condition.statistics().lock_statistics.locked assert condition.statistics().tasks_waiting == 0 for i in range(1, 3): tg.start_soon(waiter) await wait_all_tasks_blocked() assert condition.statistics().tasks_waiting == i for i in range(1, -1, -1): async with condition: condition.notify(1) await wait_all_tasks_blocked() assert condition.statistics().tasks_waiting == i assert not condition.statistics().lock_statistics.locked assert condition.statistics().tasks_waiting == 0 def test_instantiate_outside_event_loop( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def use_condition() -> None: async with condition: pass condition = Condition() assert condition.statistics().tasks_waiting == 0 run( use_condition, backend=anyio_backend_name, backend_options=anyio_backend_options, ) class TestSemaphore: async def test_contextmanager(self) -> None: async def acquire() -> None: async with semaphore: assert semaphore.value in (0, 1) semaphore = Semaphore(2) async with create_task_group() as tg: tg.start_soon(acquire, name="task 1") tg.start_soon(acquire, name="task 2") assert semaphore.value == 2 async def test_manual_acquire(self) -> None: async def acquire() -> None: await semaphore.acquire() try: assert semaphore.value in (0, 1) finally: semaphore.release() semaphore = Semaphore(2) async with create_task_group() as tg: tg.start_soon(acquire, name="task 1") tg.start_soon(acquire, name="task 2") assert semaphore.value == 2 async def test_acquire_nowait(self) -> None: semaphore = Semaphore(1) semaphore.acquire_nowait() assert semaphore.value == 0 pytest.raises(WouldBlock, semaphore.acquire_nowait) @pytest.mark.parametrize( "release_first", [pytest.param(False, id="releaselast"), pytest.param(True, id="releasefirst")], ) async def test_cancel_during_acquire(self, release_first: bool) -> None: acquired = False async def task(*, task_status: TaskStatus) -> None: nonlocal acquired task_status.started() async with semaphore: acquired = True semaphore = Semaphore(1) async with create_task_group() as tg: await semaphore.acquire() await tg.start(task) tg.cancel_scope.cancel() with CancelScope(shield=True): if release_first: semaphore.release() await wait_all_tasks_blocked() else: await wait_all_tasks_blocked() semaphore.release() assert not acquired assert semaphore.value == 1 @pytest.mark.parametrize("max_value", [2, None]) async def test_max_value(self, max_value: int | None) -> None: semaphore = Semaphore(0, max_value=max_value) assert semaphore.max_value == max_value async def test_max_value_exceeded(self) -> None: semaphore = Semaphore(1, max_value=2) semaphore.release() pytest.raises(ValueError, semaphore.release) async def test_statistics(self) -> None: async def waiter() -> None: async with semaphore: pass semaphore = Semaphore(1) async with create_task_group() as tg: assert semaphore.statistics().tasks_waiting == 0 async with semaphore: assert semaphore.statistics().tasks_waiting == 0 for i in range(1, 3): tg.start_soon(waiter) await wait_all_tasks_blocked() assert semaphore.statistics().tasks_waiting == i assert semaphore.statistics().tasks_waiting == 0 async def test_acquire_race(self) -> None: """ Test against a race condition: when a task waiting on acquire() is rescheduled but another task snatches the last available slot, the task should not raise WouldBlock. """ semaphore = Semaphore(1) async with create_task_group() as tg: semaphore.acquire_nowait() tg.start_soon(semaphore.acquire) await wait_all_tasks_blocked() semaphore.release() pytest.raises(WouldBlock, semaphore.acquire_nowait) @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_asyncio_deadlock(self) -> None: """Regression test for #398.""" semaphore = Semaphore(1) async def acquire() -> None: async with semaphore: await asyncio.sleep(0) loop = asyncio.get_running_loop() task1 = loop.create_task(acquire()) task2 = loop.create_task(acquire()) await asyncio.sleep(0) task1.cancel() await asyncio.wait_for(task2, 1) def test_instantiate_outside_event_loop( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def use_semaphore() -> None: async with semaphore: pass semaphore = Semaphore(1) assert semaphore.statistics().tasks_waiting == 0 run( use_semaphore, backend=anyio_backend_name, backend_options=anyio_backend_options, ) class TestCapacityLimiter: async def test_bad_init_type(self) -> None: pytest.raises(TypeError, CapacityLimiter, 1.0).match( "total_tokens must be an int or math.inf" ) async def test_bad_init_value(self) -> None: pytest.raises(ValueError, CapacityLimiter, 0).match("total_tokens must be >= 1") async def test_borrow(self) -> None: limiter = CapacityLimiter(2) assert limiter.total_tokens == 2 assert limiter.available_tokens == 2 assert limiter.borrowed_tokens == 0 async with limiter: assert limiter.total_tokens == 2 assert limiter.available_tokens == 1 assert limiter.borrowed_tokens == 1 async def test_limit(self) -> None: value = 0 async def taskfunc() -> None: nonlocal value for _ in range(5): async with limiter: assert value == 0 value = 1 await wait_all_tasks_blocked() value = 0 limiter = CapacityLimiter(1) async with create_task_group() as tg: for _ in range(3): tg.start_soon(taskfunc) async def test_borrow_twice(self) -> None: limiter = CapacityLimiter(1) await limiter.acquire() with pytest.raises(RuntimeError) as exc: await limiter.acquire() exc.match( "this borrower is already holding one of this CapacityLimiter's tokens" ) async def test_bad_release(self) -> None: limiter = CapacityLimiter(1) with pytest.raises(RuntimeError) as exc: limiter.release() exc.match("this borrower isn't holding any of this CapacityLimiter's tokens") async def test_increase_tokens(self) -> None: async def setter() -> None: # Wait until waiter() is inside the limiter block await event1.wait() async with limiter: # This can only happen when total_tokens has been increased event2.set() async def waiter() -> None: async with limiter: event1.set() await event2.wait() limiter = CapacityLimiter(1) event1, event2 = Event(), Event() async with create_task_group() as tg: tg.start_soon(setter) tg.start_soon(waiter) await wait_all_tasks_blocked() assert event1.is_set() assert not event2.is_set() limiter.total_tokens = 2 assert event2.is_set() async def test_current_default_thread_limiter(self) -> None: limiter = to_thread.current_default_thread_limiter() assert isinstance(limiter, CapacityLimiter) assert limiter.total_tokens == 40 async def test_statistics(self) -> None: async def waiter() -> None: async with limiter: pass limiter = CapacityLimiter(1) assert limiter.statistics().total_tokens == 1 assert limiter.statistics().borrowed_tokens == 0 assert limiter.statistics().tasks_waiting == 0 async with create_task_group() as tg: async with limiter: assert limiter.statistics().borrowed_tokens == 1 assert limiter.statistics().tasks_waiting == 0 for i in range(1, 3): tg.start_soon(waiter) await wait_all_tasks_blocked() assert limiter.statistics().tasks_waiting == i assert limiter.statistics().tasks_waiting == 0 assert limiter.statistics().borrowed_tokens == 0 @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_asyncio_deadlock(self) -> None: """Regression test for #398.""" limiter = CapacityLimiter(1) async def acquire() -> None: async with limiter: await asyncio.sleep(0) loop = asyncio.get_running_loop() task1 = loop.create_task(acquire()) task2 = loop.create_task(acquire()) await asyncio.sleep(0) task1.cancel() await asyncio.wait_for(task2, 1) async def test_ordered_queue(self) -> None: limiter = CapacityLimiter(1) results = [] event = Event() async def append(x: int, task_status: TaskStatus) -> None: task_status.started() async with limiter: await event.wait() results.append(x) async with create_task_group() as tg: for i in [0, 1, 2]: await tg.start(append, i) event.set() assert results == [0, 1, 2] async def test_increase_tokens_lets_others_acquire(self) -> None: limiter = CapacityLimiter(1) entered_events = [Event() for _ in range(3)] continue_event = Event() async def worker(entered_event: Event) -> None: async with limiter: entered_event.set() await continue_event.wait() async with create_task_group() as tg: for event in entered_events[:2]: tg.start_soon(worker, event) # One task should be able to acquire the limiter while the other is left # waiting await wait_all_tasks_blocked() assert sum(ev.is_set() for ev in entered_events) == 1 # Increase the total tokens and start another worker. # All tasks should be able to acquire the limiter now. limiter.total_tokens = 3 tg.start_soon(worker, entered_events[2]) with fail_after(1): for ev in entered_events[1:]: await ev.wait() # Allow all tasks to exit continue_event.set() def test_instantiate_outside_event_loop( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def use_limiter() -> None: async with limiter: pass limiter = CapacityLimiter(1) limiter.total_tokens = 2 with pytest.raises(TypeError): limiter.total_tokens = "2" # type: ignore[assignment] with pytest.raises(TypeError): limiter.total_tokens = 3.0 assert limiter.total_tokens == 2 assert limiter.borrowed_tokens == 0 statistics = limiter.statistics() assert statistics.total_tokens == 2 assert statistics.borrowed_tokens == 0 assert statistics.borrowers == () assert statistics.tasks_waiting == 0 run( use_limiter, backend=anyio_backend_name, backend_options=anyio_backend_options, ) async def test_total_tokens_as_kwarg(self) -> None: # Regression test for #515 limiter = CapacityLimiter(total_tokens=1) assert limiter.total_tokens == 1 anyio-4.4.0/tests/test_taskgroups.py000066400000000000000000001206631462472665400176430ustar00rootroot00000000000000from __future__ import annotations import asyncio import math import sys import time from collections.abc import AsyncGenerator, Coroutine, Generator from typing import Any, NoReturn, cast import pytest from exceptiongroup import catch import anyio from anyio import ( TASK_STATUS_IGNORED, CancelScope, create_task_group, current_effective_deadline, current_time, fail_after, get_cancelled_exc_class, get_current_task, move_on_after, sleep, sleep_forever, wait_all_tasks_blocked, ) from anyio.abc import TaskGroup, TaskStatus from anyio.lowlevel import checkpoint if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup, ExceptionGroup pytestmark = pytest.mark.anyio async def async_error(text: str, delay: float = 0.1) -> NoReturn: try: if delay: await sleep(delay) finally: raise Exception(text) async def test_already_closed() -> None: async with create_task_group() as tg: pass with pytest.raises(RuntimeError) as exc: tg.start_soon(async_error, "fail") exc.match("This task group is not active; no new tasks can be started") async def test_success() -> None: async def async_add(value: str) -> None: results.add(value) results: set[str] = set() async with create_task_group() as tg: tg.start_soon(async_add, "a") tg.start_soon(async_add, "b") assert results == {"a", "b"} @pytest.mark.parametrize( "module", [ pytest.param(asyncio, id="asyncio"), pytest.param(pytest.importorskip("trio"), id="trio"), ], ) def test_run_natively(module: Any) -> None: async def testfunc() -> None: async with create_task_group() as tg: tg.start_soon(sleep, 0) if module is asyncio: asyncio.run(testfunc()) else: module.run(testfunc) async def test_start_soon_while_running() -> None: async def task_func() -> None: tg.start_soon(sleep, 0) async with create_task_group() as tg: tg.start_soon(task_func) async def test_start_soon_after_error() -> None: with pytest.raises(ExceptionGroup): async with create_task_group() as tg: a = 1 / 0 # noqa: F841 with pytest.raises(RuntimeError) as exc: tg.start_soon(sleep, 0) exc.match("This task group is not active; no new tasks can be started") async def test_start_no_value() -> None: async def taskfunc(*, task_status: TaskStatus) -> None: task_status.started() async with create_task_group() as tg: value = await tg.start(taskfunc) assert value is None async def test_start_called_twice() -> None: async def taskfunc(*, task_status: TaskStatus) -> None: task_status.started() with pytest.raises( RuntimeError, match="called 'started' twice on the same task status" ): task_status.started() async with create_task_group() as tg: value = await tg.start(taskfunc) assert value is None async def test_no_called_started_twice() -> None: async def taskfunc(*, task_status: TaskStatus) -> None: task_status.started() async with create_task_group() as tg: coro = tg.start(taskfunc) tg.cancel_scope.cancel() await coro async def test_start_with_value() -> None: async def taskfunc(*, task_status: TaskStatus) -> None: task_status.started("foo") async with create_task_group() as tg: value = await tg.start(taskfunc) assert value == "foo" async def test_start_crash_before_started_call() -> None: async def taskfunc(*, task_status: TaskStatus) -> NoReturn: raise Exception("foo") async with create_task_group() as tg: with pytest.raises(Exception) as exc: await tg.start(taskfunc) exc.match("foo") async def test_start_crash_after_started_call() -> None: async def taskfunc(*, task_status: TaskStatus) -> NoReturn: task_status.started(2) raise Exception("foo") with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: value = await tg.start(taskfunc) assert len(exc.value.exceptions) == 1 assert str(exc.value.exceptions[0]) == "foo" assert value == 2 async def test_start_no_started_call() -> None: async def taskfunc(*, task_status: TaskStatus) -> None: pass async with create_task_group() as tg: with pytest.raises(RuntimeError) as exc: await tg.start(taskfunc) exc.match("hild exited") async def test_start_cancelled() -> None: started = finished = False async def taskfunc(*, task_status: TaskStatus) -> None: nonlocal started, finished started = True await sleep(2) finished = True async with create_task_group() as tg: tg.cancel_scope.cancel() await tg.start(taskfunc) assert started assert not finished @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_start_native_host_cancelled() -> None: started = finished = False async def taskfunc(*, task_status: TaskStatus) -> None: nonlocal started, finished started = True await sleep(2) finished = True async def start_another() -> None: async with create_task_group() as tg: await tg.start(taskfunc) task = asyncio.get_running_loop().create_task(start_another()) await wait_all_tasks_blocked() task.cancel() with pytest.raises(asyncio.CancelledError): await task assert started assert not finished @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_start_native_child_cancelled() -> None: task = None finished = False async def taskfunc(*, task_status: TaskStatus) -> None: nonlocal task, finished task = asyncio.current_task() await sleep(2) finished = True async def start_another() -> None: async with create_task_group() as tg2: await tg2.start(taskfunc) async with create_task_group() as tg: tg.start_soon(start_another) await wait_all_tasks_blocked() assert task is not None task.cancel() assert not finished @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_propagate_native_cancellation_from_taskgroup() -> None: async def taskfunc() -> None: async with create_task_group() as tg: tg.start_soon(asyncio.sleep, 2) task = asyncio.create_task(taskfunc()) await wait_all_tasks_blocked() task.cancel() with pytest.raises(asyncio.CancelledError): await task async def test_start_exception_delivery(anyio_backend_name: str) -> None: def task_fn(*, task_status: TaskStatus = TASK_STATUS_IGNORED) -> None: task_status.started("hello") if anyio_backend_name == "trio": pattern = "appears to be synchronous" else: pattern = "is not a coroutine object" async with anyio.create_task_group() as tg: with pytest.raises(TypeError, match=pattern): await tg.start(task_fn) # type: ignore[arg-type] async def test_start_cancel_after_error() -> None: """Regression test for #517.""" sleep_completed = False async def sleep_and_raise() -> None: await wait_all_tasks_blocked() raise RuntimeError("This should cancel the second start() call") async def sleep_only(task_status: TaskStatus[None]) -> None: nonlocal sleep_completed await sleep(1) sleep_completed = True task_status.started() with pytest.raises(ExceptionGroup) as exc: async with anyio.create_task_group() as outer_tg: async with anyio.create_task_group() as inner_tg: inner_tg.start_soon(sleep_and_raise) await outer_tg.start(sleep_only) assert isinstance(exc.value.exceptions[0], ExceptionGroup) assert isinstance(exc.value.exceptions[0].exceptions[0], RuntimeError) assert not sleep_completed async def test_host_exception() -> None: result = None async def set_result(value: str) -> None: nonlocal result await sleep(3) result = value with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(set_result, "a") raise Exception("dummy error") assert len(exc.value.exceptions) == 1 assert str(exc.value.exceptions[0]) == "dummy error" assert result is None async def test_level_cancellation() -> None: marker = None async def dummy() -> None: nonlocal marker marker = 1 # At this point the task has been cancelled so sleep() will raise an exception await sleep(0) # Execution should never get this far marker = 2 async with create_task_group() as tg: tg.start_soon(dummy) assert marker is None tg.cancel_scope.cancel() assert marker == 1 async def test_failing_child_task_cancels_host() -> None: async def child() -> NoReturn: await wait_all_tasks_blocked() raise Exception("foo") sleep_completed = False with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(child) await sleep(0.5) sleep_completed = True assert len(exc.value.exceptions) == 1 assert str(exc.value.exceptions[0]) == "foo" assert not sleep_completed async def test_failing_host_task_cancels_children() -> None: sleep_completed = False async def child() -> None: nonlocal sleep_completed await sleep(1) sleep_completed = True with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(child) await wait_all_tasks_blocked() raise Exception("foo") assert len(exc.value.exceptions) == 1 assert str(exc.value.exceptions[0]) == "foo" assert not sleep_completed async def test_cancel_scope_in_another_task() -> None: local_scope = None result = False async def child() -> None: nonlocal result, local_scope with CancelScope() as local_scope: await sleep(2) result = True async with create_task_group() as tg: tg.start_soon(child) while local_scope is None: await sleep(0) local_scope.cancel() assert not result async def test_cancel_propagation() -> None: async def g() -> NoReturn: async with create_task_group(): await sleep(1) assert False async with create_task_group() as tg: tg.start_soon(g) await sleep(0) tg.cancel_scope.cancel() async def test_cancel_twice() -> None: """Test that the same task can receive two cancellations.""" async def cancel_group() -> None: await wait_all_tasks_blocked() tg.cancel_scope.cancel() for _ in range(2): async with create_task_group() as tg: tg.start_soon(cancel_group) await sleep(1) pytest.fail("Execution should not reach this point") async def test_cancel_exiting_task_group() -> None: """ Test that if a task group is waiting for subtasks to finish and it receives a cancellation, the subtasks are also cancelled and the waiting continues. """ cancel_received = False async def waiter() -> None: nonlocal cancel_received try: await sleep(5) finally: cancel_received = True async def subgroup() -> None: async with create_task_group() as tg2: tg2.start_soon(waiter) async with create_task_group() as tg: tg.start_soon(subgroup) await wait_all_tasks_blocked() tg.cancel_scope.cancel() assert cancel_received async def test_cancel_before_entering_scope() -> None: """ Test that CancelScope.cancel() is honored even if called before entering the scope. """ cancel_scope = anyio.CancelScope() cancel_scope.cancel() with cancel_scope: await anyio.sleep(1) # Checkpoint to allow anyio to check for cancellation pytest.fail("execution should not reach this point") @pytest.mark.xfail( sys.version_info < (3, 11), reason="Requires asyncio.Task.cancelling()" ) @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_cancel_counter_nested_scopes() -> None: with CancelScope() as root_scope: with CancelScope(): root_scope.cancel() await sleep(0.5) assert not cast(asyncio.Task, asyncio.current_task()).cancelling() async def test_exception_group_children() -> None: with pytest.raises(BaseExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(async_error, "task1") tg.start_soon(async_error, "task2", 0.15) assert len(exc.value.exceptions) == 2 assert sorted(str(e) for e in exc.value.exceptions) == ["task1", "task2"] async def test_exception_group_host() -> None: with pytest.raises(BaseExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(async_error, "child", 2) await wait_all_tasks_blocked() raise Exception("host") assert len(exc.value.exceptions) == 2 assert sorted(str(e) for e in exc.value.exceptions) == ["child", "host"] async def test_escaping_cancelled_exception() -> None: async with create_task_group() as tg: tg.cancel_scope.cancel() await sleep(0) async def test_cancel_scope_cleared() -> None: with move_on_after(0.1): await sleep(1) await sleep(0) @pytest.mark.parametrize("delay", [0, 0.1], ids=["instant", "delayed"]) async def test_fail_after(delay: float) -> None: with pytest.raises(TimeoutError): with fail_after(delay) as scope: await sleep(1) assert scope.cancel_called assert scope.cancelled_caught async def test_fail_after_no_timeout() -> None: with fail_after(None) as scope: assert scope.deadline == float("inf") await sleep(0.1) assert not scope.cancel_called assert not scope.cancelled_caught async def test_fail_after_after_cancellation() -> None: event = anyio.Event() async with anyio.create_task_group() as tg: tg.cancel_scope.cancel() await event.wait() block_complete = False with pytest.raises(TimeoutError): with fail_after(0.1): await anyio.sleep(0.5) block_complete = True assert not block_complete async def test_fail_after_cancelled_before_deadline() -> None: """ Test that fail_after() won't raise TimeoutError if its scope is cancelled before the deadline. """ with fail_after(1) as scope: scope.cancel() await checkpoint() @pytest.mark.xfail( reason="There is currently no way to tell if cancellation happened due to timeout " "explicitly if the deadline has been exceeded" ) async def test_fail_after_scope_cancelled_before_timeout() -> None: with fail_after(0.1) as scope: scope.cancel() time.sleep(0.11) # noqa: ASYNC101 await sleep(0) @pytest.mark.parametrize("delay", [0, 0.1], ids=["instant", "delayed"]) async def test_move_on_after(delay: float) -> None: result = False with move_on_after(delay) as scope: await sleep(1) result = True assert not result assert scope.cancel_called assert scope.cancelled_caught async def test_move_on_after_no_timeout() -> None: result = False with move_on_after(None) as scope: assert scope.deadline == float("inf") await sleep(0.1) result = True assert result assert not scope.cancel_called async def test_nested_move_on_after() -> None: sleep_completed = inner_scope_completed = False with move_on_after(0.1) as outer_scope: assert current_effective_deadline() == outer_scope.deadline with move_on_after(1) as inner_scope: assert current_effective_deadline() == outer_scope.deadline await sleep(2) sleep_completed = True inner_scope_completed = True assert not sleep_completed assert not inner_scope_completed assert outer_scope.cancel_called assert outer_scope.cancelled_caught assert not inner_scope.cancel_called assert not inner_scope.cancelled_caught async def test_shielding() -> None: async def cancel_when_ready() -> None: await wait_all_tasks_blocked() tg.cancel_scope.cancel() inner_sleep_completed = outer_sleep_completed = False async with create_task_group() as tg: tg.start_soon(cancel_when_ready) with move_on_after(10, shield=True) as inner_scope: assert inner_scope.shield await sleep(0.1) inner_sleep_completed = True await sleep(1) outer_sleep_completed = True assert inner_sleep_completed assert not outer_sleep_completed assert tg.cancel_scope.cancel_called assert not inner_scope.cancel_called async def test_cancel_from_shielded_scope() -> None: async with create_task_group() as tg: with CancelScope(shield=True) as inner_scope: assert inner_scope.shield tg.cancel_scope.cancel() assert current_effective_deadline() == math.inf assert current_effective_deadline() == -math.inf with pytest.raises(get_cancelled_exc_class()): await sleep(0.01) with pytest.raises(get_cancelled_exc_class()): await sleep(0.01) async def test_cancel_shielded_scope() -> None: with CancelScope(shield=True) as cancel_scope: assert cancel_scope.shield cancel_scope.cancel() assert current_effective_deadline() == -math.inf with pytest.raises(get_cancelled_exc_class()): await sleep(0) async def test_cancelled_not_caught() -> None: with CancelScope() as scope: scope.cancel() assert scope.cancel_called assert not scope.cancelled_caught @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_cancel_host_asyncgen() -> None: done = False async def host_task() -> None: nonlocal done async with create_task_group() as tg: with CancelScope(shield=True) as inner_scope: assert inner_scope.shield tg.cancel_scope.cancel() with pytest.raises(get_cancelled_exc_class()): await sleep(0) with pytest.raises(get_cancelled_exc_class()): await sleep(0) done = True async def host_agen_fn() -> AsyncGenerator[None, None]: await host_task() yield pytest.fail("host_agen_fn should only be __anext__ed once") host_agen = host_agen_fn() try: loop = asyncio.get_running_loop() await loop.create_task(host_agen.__anext__()) # type: ignore[arg-type] finally: await host_agen.aclose() assert done async def test_shielding_immediate_scope_cancelled() -> None: async def cancel_when_ready() -> None: await wait_all_tasks_blocked() scope.cancel() sleep_completed = False async with create_task_group() as tg: with CancelScope(shield=True) as scope: tg.start_soon(cancel_when_ready) await sleep(0.5) sleep_completed = True assert not sleep_completed async def test_shielding_mutate() -> None: completed = False async def task(task_status: TaskStatus) -> NoReturn: nonlocal completed with CancelScope() as scope: # Enable the shield a little after the scope starts to make this test # general, even though it has no bearing on the current implementation. await sleep(0.1) scope.shield = True task_status.started() await sleep(0.1) completed = True scope.shield = False await sleep(1) pytest.fail("Execution should not reach this point") async with create_task_group() as tg: await tg.start(task) tg.cancel_scope.cancel() assert completed async def test_cancel_scope_in_child_task() -> None: child_scope = None async def child() -> None: nonlocal child_scope with CancelScope() as child_scope: await sleep(2) host_done = False async with create_task_group() as tg: tg.start_soon(child) await wait_all_tasks_blocked() assert child_scope is not None child_scope.cancel() await sleep(0.1) host_done = True assert host_done assert not tg.cancel_scope.cancel_called async def test_exception_cancels_siblings() -> None: sleep_completed = False async def child(fail: bool) -> None: if fail: raise Exception("foo") else: nonlocal sleep_completed await sleep(1) sleep_completed = True with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(child, False) await wait_all_tasks_blocked() tg.start_soon(child, True) assert len(exc.value.exceptions) == 1 assert str(exc.value.exceptions[0]) == "foo" assert not sleep_completed async def test_cancel_cascade() -> None: async def do_something() -> NoReturn: async with create_task_group() as tg2: tg2.start_soon(sleep, 1) raise Exception("foo") async with create_task_group() as tg: tg.start_soon(do_something) await wait_all_tasks_blocked() tg.cancel_scope.cancel() async def test_cancelled_parent() -> None: async def child() -> NoReturn: with CancelScope(): await sleep(1) raise Exception("foo") async def parent(tg: TaskGroup) -> None: await wait_all_tasks_blocked() tg.start_soon(child) async with create_task_group() as tg: tg.start_soon(parent, tg) tg.cancel_scope.cancel() async def test_shielded_deadline() -> None: with move_on_after(10): with CancelScope(shield=True): with move_on_after(1000): assert current_effective_deadline() - current_time() > 900 async def test_deadline_reached_on_start() -> None: with move_on_after(0): await sleep(0) pytest.fail("Execution should not reach this point") async def test_deadline_moved() -> None: with fail_after(0.1) as scope: scope.deadline += 0.3 await sleep(0.2) async def test_timeout_error_with_multiple_cancellations() -> None: with pytest.raises(TimeoutError): with fail_after(0.1): async with create_task_group() as tg: tg.start_soon(sleep, 2) await sleep(2) async def test_nested_fail_after() -> None: async def killer(scope: CancelScope) -> None: await wait_all_tasks_blocked() scope.cancel() async with create_task_group() as tg: with CancelScope() as scope: with CancelScope(): tg.start_soon(killer, scope) with fail_after(1): await sleep(2) pytest.fail("Execution should not reach this point") pytest.fail("Execution should not reach this point either") pytest.fail("Execution should also not reach this point") assert scope.cancel_called async def test_nested_shield() -> None: async def killer(scope: CancelScope) -> None: await wait_all_tasks_blocked() scope.cancel() with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: with CancelScope() as scope: with CancelScope(shield=True): tg.start_soon(killer, scope) with fail_after(0.2): await sleep(2) assert len(exc.value.exceptions) == 1 assert isinstance(exc.value.exceptions[0], TimeoutError) async def test_triple_nested_shield_checkpoint_in_outer() -> None: """Regression test for #370.""" got_past_checkpoint = False async def taskfunc() -> None: nonlocal got_past_checkpoint with CancelScope() as scope1: with CancelScope() as scope2: with CancelScope(shield=True): scope1.cancel() scope2.cancel() await checkpoint() got_past_checkpoint = True async with create_task_group() as tg: tg.start_soon(taskfunc) assert not got_past_checkpoint async def test_triple_nested_shield_checkpoint_in_middle() -> None: got_past_checkpoint = False async def taskfunc() -> None: nonlocal got_past_checkpoint with CancelScope() as scope1: with CancelScope(): with CancelScope(shield=True): scope1.cancel() await checkpoint() got_past_checkpoint = True async with create_task_group() as tg: tg.start_soon(taskfunc) assert not got_past_checkpoint def test_task_group_in_generator( anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> None: async def task_group_generator() -> AsyncGenerator[None, None]: async with create_task_group(): yield gen = task_group_generator() anyio.run( gen.__anext__, backend=anyio_backend_name, backend_options=anyio_backend_options, ) pytest.raises( StopAsyncIteration, anyio.run, gen.__anext__, backend=anyio_backend_name, backend_options=anyio_backend_options, ) async def test_exception_group_filtering() -> None: """Test that CancelledErrors are filtered out of nested exception groups.""" async def fail(name: str) -> NoReturn: try: await anyio.sleep(0.1) finally: raise Exception(f"{name} task failed") async def fn() -> None: async with anyio.create_task_group() as tg: tg.start_soon(fail, "parent") async with anyio.create_task_group() as tg2: tg2.start_soon(fail, "child") await anyio.sleep(1) with pytest.raises(BaseExceptionGroup) as exc: await fn() assert len(exc.value.exceptions) == 2 assert str(exc.value.exceptions[0]) == "parent task failed" assert isinstance(exc.value.exceptions[1], ExceptionGroup) assert len(exc.value.exceptions[1].exceptions) == 1 assert str(exc.value.exceptions[1].exceptions[0]) == "child task failed" async def test_cancel_propagation_with_inner_spawn() -> None: async def g() -> NoReturn: async with anyio.create_task_group() as tg2: tg2.start_soon(anyio.sleep, 10) await anyio.sleep(1) assert False async with anyio.create_task_group() as tg: tg.start_soon(g) await wait_all_tasks_blocked() tg.cancel_scope.cancel() async def test_escaping_cancelled_error_from_cancelled_task() -> None: """ Regression test for issue #88. No CancelledError should escape the outer scope. """ with CancelScope() as scope: with move_on_after(0.1): await sleep(1) scope.cancel() @pytest.mark.skipif( sys.version_info >= (3, 11), reason="Generator based coroutines have been removed in Python 3.11", ) @pytest.mark.filterwarnings( 'ignore:"@coroutine" decorator is deprecated:DeprecationWarning' ) def test_cancel_generator_based_task() -> None: async def native_coro_part() -> None: with CancelScope() as scope: asyncio.get_running_loop().call_soon(scope.cancel) await asyncio.sleep(1) pytest.fail("Execution should not have reached this line") @asyncio.coroutine # type: ignore[attr-defined] def generator_part() -> Generator[object, BaseException, None]: yield from native_coro_part() # type: ignore[misc] anyio.run(generator_part, backend="asyncio") @pytest.mark.skipif( sys.version_info >= (3, 11), reason="Generator based coroutines have been removed in Python 3.11", ) @pytest.mark.filterwarnings( 'ignore:"@coroutine" decorator is deprecated:DeprecationWarning' ) @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_schedule_old_style_coroutine_func() -> None: """ Test that we give a sensible error when a user tries to spawn a task from a generator-style coroutine function. """ @asyncio.coroutine # type: ignore[attr-defined] def corofunc() -> Generator[Any, Any, None]: yield from asyncio.sleep(1) # type: ignore[misc] async with create_task_group() as tg: funcname = ( f"{__name__}.test_schedule_old_style_coroutine_func..corofunc" ) with pytest.raises( TypeError, match=f"Expected {funcname}\\(\\) to return a coroutine, but the return " f"value \\(\\) is not a coroutine object", ): tg.start_soon(corofunc) @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_cancel_native_future_tasks() -> None: async def wait_native_future() -> None: loop = asyncio.get_running_loop() await loop.create_future() async with anyio.create_task_group() as tg: tg.start_soon(wait_native_future) tg.cancel_scope.cancel() @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_cancel_native_future_tasks_cancel_scope() -> None: async def wait_native_future() -> None: with anyio.CancelScope(): loop = asyncio.get_running_loop() await loop.create_future() async with anyio.create_task_group() as tg: tg.start_soon(wait_native_future) tg.cancel_scope.cancel() @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_cancel_completed_task() -> None: loop = asyncio.get_running_loop() old_exception_handler = loop.get_exception_handler() exceptions = [] def exception_handler(*args: object, **kwargs: object) -> None: exceptions.append((args, kwargs)) loop.set_exception_handler(exception_handler) try: async def noop() -> None: pass async with anyio.create_task_group() as tg: tg.start_soon(noop) tg.cancel_scope.cancel() assert exceptions == [] finally: loop.set_exception_handler(old_exception_handler) async def test_task_in_sync_spawn_callback() -> None: outer_task_id = anyio.get_current_task().id inner_task_id = None def task_wrap() -> Coroutine[object, object, None]: assert anyio.get_current_task().id == outer_task_id async def corofn() -> None: nonlocal inner_task_id inner_task_id = anyio.get_current_task().id return corofn() async with create_task_group() as tg: tg.start_soon(task_wrap) assert inner_task_id is not None assert inner_task_id != outer_task_id async def test_shielded_cancel_sleep_time() -> None: """ Test that cancelling a shielded tasks spends more time sleeping than cancelling. """ event = anyio.Event() hang_time = 0.2 async def set_event() -> None: await sleep(hang_time) event.set() async def never_cancel_task() -> None: with CancelScope(shield=True): await sleep(0.2) await event.wait() async with create_task_group() as tg: tg.start_soon(set_event) async with create_task_group() as tg: tg.start_soon(never_cancel_task) tg.cancel_scope.cancel() process_time = time.process_time() assert (time.process_time() - process_time) < hang_time async def test_cancelscope_wrong_exit_order() -> None: """ Test that a RuntimeError is raised if the task tries to exit cancel scopes in the wrong order. """ scope1 = CancelScope() scope2 = CancelScope() scope1.__enter__() scope2.__enter__() pytest.raises(RuntimeError, scope1.__exit__, None, None, None) async def test_cancelscope_exit_before_enter() -> None: """ Test that a RuntimeError is raised if one tries to exit a cancel scope before entering. """ scope = CancelScope() pytest.raises(RuntimeError, scope.__exit__, None, None, None) @pytest.mark.parametrize( "anyio_backend", ["asyncio"] ) # trio does not check for this yet async def test_cancelscope_exit_in_wrong_task() -> None: async def enter_scope(scope: CancelScope) -> None: scope.__enter__() async def exit_scope(scope: CancelScope) -> None: scope.__exit__(None, None, None) scope = CancelScope() async with create_task_group() as tg: tg.start_soon(enter_scope, scope) with pytest.raises(ExceptionGroup) as exc: async with create_task_group() as tg: tg.start_soon(exit_scope, scope) assert len(exc.value.exceptions) == 1 assert str(exc.value.exceptions[0]) == ( "Attempted to exit cancel scope in a different task than it was entered in" ) def test_unhandled_exception_group(caplog: pytest.LogCaptureFixture) -> None: def crash() -> NoReturn: raise KeyboardInterrupt async def nested() -> None: async with anyio.create_task_group() as tg: tg.start_soon(anyio.sleep, 5) await anyio.sleep(5) async def main() -> NoReturn: async with anyio.create_task_group() as tg: tg.start_soon(nested) await wait_all_tasks_blocked() asyncio.get_running_loop().call_soon(crash) await anyio.sleep(5) pytest.fail("Execution should never reach this point") with pytest.raises(KeyboardInterrupt): anyio.run(main, backend="asyncio") assert not caplog.messages async def test_single_cancellation_exc() -> None: """ Test that only a single cancellation exception bubbles out of the task group when case it was cancelled via an outer scope and no actual errors were raised. """ with CancelScope() as outer: try: async with create_task_group() as tg: tg.start_soon(sleep, 5) await wait_all_tasks_blocked() outer.cancel() await sleep(5) except BaseException as exc: if isinstance(exc, get_cancelled_exc_class()): raise pytest.fail(f"Raised the wrong type of exception: {exc}") else: pytest.fail("Did not raise a cancellation exception") async def test_start_soon_parent_id() -> None: root_task_id = get_current_task().id parent_id: int | None = None async def subtask() -> None: nonlocal parent_id parent_id = get_current_task().parent_id async def starter_task() -> None: tg.start_soon(subtask) async with anyio.create_task_group() as tg: tg.start_soon(starter_task) assert parent_id == root_task_id async def test_start_parent_id() -> None: root_task_id = get_current_task().id starter_task_id: int | None = None initial_parent_id: int | None = None permanent_parent_id: int | None = None async def subtask(*, task_status: TaskStatus) -> None: nonlocal initial_parent_id, permanent_parent_id initial_parent_id = get_current_task().parent_id task_status.started() permanent_parent_id = get_current_task().parent_id async def starter_task() -> None: nonlocal starter_task_id starter_task_id = get_current_task().id await tg.start(subtask) async with anyio.create_task_group() as tg: tg.start_soon(starter_task) assert initial_parent_id != permanent_parent_id assert initial_parent_id == starter_task_id assert permanent_parent_id == root_task_id @pytest.mark.skipif( sys.version_info < (3, 11), reason="Task uncancelling is only supported on Python 3.11", ) @pytest.mark.parametrize("anyio_backend", ["asyncio"]) class TestUncancel: async def test_uncancel_after_native_cancel(self) -> None: task = cast(asyncio.Task, asyncio.current_task()) with pytest.raises(asyncio.CancelledError), CancelScope(): task.cancel() await anyio.sleep(0) assert task.cancelling() == 1 task.uncancel() async def test_uncancel_after_scope_cancel(self) -> None: task = cast(asyncio.Task, asyncio.current_task()) with CancelScope() as scope: scope.cancel() await anyio.sleep(0) assert task.cancelling() == 0 async def test_uncancel_after_scope_and_native_cancel(self) -> None: task = cast(asyncio.Task, asyncio.current_task()) with pytest.raises(asyncio.CancelledError), CancelScope() as scope: scope.cancel() task.cancel() await anyio.sleep(0) assert task.cancelling() == 1 task.uncancel() async def test_cancel_message_replaced(self) -> None: task = asyncio.current_task() assert task try: task.cancel() await anyio.sleep(0) except asyncio.CancelledError: try: with CancelScope() as scope: scope.cancel() try: await anyio.sleep(0) except asyncio.CancelledError: raise asyncio.CancelledError except asyncio.CancelledError: pytest.fail("Should have swallowed the CancelledError") async def test_cancel_before_entering_task_group() -> None: with CancelScope() as scope: scope.cancel() try: async with create_task_group(): pass except get_cancelled_exc_class(): pytest.fail("This should not raise a cancellation exception") async def test_reraise_cancelled_in_excgroup() -> None: def handler(excgrp: BaseExceptionGroup) -> None: raise with CancelScope() as scope: scope.cancel() with catch({get_cancelled_exc_class(): handler}): await anyio.sleep_forever() async def test_cancel_child_task_when_host_is_shielded() -> None: # Regression test for #642 # Tests that cancellation propagates to a child task even if the host task is within # a shielded cancel scope. cancelled = anyio.Event() async def wait_cancel() -> None: try: await anyio.sleep_forever() except anyio.get_cancelled_exc_class(): cancelled.set() raise with CancelScope() as parent_scope: async with anyio.create_task_group() as task_group: task_group.start_soon(wait_cancel) await wait_all_tasks_blocked() with CancelScope(shield=True), fail_after(1): parent_scope.cancel() await cancelled.wait() async def test_start_cancels_parent_scope() -> None: """Regression test for #685 / #710.""" started: bool = False async def in_task_group(task_status: TaskStatus[None]) -> None: nonlocal started started = True await sleep_forever() async with create_task_group() as tg: with CancelScope() as inner_scope: inner_scope.cancel() await tg.start(in_task_group) assert started assert not tg.cancel_scope.cancel_called class TestTaskStatusTyping: """ These tests do not do anything at run time, but since the test suite is also checked with a static type checker, it ensures that the `TaskStatus` typing works as intended. """ async def typetest_None(*, task_status: TaskStatus[None]) -> None: task_status.started() task_status.started(None) async def typetest_None_Union(*, task_status: TaskStatus[int | None]) -> None: task_status.started() task_status.started(None) async def typetest_non_None(*, task_status: TaskStatus[int]) -> None: # We use `type: ignore` and `--warn-unused-ignores` to get type checking errors # if these ever stop failing. task_status.started() # type: ignore[call-arg] task_status.started(None) # type: ignore[arg-type] async def typetest_variance_good(*, task_status: TaskStatus[float]) -> None: task_status2: TaskStatus[int] = task_status task_status2.started(0) async def typetest_variance_bad(*, task_status: TaskStatus[int]) -> None: # We use `type: ignore` and `--warn-unused-ignores` to get type checking errors # if these ever stop failing. task_status2: TaskStatus[float] = task_status # type: ignore[assignment] task_status2.started(0.0) async def typetest_optional_status( *, task_status: TaskStatus[int] = TASK_STATUS_IGNORED, ) -> None: task_status.started(1) anyio-4.4.0/tests/test_to_process.py000066400000000000000000000066271462472665400176240ustar00rootroot00000000000000from __future__ import annotations import os import sys import time from functools import partial from unittest.mock import Mock import pytest from anyio import ( CancelScope, create_task_group, fail_after, to_process, wait_all_tasks_blocked, ) from anyio.abc import Process pytestmark = pytest.mark.anyio async def test_run_sync_in_process_pool() -> None: """ Test that the function runs in a different process, and the same process in both calls. """ worker_pid = await to_process.run_sync(os.getpid) assert worker_pid != os.getpid() assert await to_process.run_sync(os.getpid) == worker_pid async def test_identical_sys_path() -> None: """Test that partial() can be used to pass keyword arguments.""" assert await to_process.run_sync(eval, "sys.path") == sys.path async def test_partial() -> None: """Test that partial() can be used to pass keyword arguments.""" assert await to_process.run_sync(partial(sorted, reverse=True), ["a", "b"]) == [ "b", "a", ] async def test_exception() -> None: """Test that exceptions are delivered properly.""" with pytest.raises(ValueError, match="invalid literal for int"): assert await to_process.run_sync(int, "a") async def test_print() -> None: """Test that print() won't interfere with parent-worker communication.""" worker_pid = await to_process.run_sync(os.getpid) await to_process.run_sync(print, "hello") await to_process.run_sync(print, "world") assert await to_process.run_sync(os.getpid) == worker_pid async def test_cancel_before() -> None: """ Test that starting to_process.run_sync() in a cancelled scope does not cause a worker process to be reserved. """ with CancelScope() as scope: scope.cancel() await to_process.run_sync(os.getpid) pytest.raises(LookupError, to_process._process_pool_workers.get) async def test_cancel_during() -> None: """ Test that cancelling an operation on the worker process causes the process to be killed. """ worker_pid = await to_process.run_sync(os.getpid) with fail_after(4): async with create_task_group() as tg: tg.start_soon(partial(to_process.run_sync, cancellable=True), time.sleep, 5) await wait_all_tasks_blocked() tg.cancel_scope.cancel() # The previous worker was killed so we should get a new one now assert await to_process.run_sync(os.getpid) != worker_pid async def test_exec_while_pruning() -> None: """ Test that in the case when one or more idle workers are pruned, the originally selected idle worker is re-added to the queue of idle workers. """ worker_pid1 = await to_process.run_sync(os.getpid) workers = to_process._process_pool_workers.get() idle_workers = to_process._process_pool_idle_workers.get() real_worker = next(iter(workers)) fake_idle_process = Mock(Process) workers.add(fake_idle_process) try: # Add a mock worker process that's guaranteed to be eligible for pruning idle_workers.appendleft( (fake_idle_process, -to_process.WORKER_MAX_IDLE_TIME - 1) ) worker_pid2 = await to_process.run_sync(os.getpid) assert worker_pid1 == worker_pid2 fake_idle_process.kill.assert_called_once_with() assert idle_workers[0][0] is real_worker finally: workers.discard(fake_idle_process) anyio-4.4.0/tests/test_to_thread.py000066400000000000000000000245641462472665400174150ustar00rootroot00000000000000from __future__ import annotations import asyncio import threading import time from concurrent.futures import Future, ThreadPoolExecutor from contextvars import ContextVar from functools import partial from typing import Any, NoReturn import pytest import sniffio import anyio.to_thread from anyio import ( CapacityLimiter, Event, create_task_group, from_thread, sleep, to_thread, wait_all_tasks_blocked, ) from anyio.from_thread import BlockingPortalProvider pytestmark = pytest.mark.anyio async def test_run_in_thread_cancelled() -> None: state = 0 def thread_worker() -> None: nonlocal state state = 2 async def worker() -> None: nonlocal state state = 1 await to_thread.run_sync(thread_worker) state = 3 async with create_task_group() as tg: tg.start_soon(worker) tg.cancel_scope.cancel() assert state == 1 async def test_run_in_thread_exception() -> None: def thread_worker() -> NoReturn: raise ValueError("foo") with pytest.raises(ValueError) as exc: await to_thread.run_sync(thread_worker) exc.match("^foo$") async def test_run_in_custom_limiter() -> None: max_active_threads = 0 def thread_worker() -> None: nonlocal max_active_threads active_threads.add(threading.current_thread()) max_active_threads = max(max_active_threads, len(active_threads)) event.wait(1) active_threads.remove(threading.current_thread()) async def task_worker() -> None: await to_thread.run_sync(thread_worker, limiter=limiter) event = threading.Event() limiter = CapacityLimiter(3) active_threads: set[threading.Thread] = set() async with create_task_group() as tg: for _ in range(4): tg.start_soon(task_worker) await sleep(0.1) assert len(active_threads) == 3 assert limiter.borrowed_tokens == 3 event.set() assert len(active_threads) == 0 assert max_active_threads == 3 @pytest.mark.parametrize( "abandon_on_cancel, expected_last_active", [ pytest.param(False, "task", id="noabandon"), pytest.param(True, "thread", id="abandon"), ], ) async def test_cancel_worker_thread( abandon_on_cancel: bool, expected_last_active: str ) -> None: """ Test that when a task running a worker thread is cancelled, the cancellation is not acted on until the thread finishes. """ last_active: str | None = None def thread_worker() -> None: nonlocal last_active from_thread.run_sync(sleep_event.set) time.sleep(0.2) last_active = "thread" from_thread.run_sync(finish_event.set) async def task_worker() -> None: nonlocal last_active try: await to_thread.run_sync(thread_worker, abandon_on_cancel=abandon_on_cancel) finally: last_active = "task" sleep_event = Event() finish_event = Event() async with create_task_group() as tg: tg.start_soon(task_worker) await sleep_event.wait() tg.cancel_scope.cancel() await finish_event.wait() assert last_active == expected_last_active async def test_cancel_wait_on_thread() -> None: event = threading.Event() future: Future[bool] = Future() def wait_event() -> None: future.set_result(event.wait(1)) async with create_task_group() as tg: tg.start_soon(partial(to_thread.run_sync, abandon_on_cancel=True), wait_event) await wait_all_tasks_blocked() tg.cancel_scope.cancel() await to_thread.run_sync(event.set) assert future.result(1) async def test_deprecated_cancellable_param() -> None: with pytest.warns(DeprecationWarning, match="The `cancellable=`"): await to_thread.run_sync(bool, cancellable=True) async def test_contextvar_propagation() -> None: var = ContextVar("var", default=1) var.set(6) assert await to_thread.run_sync(var.get) == 6 async def test_asynclib_detection() -> None: with pytest.raises(sniffio.AsyncLibraryNotFoundError): await to_thread.run_sync(sniffio.current_async_library) @pytest.mark.parametrize("anyio_backend", ["asyncio"]) async def test_asyncio_cancel_native_task() -> None: task: asyncio.Task[None] | None = None async def run_in_thread() -> None: nonlocal task task = asyncio.current_task() await to_thread.run_sync(time.sleep, 0.2, abandon_on_cancel=True) async with create_task_group() as tg: tg.start_soon(run_in_thread) await wait_all_tasks_blocked() assert task is not None task.cancel() def test_asyncio_no_root_task(asyncio_event_loop: asyncio.AbstractEventLoop) -> None: """ Regression test for #264. Ensures that to_thread.run_sync() does not raise an error when there is no root task, but instead tries to find the top most parent task by traversing the cancel scope tree, or failing that, uses the current task to set up a shutdown callback. """ async def run_in_thread() -> None: try: await to_thread.run_sync(time.sleep, 0) finally: asyncio_event_loop.call_soon(asyncio_event_loop.stop) task = asyncio_event_loop.create_task(run_in_thread()) asyncio_event_loop.run_forever() task.result() # Wait for worker threads to exit for t in threading.enumerate(): if t.name == "AnyIO worker thread": t.join(2) assert not t.is_alive() def test_asyncio_future_callback_partial( asyncio_event_loop: asyncio.AbstractEventLoop, ) -> None: """ Regression test for #272. Ensures that futures with partial callbacks are handled correctly when the root task cannot be determined. """ def func(future: object) -> None: pass async def sleep_sync() -> None: return await to_thread.run_sync(time.sleep, 0) task = asyncio_event_loop.create_task(sleep_sync()) task.add_done_callback(partial(func)) asyncio_event_loop.run_until_complete(task) def test_asyncio_run_sync_no_asyncio_run( asyncio_event_loop: asyncio.AbstractEventLoop, ) -> None: """Test that the thread pool shutdown callback does not raise an exception.""" def exception_handler(loop: object, context: Any = None) -> None: exceptions.append(context["exception"]) exceptions: list[BaseException] = [] asyncio_event_loop.set_exception_handler(exception_handler) asyncio_event_loop.run_until_complete(to_thread.run_sync(time.sleep, 0)) assert not exceptions def test_asyncio_run_sync_multiple( asyncio_event_loop: asyncio.AbstractEventLoop, ) -> None: """Regression test for #304.""" asyncio_event_loop.call_later(0.5, asyncio_event_loop.stop) for _ in range(3): asyncio_event_loop.run_until_complete(to_thread.run_sync(time.sleep, 0)) for t in threading.enumerate(): if t.name == "AnyIO worker thread": t.join(2) assert not t.is_alive() def test_asyncio_no_recycle_stopping_worker( asyncio_event_loop: asyncio.AbstractEventLoop, ) -> None: """Regression test for #323.""" async def taskfunc1() -> None: await anyio.to_thread.run_sync(time.sleep, 0) event1.set() await event2.wait() async def taskfunc2() -> None: await event1.wait() asyncio_event_loop.call_soon(event2.set) await anyio.to_thread.run_sync(time.sleep, 0) # At this point, the worker would be stopped but still in the idle workers pool, # so the following would hang prior to the fix await anyio.to_thread.run_sync(time.sleep, 0) event1 = asyncio.Event() event2 = asyncio.Event() task1 = asyncio_event_loop.create_task(taskfunc1()) task2 = asyncio_event_loop.create_task(taskfunc2()) asyncio_event_loop.run_until_complete(asyncio.gather(task1, task2)) async def test_stopiteration() -> None: """ Test that raising StopIteration in a worker thread raises a RuntimeError on the caller. """ def raise_stopiteration() -> NoReturn: raise StopIteration with pytest.raises(RuntimeError, match="coroutine raised StopIteration"): await to_thread.run_sync(raise_stopiteration) class TestBlockingPortalProvider: @pytest.fixture def provider( self, anyio_backend_name: str, anyio_backend_options: dict[str, Any] ) -> BlockingPortalProvider: return BlockingPortalProvider( backend=anyio_backend_name, backend_options=anyio_backend_options ) def test_single_thread( self, provider: BlockingPortalProvider, anyio_backend_name: str ) -> None: threads: set[threading.Thread] = set() async def check_thread() -> None: assert sniffio.current_async_library() == anyio_backend_name threads.add(threading.current_thread()) active_threads_before = threading.active_count() for _ in range(3): with provider as portal: portal.call(check_thread) assert len(threads) == 3 assert threading.active_count() == active_threads_before def test_single_thread_overlapping( self, provider: BlockingPortalProvider, anyio_backend_name: str ) -> None: threads: set[threading.Thread] = set() async def check_thread() -> None: assert sniffio.current_async_library() == anyio_backend_name threads.add(threading.current_thread()) with provider as portal1: with provider as portal2: assert portal1 is portal2 portal2.call(check_thread) portal1.call(check_thread) assert len(threads) == 1 def test_multiple_threads( self, provider: BlockingPortalProvider, anyio_backend_name: str ) -> None: threads: set[threading.Thread] = set() event = Event() async def check_thread() -> None: assert sniffio.current_async_library() == anyio_backend_name await event.wait() threads.add(threading.current_thread()) def dummy() -> None: with provider as portal: portal.call(check_thread) with ThreadPoolExecutor(max_workers=3) as pool: for _ in range(3): pool.submit(dummy) with provider as portal: portal.call(wait_all_tasks_blocked) portal.call(event.set) assert len(threads) == 1 anyio-4.4.0/tests/test_typedattr.py000066400000000000000000000012041462472665400174460ustar00rootroot00000000000000from __future__ import annotations from typing import Any, Callable, Mapping import pytest from anyio import TypedAttributeProvider class DummyAttributeProvider(TypedAttributeProvider): def get_dummyattr(self) -> str: raise KeyError("foo") @property def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: return {str: self.get_dummyattr} def test_typedattr_keyerror() -> None: """ Test that if the extra attribute getter raises KeyError, it won't be confused for a missing attribute. """ with pytest.raises(KeyError, match="^'foo'$"): DummyAttributeProvider().extra(str)