├── asgiref ├── py.typed ├── __init__.py ├── compatibility.py ├── timeout.py ├── current_thread_executor.py ├── testing.py ├── local.py ├── server.py ├── wsgi.py ├── typing.py └── sync.py ├── docs ├── requirements.txt ├── specs │ ├── main.rst │ ├── tls.rst │ ├── www.rst │ ├── lifespan.rst │ └── index.rst ├── Makefile ├── index.rst ├── introduction.rst ├── conf.py ├── implementations.rst └── extensions.rst ├── setup.py ├── MANIFEST.in ├── Makefile ├── .gitignore ├── .readthedocs.yaml ├── tox.ini ├── .pre-commit-config.yaml ├── .github └── workflows │ └── tests.yml ├── LICENSE ├── tests ├── test_garbage_collection.py ├── test_compatibility.py ├── test_testing.py ├── test_sync_contextvars.py ├── test_server.py ├── test_wsgi.py └── test_local.py ├── setup.cfg ├── specs ├── lifespan.rst ├── tls.rst └── asgi.rst ├── README.rst └── CHANGELOG.txt /asgiref/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /asgiref/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "3.11.0" 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx_rtd_theme 3 | -------------------------------------------------------------------------------- /docs/specs/main.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../specs/asgi.rst 2 | -------------------------------------------------------------------------------- /docs/specs/tls.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../specs/tls.rst 2 | -------------------------------------------------------------------------------- /docs/specs/www.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../specs/www.rst 2 | -------------------------------------------------------------------------------- /docs/specs/lifespan.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../specs/lifespan.rst 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup # type: ignore[import-untyped] 2 | 3 | setup() 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include asgiref/py.typed 3 | include tox.ini 4 | recursive-include tests *.py 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: release clean 2 | 3 | clean: 4 | rm -rf build/ dist/ asgiref.egg-info/ 5 | 6 | release: clean 7 | python3 -m build 8 | python3 -m twine upload dist/* 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | dist/ 3 | build/ 4 | _build/ 5 | __pycache__/ 6 | *.pyc 7 | .tox/ 8 | *~ 9 | .cache 10 | .eggs 11 | .python-version 12 | .pytest_cache/ 13 | .vscode/ 14 | .venv 15 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: ubuntu-22.04 4 | tools: 5 | python: "3.12" 6 | 7 | python: 8 | install: 9 | - requirements: docs/requirements.txt 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | formats: 15 | - pdf 16 | - epub 17 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{39,310,311,312,313,314}-{test,mypy} 4 | qa 5 | 6 | [testenv] 7 | usedevelop = true 8 | extras = tests 9 | commands = 10 | test: pytest -v {posargs} 11 | mypy: mypy . {posargs} 12 | deps = 13 | setuptools 14 | 15 | [testenv:qa] 16 | skip_install = true 17 | deps = 18 | pre-commit 19 | commands = 20 | pre-commit {posargs:run --all-files --show-diff-on-failure} 21 | -------------------------------------------------------------------------------- /docs/specs/index.rst: -------------------------------------------------------------------------------- 1 | Specifications 2 | ============== 3 | 4 | These are the specifications for ASGI. The root specification outlines how 5 | applications are structured and called, and the protocol specifications outline 6 | the events that can be sent and received for each protocol. 7 | 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | ASGI Specification
13 | HTTP and WebSocket protocol 14 | Lifespan 15 | TLS Extension 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = ASGI 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/asottile/pyupgrade 3 | rev: v3.3.1 4 | hooks: 5 | - id: pyupgrade 6 | args: ["--py38-plus"] 7 | 8 | - repo: https://github.com/psf/black 9 | rev: 22.12.0 10 | hooks: 11 | - id: black 12 | args: ["--target-version=py38"] 13 | 14 | - repo: https://github.com/pycqa/isort 15 | rev: 5.11.5 16 | hooks: 17 | - id: isort 18 | args: ["--profile=black"] 19 | 20 | - repo: https://github.com/pycqa/flake8 21 | rev: 6.0.0 22 | hooks: 23 | - id: flake8 24 | 25 | - repo: https://github.com/asottile/yesqa 26 | rev: v1.4.0 27 | hooks: 28 | - id: yesqa 29 | 30 | - repo: https://github.com/pre-commit/pre-commit-hooks 31 | rev: v4.4.0 32 | hooks: 33 | - id: check-merge-conflict 34 | - id: check-toml 35 | - id: check-yaml 36 | - id: mixed-line-ending 37 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | 2 | ASGI Documentation 3 | ================== 4 | 5 | ASGI (*Asynchronous Server Gateway Interface*) is a spiritual successor to 6 | WSGI, intended to provide a standard interface between async-capable Python 7 | web servers, frameworks, and applications. 8 | 9 | Where WSGI provided a standard for synchronous Python apps, ASGI provides one 10 | for both asynchronous and synchronous apps, with a WSGI backwards-compatibility 11 | implementation and multiple servers and application frameworks. 12 | 13 | You can read more in the :doc:`introduction ` to ASGI, look 14 | through the :doc:`specifications `, and see what 15 | :doc:`implementations ` there already are or that are upcoming. 16 | 17 | Contribution and discussion about ASGI is welcome, and mostly happens on 18 | the `asgiref GitHub repository `_. 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | 23 | introduction 24 | specs/index 25 | extensions 26 | implementations 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | tests: 11 | name: Python ${{ matrix.python-version }} 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: 17 | - 3.9 18 | - '3.10' 19 | - '3.11' 20 | - '3.12' 21 | - '3.13' 22 | - '3.14' 23 | 24 | steps: 25 | - uses: actions/checkout@v5 26 | 27 | - name: Set up Python ${{ matrix.python-version }} 28 | uses: actions/setup-python@v6 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | allow-prereleases: true 32 | 33 | - name: Install dependencies 34 | run: | 35 | python -m pip install --upgrade pip setuptools wheel 36 | python -m pip install --upgrade tox tox-py 37 | 38 | - name: Run tox targets for ${{ matrix.python-version }} 39 | run: tox --py current 40 | 41 | lint: 42 | name: Lint 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Set up Python 47 | uses: actions/setup-python@v5 48 | with: 49 | python-version: '3.13' 50 | - name: Install dependencies 51 | run: | 52 | python -m pip install --upgrade pip tox 53 | - name: Run lint 54 | run: tox -e qa 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Django Software Foundation and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Django nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /asgiref/compatibility.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from .sync import iscoroutinefunction 4 | 5 | 6 | def is_double_callable(application): 7 | """ 8 | Tests to see if an application is a legacy-style (double-callable) application. 9 | """ 10 | # Look for a hint on the object first 11 | if getattr(application, "_asgi_single_callable", False): 12 | return False 13 | if getattr(application, "_asgi_double_callable", False): 14 | return True 15 | # Uninstanted classes are double-callable 16 | if inspect.isclass(application): 17 | return True 18 | # Instanted classes depend on their __call__ 19 | if hasattr(application, "__call__"): 20 | # We only check to see if its __call__ is a coroutine function - 21 | # if it's not, it still might be a coroutine function itself. 22 | if iscoroutinefunction(application.__call__): 23 | return False 24 | # Non-classes we just check directly 25 | return not iscoroutinefunction(application) 26 | 27 | 28 | def double_to_single_callable(application): 29 | """ 30 | Transforms a double-callable ASGI application into a single-callable one. 31 | """ 32 | 33 | async def new_application(scope, receive, send): 34 | instance = application(scope) 35 | return await instance(receive, send) 36 | 37 | return new_application 38 | 39 | 40 | def guarantee_single_callable(application): 41 | """ 42 | Takes either a single- or double-callable application and always returns it 43 | in single-callable style. Use this to add backwards compatibility for ASGI 44 | 2.0 applications to your server/test harness/etc. 45 | """ 46 | if is_double_callable(application): 47 | application = double_to_single_callable(application) 48 | return application 49 | -------------------------------------------------------------------------------- /tests/test_garbage_collection.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import sys 3 | 4 | import pytest 5 | 6 | from asgiref.local import Local 7 | 8 | 9 | def disable_gc_for_garbage_collection_test() -> None: 10 | # Disable automatic garbage collection. To have control over when 11 | # garbage collection is performed. This is necessary to ensure that another 12 | # that thread doesn't accidentally trigger it by simply executing code. 13 | gc.disable() 14 | 15 | # Delete the garbage list(`gc.garbage`) to ensure that other tests don't 16 | # interfere with this test. 17 | gc.collect() 18 | 19 | # Set the garbage collection debugging flag to store all unreachable 20 | # objects in `gc.garbage`. This is necessary to ensure that the 21 | # garbage list is empty after execute test code. Otherwise, the test 22 | # will always pass. The garbage list isn't automatically populated 23 | # because it costs extra CPU cycles 24 | gc.set_debug(gc.DEBUG_SAVEALL) 25 | 26 | 27 | def clean_up_after_garbage_collection_test() -> None: 28 | # Clean up the garbage collection settings. Re-enable automatic garbage 29 | # collection. This step is mandatory to avoid running other tests without 30 | # automatic garbage collection. 31 | gc.set_debug(0) 32 | gc.enable() 33 | 34 | 35 | @pytest.mark.skipif( 36 | sys.implementation.name == "pypy", reason="Test relies on CPython GC internals" 37 | ) 38 | def test_thread_critical_Local_remove_all_reference_cycles() -> None: 39 | try: 40 | # given 41 | # Disable automatic garbage collection and set debugging flag. 42 | disable_gc_for_garbage_collection_test() 43 | 44 | # when 45 | # Create thread critical Local object in sync context. 46 | try: 47 | getattr(Local(thread_critical=True), "missing") 48 | except AttributeError: 49 | pass 50 | # Enforce garbage collection to populate the garbage list for inspection. 51 | gc.collect() 52 | 53 | # then 54 | # Ensure that the garbage list is empty. The garbage list is only valid 55 | # until the next collection cycle so we can only make assertions about it 56 | # before re-enabling automatic collection. 57 | assert gc.garbage == [] 58 | # Restore garbage collection settings to their original state. This should always be run to avoid interfering 59 | # with other tests to ensure that code should be executed in the `finally' block. 60 | finally: 61 | clean_up_after_garbage_collection_test() 62 | -------------------------------------------------------------------------------- /tests/test_compatibility.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from asgiref.compatibility import double_to_single_callable, is_double_callable 4 | from asgiref.testing import ApplicationCommunicator 5 | 6 | 7 | def double_application_function(scope): 8 | """ 9 | A nested function based double-callable application. 10 | """ 11 | 12 | async def inner(receive, send): 13 | message = await receive() 14 | await send({"scope": scope["value"], "message": message["value"]}) 15 | 16 | return inner 17 | 18 | 19 | class DoubleApplicationClass: 20 | """ 21 | A classic class-based double-callable application. 22 | """ 23 | 24 | def __init__(self, scope): 25 | pass 26 | 27 | async def __call__(self, receive, send): 28 | pass 29 | 30 | 31 | class DoubleApplicationClassNestedFunction: 32 | """ 33 | A function closure inside a class! 34 | """ 35 | 36 | def __init__(self): 37 | pass 38 | 39 | def __call__(self, scope): 40 | async def inner(receive, send): 41 | pass 42 | 43 | return inner 44 | 45 | 46 | async def single_application_function(scope, receive, send): 47 | """ 48 | A single-function single-callable application 49 | """ 50 | pass 51 | 52 | 53 | class SingleApplicationClass: 54 | """ 55 | A single-callable class (where you'd pass the class instance in, 56 | e.g. middleware) 57 | """ 58 | 59 | def __init__(self): 60 | pass 61 | 62 | async def __call__(self, scope, receive, send): 63 | pass 64 | 65 | 66 | def test_is_double_callable(): 67 | """ 68 | Tests that the signature matcher works as expected. 69 | """ 70 | assert is_double_callable(double_application_function) is True 71 | assert is_double_callable(DoubleApplicationClass) is True 72 | assert is_double_callable(DoubleApplicationClassNestedFunction()) is True 73 | assert is_double_callable(single_application_function) is False 74 | assert is_double_callable(SingleApplicationClass()) is False 75 | 76 | 77 | def test_double_to_single_signature(): 78 | """ 79 | Test that the new object passes a signature test. 80 | """ 81 | assert ( 82 | is_double_callable(double_to_single_callable(double_application_function)) 83 | is False 84 | ) 85 | 86 | 87 | @pytest.mark.asyncio 88 | async def test_double_to_single_communicator(): 89 | """ 90 | Test that the new application works 91 | """ 92 | new_app = double_to_single_callable(double_application_function) 93 | instance = ApplicationCommunicator(new_app, {"value": "woohoo"}) 94 | await instance.send_input({"value": 42}) 95 | assert await instance.receive_output() == {"scope": "woohoo", "message": 42} 96 | -------------------------------------------------------------------------------- /tests/test_testing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | 5 | from asgiref.testing import ApplicationCommunicator 6 | from asgiref.wsgi import WsgiToAsgi 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_receive_nothing(): 11 | """ 12 | Tests ApplicationCommunicator.receive_nothing to return the correct value. 13 | """ 14 | # Get an ApplicationCommunicator instance 15 | def wsgi_application(environ, start_response): 16 | start_response("200 OK", []) 17 | yield b"content" 18 | 19 | application = WsgiToAsgi(wsgi_application) 20 | instance = ApplicationCommunicator( 21 | application, 22 | { 23 | "type": "http", 24 | "http_version": "1.0", 25 | "method": "GET", 26 | "path": "/foo/", 27 | "query_string": b"bar=baz", 28 | "headers": [], 29 | }, 30 | ) 31 | 32 | # No event 33 | assert await instance.receive_nothing() is True 34 | 35 | # Produce 3 events to receive 36 | await instance.send_input({"type": "http.request"}) 37 | # Start event of the response 38 | assert await instance.receive_nothing() is False 39 | await instance.receive_output() 40 | # First body event of the response announcing further body event 41 | assert await instance.receive_nothing() is False 42 | await instance.receive_output() 43 | # Last body event of the response 44 | assert await instance.receive_nothing() is False 45 | await instance.receive_output() 46 | # Response received completely 47 | assert await instance.receive_nothing(0.01) is True 48 | 49 | 50 | def test_receive_nothing_lazy_loop(): 51 | """ 52 | Tests ApplicationCommunicator.receive_nothing to return the correct value. 53 | """ 54 | # Get an ApplicationCommunicator instance 55 | def wsgi_application(environ, start_response): 56 | start_response("200 OK", []) 57 | yield b"content" 58 | 59 | application = WsgiToAsgi(wsgi_application) 60 | instance = ApplicationCommunicator( 61 | application, 62 | { 63 | "type": "http", 64 | "http_version": "1.0", 65 | "method": "GET", 66 | "path": "/foo/", 67 | "query_string": b"bar=baz", 68 | "headers": [], 69 | }, 70 | ) 71 | 72 | async def test(): 73 | # No event 74 | assert await instance.receive_nothing() is True 75 | 76 | # Produce 3 events to receive 77 | await instance.send_input({"type": "http.request"}) 78 | # Start event of the response 79 | assert await instance.receive_nothing() is False 80 | await instance.receive_output() 81 | # First body event of the response announcing further body event 82 | assert await instance.receive_nothing() is False 83 | await instance.receive_output() 84 | # Last body event of the response 85 | assert await instance.receive_nothing() is False 86 | await instance.receive_output() 87 | # Response received completely 88 | assert await instance.receive_nothing(0.01) is True 89 | 90 | asyncio.run(test()) 91 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | ASGI is a spiritual successor to 5 | `WSGI `_, the long-standing Python 6 | standard for compatibility between web servers, frameworks, and applications. 7 | 8 | WSGI succeeded in allowing much more freedom and innovation in the Python 9 | web space, and ASGI's goal is to continue this onward into the land of 10 | asynchronous Python. 11 | 12 | 13 | What's wrong with WSGI? 14 | ----------------------- 15 | 16 | You may ask "why not just upgrade WSGI"? This has been asked many times over 17 | the years, and the problem usually ends up being that WSGI's single-callable 18 | interface just isn't suitable for more involved Web protocols like WebSocket. 19 | 20 | WSGI applications are a single, synchronous callable that takes a request and 21 | returns a response; this doesn't allow for long-lived connections, like you 22 | get with long-poll HTTP or WebSocket connections. 23 | 24 | Even if we made this callable asynchronous, it still only has a single path 25 | to provide a request, so protocols that have multiple incoming events (like 26 | receiving WebSocket frames) can't trigger this. 27 | 28 | 29 | How does ASGI work? 30 | ------------------- 31 | 32 | ASGI is structured as a single, asynchronous callable. It takes a ``scope``, 33 | which is a ``dict`` containing details about the specific connection, 34 | ``send``, an asynchronous callable, that lets the application send event messages 35 | to the client, and ``receive``, an asynchronous callable which lets the application 36 | receive event messages from the client. 37 | 38 | This not only allows multiple incoming events and outgoing events for each 39 | application, but also allows for a background coroutine so the application can 40 | do other things (such as listening for events on an external trigger, like a 41 | Redis queue). 42 | 43 | In its simplest form, an application can be written as an asynchronous function, 44 | like this:: 45 | 46 | async def application(scope, receive, send): 47 | event = await receive() 48 | ... 49 | await send({"type": "websocket.send", ...}) 50 | 51 | Every *event* that you send or receive is a Python ``dict``, with a predefined 52 | format. It's these event formats that form the basis of the standard, and allow 53 | applications to be swappable between servers. 54 | 55 | These *events* each have a defined ``type`` key, which can be used to infer 56 | the event's structure. Here's an example event that you might receive from 57 | ``receive`` with the body from a HTTP request:: 58 | 59 | { 60 | "type": "http.request", 61 | "body": b"Hello World", 62 | "more_body": False, 63 | } 64 | 65 | And here's an example of an event you might pass to ``send`` to send an 66 | outgoing WebSocket message:: 67 | 68 | { 69 | "type": "websocket.send", 70 | "text": "Hello world!", 71 | } 72 | 73 | 74 | WSGI compatibility 75 | ------------------ 76 | 77 | ASGI is also designed to be a superset of WSGI, and there's a defined way 78 | of translating between the two, allowing WSGI applications to be run inside 79 | ASGI servers through a translation wrapper (provided in the ``asgiref`` 80 | library). A threadpool can be used to run the synchronous WSGI applications 81 | away from the async event loop. 82 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = asgiref 3 | version = attr: asgiref.__version__ 4 | url = https://github.com/django/asgiref/ 5 | author = Django Software Foundation 6 | author_email = foundation@djangoproject.com 7 | description = ASGI specs, helper code, and adapters 8 | long_description = file: README.rst 9 | license = BSD-3-Clause 10 | classifiers = 11 | Development Status :: 5 - Production/Stable 12 | Environment :: Web Environment 13 | Intended Audience :: Developers 14 | License :: OSI Approved :: BSD License 15 | Operating System :: OS Independent 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3 :: Only 19 | Programming Language :: Python :: 3.9 20 | Programming Language :: Python :: 3.10 21 | Programming Language :: Python :: 3.11 22 | Programming Language :: Python :: 3.12 23 | Programming Language :: Python :: 3.13 24 | Topic :: Internet :: WWW/HTTP 25 | project_urls = 26 | Documentation = https://asgi.readthedocs.io/ 27 | Further Documentation = https://docs.djangoproject.com/en/stable/topics/async/#async-adapter-functions 28 | Changelog = https://github.com/django/asgiref/blob/master/CHANGELOG.txt 29 | 30 | [options] 31 | python_requires = >=3.9 32 | packages = find: 33 | include_package_data = true 34 | install_requires = 35 | typing_extensions>=4; python_version < "3.11" 36 | zip_safe = false 37 | 38 | [options.extras_require] 39 | tests = 40 | pytest 41 | pytest-asyncio 42 | mypy>=1.14.0 43 | 44 | [tool:pytest] 45 | testpaths = tests 46 | asyncio_mode = strict 47 | asyncio_default_fixture_loop_scope=function 48 | 49 | [flake8] 50 | exclude = venv/*,tox/*,specs/* 51 | ignore = E123,E128,E266,E402,W503,E731,W601,E203 52 | max-line-length = 119 53 | 54 | [isort] 55 | profile = black 56 | multi_line_output = 3 57 | 58 | [mypy] 59 | warn_unused_ignores = True 60 | strict = True 61 | 62 | [mypy-asgiref.current_thread_executor] 63 | disallow_untyped_defs = False 64 | check_untyped_defs = False 65 | 66 | [mypy-asgiref.local] 67 | disallow_untyped_defs = False 68 | check_untyped_defs = False 69 | 70 | [mypy-asgiref.sync] 71 | disallow_untyped_defs = False 72 | check_untyped_defs = False 73 | 74 | [mypy-asgiref.compatibility] 75 | disallow_untyped_defs = False 76 | check_untyped_defs = False 77 | 78 | [mypy-asgiref.wsgi] 79 | disallow_untyped_defs = False 80 | check_untyped_defs = False 81 | 82 | [mypy-asgiref.testing] 83 | disallow_untyped_defs = False 84 | check_untyped_defs = False 85 | 86 | [mypy-asgiref.server] 87 | disallow_untyped_defs = False 88 | check_untyped_defs = False 89 | 90 | [mypy-test_server] 91 | disallow_untyped_defs = False 92 | check_untyped_defs = False 93 | 94 | [mypy-test_wsgi] 95 | disallow_untyped_defs = False 96 | check_untyped_defs = False 97 | 98 | [mypy-test_testing] 99 | disallow_untyped_defs = False 100 | check_untyped_defs = False 101 | 102 | [mypy-test_sync_contextvars] 103 | disallow_untyped_defs = False 104 | check_untyped_defs = False 105 | 106 | [mypy-test_sync] 107 | disallow_untyped_defs = False 108 | check_untyped_defs = False 109 | 110 | [mypy-test_local] 111 | disallow_untyped_defs = False 112 | check_untyped_defs = False 113 | 114 | [mypy-test_compatibility] 115 | disallow_untyped_defs = False 116 | check_untyped_defs = False 117 | -------------------------------------------------------------------------------- /asgiref/timeout.py: -------------------------------------------------------------------------------- 1 | # This code is originally sourced from the aio-libs project "async_timeout", 2 | # under the Apache 2.0 license. You may see the original project at 3 | # https://github.com/aio-libs/async-timeout 4 | 5 | # It is vendored here to reduce chain-dependencies on this library, and 6 | # modified slightly to remove some features we don't use. 7 | 8 | 9 | import asyncio 10 | import warnings 11 | from types import TracebackType 12 | from typing import Any # noqa 13 | from typing import Optional, Type 14 | 15 | 16 | class timeout: 17 | """timeout context manager. 18 | 19 | Useful in cases when you want to apply timeout logic around block 20 | of code or in cases when asyncio.wait_for is not suitable. For example: 21 | 22 | >>> with timeout(0.001): 23 | ... async with aiohttp.get('https://github.com') as r: 24 | ... await r.text() 25 | 26 | 27 | timeout - value in seconds or None to disable timeout logic 28 | loop - asyncio compatible event loop 29 | """ 30 | 31 | def __init__( 32 | self, 33 | timeout: Optional[float], 34 | *, 35 | loop: Optional[asyncio.AbstractEventLoop] = None, 36 | ) -> None: 37 | self._timeout = timeout 38 | if loop is None: 39 | loop = asyncio.get_running_loop() 40 | else: 41 | warnings.warn( 42 | """The loop argument to timeout() is deprecated.""", DeprecationWarning 43 | ) 44 | self._loop = loop 45 | self._task = None # type: Optional[asyncio.Task[Any]] 46 | self._cancelled = False 47 | self._cancel_handler = None # type: Optional[asyncio.Handle] 48 | self._cancel_at = None # type: Optional[float] 49 | 50 | def __enter__(self) -> "timeout": 51 | return self._do_enter() 52 | 53 | def __exit__( 54 | self, 55 | exc_type: Type[BaseException], 56 | exc_val: BaseException, 57 | exc_tb: TracebackType, 58 | ) -> Optional[bool]: 59 | self._do_exit(exc_type) 60 | return None 61 | 62 | async def __aenter__(self) -> "timeout": 63 | return self._do_enter() 64 | 65 | async def __aexit__( 66 | self, 67 | exc_type: Type[BaseException], 68 | exc_val: BaseException, 69 | exc_tb: TracebackType, 70 | ) -> None: 71 | self._do_exit(exc_type) 72 | 73 | @property 74 | def expired(self) -> bool: 75 | return self._cancelled 76 | 77 | @property 78 | def remaining(self) -> Optional[float]: 79 | if self._cancel_at is not None: 80 | return max(self._cancel_at - self._loop.time(), 0.0) 81 | else: 82 | return None 83 | 84 | def _do_enter(self) -> "timeout": 85 | # Support Tornado 5- without timeout 86 | # Details: https://github.com/python/asyncio/issues/392 87 | if self._timeout is None: 88 | return self 89 | 90 | self._task = asyncio.current_task(self._loop) 91 | if self._task is None: 92 | raise RuntimeError( 93 | "Timeout context manager should be used " "inside a task" 94 | ) 95 | 96 | if self._timeout <= 0: 97 | self._loop.call_soon(self._cancel_task) 98 | return self 99 | 100 | self._cancel_at = self._loop.time() + self._timeout 101 | self._cancel_handler = self._loop.call_at(self._cancel_at, self._cancel_task) 102 | return self 103 | 104 | def _do_exit(self, exc_type: Type[BaseException]) -> None: 105 | if exc_type is asyncio.CancelledError and self._cancelled: 106 | self._cancel_handler = None 107 | self._task = None 108 | raise asyncio.TimeoutError 109 | if self._timeout is not None and self._cancel_handler is not None: 110 | self._cancel_handler.cancel() 111 | self._cancel_handler = None 112 | self._task = None 113 | return None 114 | 115 | def _cancel_task(self) -> None: 116 | if self._task is not None: 117 | self._task.cancel() 118 | self._cancelled = True 119 | -------------------------------------------------------------------------------- /asgiref/current_thread_executor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | from collections import deque 4 | from concurrent.futures import Executor, Future 5 | from typing import Any, Callable, TypeVar 6 | 7 | if sys.version_info >= (3, 10): 8 | from typing import ParamSpec 9 | else: 10 | from typing_extensions import ParamSpec 11 | 12 | _T = TypeVar("_T") 13 | _P = ParamSpec("_P") 14 | _R = TypeVar("_R") 15 | 16 | 17 | class _WorkItem: 18 | """ 19 | Represents an item needing to be run in the executor. 20 | Copied from ThreadPoolExecutor (but it's private, so we're not going to rely on importing it) 21 | """ 22 | 23 | def __init__( 24 | self, 25 | future: "Future[_R]", 26 | fn: Callable[_P, _R], 27 | *args: _P.args, 28 | **kwargs: _P.kwargs, 29 | ): 30 | self.future = future 31 | self.fn = fn 32 | self.args = args 33 | self.kwargs = kwargs 34 | 35 | def run(self) -> None: 36 | __traceback_hide__ = True # noqa: F841 37 | if not self.future.set_running_or_notify_cancel(): 38 | return 39 | try: 40 | result = self.fn(*self.args, **self.kwargs) 41 | except BaseException as exc: 42 | self.future.set_exception(exc) 43 | # Break a reference cycle with the exception 'exc' 44 | self = None # type: ignore[assignment] 45 | else: 46 | self.future.set_result(result) 47 | 48 | 49 | class CurrentThreadExecutor(Executor): 50 | """ 51 | An Executor that actually runs code in the thread it is instantiated in. 52 | Passed to other threads running async code, so they can run sync code in 53 | the thread they came from. 54 | """ 55 | 56 | def __init__(self, old_executor: "CurrentThreadExecutor | None") -> None: 57 | self._work_thread = threading.current_thread() 58 | self._work_ready = threading.Condition(threading.Lock()) 59 | self._work_items = deque[_WorkItem]() # synchronized by _work_ready 60 | self._broken = False # synchronized by _work_ready 61 | self._old_executor = old_executor 62 | 63 | def run_until_future(self, future: "Future[Any]") -> None: 64 | """ 65 | Runs the code in the work queue until a result is available from the future. 66 | Should be run from the thread the executor is initialised in. 67 | """ 68 | # Check we're in the right thread 69 | if threading.current_thread() != self._work_thread: 70 | raise RuntimeError( 71 | "You cannot run CurrentThreadExecutor from a different thread" 72 | ) 73 | 74 | def done(future: "Future[Any]") -> None: 75 | with self._work_ready: 76 | self._broken = True 77 | self._work_ready.notify() 78 | 79 | future.add_done_callback(done) 80 | # Keep getting and running work items until the future we're waiting for 81 | # is done and the queue is empty. 82 | while True: 83 | with self._work_ready: 84 | while not self._work_items and not self._broken: 85 | self._work_ready.wait() 86 | if not self._work_items: 87 | break 88 | # Get a work item and run it 89 | work_item = self._work_items.popleft() 90 | work_item.run() 91 | del work_item 92 | 93 | def submit( 94 | self, 95 | fn: Callable[_P, _R], 96 | /, 97 | *args: _P.args, 98 | **kwargs: _P.kwargs, 99 | ) -> "Future[_R]": 100 | # Check they're not submitting from the same thread 101 | if threading.current_thread() == self._work_thread: 102 | raise RuntimeError( 103 | "You cannot submit onto CurrentThreadExecutor from its own thread" 104 | ) 105 | f: "Future[_R]" = Future() 106 | work_item = _WorkItem(f, fn, *args, **kwargs) 107 | 108 | # Walk up the CurrentThreadExecutor stack to find the closest one still 109 | # running 110 | executor = self 111 | while True: 112 | with executor._work_ready: 113 | if not executor._broken: 114 | # Add to work queue 115 | executor._work_items.append(work_item) 116 | executor._work_ready.notify() 117 | break 118 | if executor._old_executor is None: 119 | raise RuntimeError("CurrentThreadExecutor already quit or is broken") 120 | executor = executor._old_executor 121 | 122 | # Return the future 123 | return f 124 | -------------------------------------------------------------------------------- /tests/test_sync_contextvars.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextvars 3 | import sys 4 | import threading 5 | import time 6 | 7 | import pytest 8 | 9 | from asgiref.sync import ThreadSensitiveContext, async_to_sync, sync_to_async 10 | 11 | foo: "contextvars.ContextVar[str]" = contextvars.ContextVar("foo") 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_thread_sensitive_with_context_different(): 16 | result_1 = {} 17 | result_2 = {} 18 | 19 | @sync_to_async 20 | def store_thread(result): 21 | result["thread"] = threading.current_thread() 22 | 23 | async def fn(result): 24 | async with ThreadSensitiveContext(): 25 | await store_thread(result) 26 | 27 | # Run it (in true parallel!) 28 | await asyncio.wait( 29 | [asyncio.create_task(fn(result_1)), asyncio.create_task(fn(result_2))] 30 | ) 31 | 32 | # They should not have run in the main thread, and on different threads 33 | assert result_1["thread"] != threading.current_thread() 34 | assert result_1["thread"] != result_2["thread"] 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_sync_to_async_contextvars(): 39 | """ 40 | Tests to make sure that contextvars from the calling context are 41 | present in the called context, and that any changes in the called context 42 | are then propagated back to the calling context. 43 | """ 44 | # Define sync function 45 | def sync_function(): 46 | time.sleep(1) 47 | assert foo.get() == "bar" 48 | foo.set("baz") 49 | return 42 50 | 51 | # Ensure outermost detection works 52 | # Wrap it 53 | foo.set("bar") 54 | async_function = sync_to_async(sync_function) 55 | assert await async_function() == 42 56 | assert foo.get() == "baz" 57 | 58 | 59 | @pytest.mark.asyncio 60 | async def test_sync_to_async_contextvars_with_custom_context(): 61 | """ 62 | Passing a custom context to `sync_to_async` ensures that changes to context 63 | variables within the synchronous function are isolated to the provided 64 | context and do not affect the caller's context. Specifically, verifies that 65 | modifications to a context variable inside the sync function are reflected 66 | only in the custom context and not in the outer context. 67 | """ 68 | 69 | def sync_function(): 70 | time.sleep(1) 71 | assert foo.get() == "bar" 72 | foo.set("baz") 73 | return 42 74 | 75 | foo.set("bar") 76 | context = contextvars.copy_context() 77 | 78 | async_function = sync_to_async(sync_function, context=context) 79 | assert await async_function() == 42 80 | 81 | # Current context remains unchanged. 82 | assert foo.get() == "bar" 83 | 84 | # Custom context reflects the changes made within the sync function. 85 | assert context.get(foo) == "baz" 86 | 87 | 88 | @pytest.mark.asyncio 89 | @pytest.mark.skipif(sys.version_info < (3, 11), reason="requires python3.11") 90 | async def test_sync_to_async_contextvars_with_custom_context_and_parallel_tasks(): 91 | """ 92 | Using a custom context with `sync_to_async` and asyncio tasks isolates 93 | contextvars changes, leaving the original context unchanged and reflecting 94 | all modifications in the custom context. 95 | """ 96 | foo.set("") 97 | 98 | def sync_function(): 99 | foo.set(foo.get() + "1") 100 | return 1 101 | 102 | async def async_function(): 103 | foo.set(foo.get() + "1") 104 | return 1 105 | 106 | context = contextvars.copy_context() 107 | 108 | await asyncio.gather( 109 | sync_to_async(sync_function, context=context)(), 110 | sync_to_async(sync_function, context=context)(), 111 | asyncio.create_task(async_function(), context=context), 112 | asyncio.create_task(async_function(), context=context), 113 | ) 114 | 115 | # Current context remains unchanged 116 | assert foo.get() == "" 117 | 118 | # Custom context reflects the changes made within all the gathered tasks. 119 | assert context.get(foo) == "1111" 120 | 121 | 122 | def test_async_to_sync_contextvars(): 123 | """ 124 | Tests to make sure that contextvars from the calling context are 125 | present in the called context, and that any changes in the called context 126 | are then propagated back to the calling context. 127 | """ 128 | # Define async function 129 | async def async_function(): 130 | await asyncio.sleep(1) 131 | assert foo.get() == "bar" 132 | foo.set("baz") 133 | return 42 134 | 135 | # Ensure outermost detection works 136 | # Wrap it 137 | foo.set("bar") 138 | sync_function = async_to_sync(async_function) 139 | assert sync_function() == 42 140 | assert foo.get() == "baz" 141 | -------------------------------------------------------------------------------- /asgiref/testing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextvars 3 | import time 4 | 5 | from .compatibility import guarantee_single_callable 6 | from .timeout import timeout as async_timeout 7 | 8 | 9 | class ApplicationCommunicator: 10 | """ 11 | Runs an ASGI application in a test mode, allowing sending of 12 | messages to it and retrieval of messages it sends. 13 | """ 14 | 15 | def __init__(self, application, scope): 16 | self._future = None 17 | self.application = guarantee_single_callable(application) 18 | self.scope = scope 19 | self._input_queue = None 20 | self._output_queue = None 21 | 22 | # For Python 3.9 we need to lazily bind the queues, on 3.10+ they bind the 23 | # event loop lazily. 24 | @property 25 | def input_queue(self): 26 | if self._input_queue is None: 27 | self._input_queue = asyncio.Queue() 28 | return self._input_queue 29 | 30 | @property 31 | def output_queue(self): 32 | if self._output_queue is None: 33 | self._output_queue = asyncio.Queue() 34 | return self._output_queue 35 | 36 | @property 37 | def future(self): 38 | if self._future is None: 39 | # Clear context - this ensures that context vars set in the testing scope 40 | # are not "leaked" into the application which would normally begin with 41 | # an empty context. In Python >= 3.11 this could also be written as: 42 | # asyncio.create_task(..., context=contextvars.Context()) 43 | self._future = contextvars.Context().run( 44 | asyncio.create_task, 45 | self.application( 46 | self.scope, self.input_queue.get, self.output_queue.put 47 | ), 48 | ) 49 | return self._future 50 | 51 | async def wait(self, timeout=1): 52 | """ 53 | Waits for the application to stop itself and returns any exceptions. 54 | """ 55 | try: 56 | async with async_timeout(timeout): 57 | try: 58 | await self.future 59 | self.future.result() 60 | except asyncio.CancelledError: 61 | pass 62 | finally: 63 | if not self.future.done(): 64 | self.future.cancel() 65 | try: 66 | await self.future 67 | except asyncio.CancelledError: 68 | pass 69 | 70 | def stop(self, exceptions=True): 71 | future = self._future 72 | if future is None: 73 | return 74 | 75 | if not future.done(): 76 | future.cancel() 77 | elif exceptions: 78 | # Give a chance to raise any exceptions 79 | future.result() 80 | 81 | def __del__(self): 82 | # Clean up on deletion 83 | try: 84 | self.stop(exceptions=False) 85 | except RuntimeError: 86 | # Event loop already stopped 87 | pass 88 | 89 | async def send_input(self, message): 90 | """ 91 | Sends a single message to the application 92 | """ 93 | # Make sure there's not an exception to raise from the task 94 | if self.future.done(): 95 | self.future.result() 96 | 97 | # Give it the message 98 | await self.input_queue.put(message) 99 | 100 | async def receive_output(self, timeout=1): 101 | """ 102 | Receives a single message from the application, with optional timeout. 103 | """ 104 | # Make sure there's not an exception to raise from the task 105 | if self.future.done(): 106 | self.future.result() 107 | # Wait and receive the message 108 | try: 109 | async with async_timeout(timeout): 110 | return await self.output_queue.get() 111 | except asyncio.TimeoutError as e: 112 | # See if we have another error to raise inside 113 | if self.future.done(): 114 | self.future.result() 115 | else: 116 | self.future.cancel() 117 | try: 118 | await self.future 119 | except asyncio.CancelledError: 120 | pass 121 | raise e 122 | 123 | async def receive_nothing(self, timeout=0.1, interval=0.01): 124 | """ 125 | Checks that there is no message to receive in the given time. 126 | """ 127 | # Make sure there's not an exception to raise from the task 128 | if self.future.done(): 129 | self.future.result() 130 | 131 | # `interval` has precedence over `timeout` 132 | start = time.monotonic() 133 | while time.monotonic() - start < timeout: 134 | if not self.output_queue.empty(): 135 | return False 136 | await asyncio.sleep(interval) 137 | return self.output_queue.empty() 138 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import socket as sock 3 | 4 | import pytest 5 | import pytest_asyncio 6 | 7 | from asgiref.server import StatelessServer 8 | 9 | 10 | async def sock_recvfrom(sock, n): 11 | while True: 12 | try: 13 | return sock.recvfrom(n) 14 | except BlockingIOError: 15 | await asyncio.sleep(0) 16 | 17 | 18 | class Server(StatelessServer): 19 | def __init__(self, application, max_applications=1000): 20 | super().__init__( 21 | application, 22 | max_applications=max_applications, 23 | ) 24 | self._sock = sock.socket(sock.AF_INET, sock.SOCK_DGRAM) 25 | self._sock.setblocking(False) 26 | self._sock.bind(("127.0.0.1", 0)) 27 | 28 | @property 29 | def address(self): 30 | return self._sock.getsockname() 31 | 32 | async def handle(self): 33 | while True: 34 | data, addr = await sock_recvfrom(self._sock, 4096) 35 | data = data.decode("utf-8") 36 | 37 | if data.startswith("Register"): 38 | _, usr_name = data.split(" ") 39 | input_quene = self.get_or_create_application_instance(usr_name, addr) 40 | input_quene.put_nowait(b"Welcome") 41 | 42 | elif data.startswith("To"): 43 | _, usr_name, msg = data.split(" ", 2) 44 | input_quene = self.get_or_create_application_instance(usr_name, addr) 45 | input_quene.put_nowait(msg.encode("utf-8")) 46 | 47 | async def application_send(self, scope, message): 48 | self._sock.sendto(message, scope) 49 | 50 | def close(self): 51 | self._sock.close() 52 | for details in self.application_instances.values(): 53 | details["future"].cancel() 54 | 55 | 56 | class Client: 57 | def __init__(self, name): 58 | self._sock = sock.socket(sock.AF_INET, sock.SOCK_DGRAM) 59 | self._sock.setblocking(False) 60 | self.name = name 61 | 62 | async def register(self, server_addr, name=None): 63 | name = name or self.name 64 | self._sock.sendto(f"Register {name}".encode(), server_addr) 65 | 66 | async def send(self, server_addr, to, msg): 67 | self._sock.sendto(f"To {to} {msg}".encode(), server_addr) 68 | 69 | async def get_msg(self): 70 | msg, server_addr = await sock_recvfrom(self._sock, 4096) 71 | return msg, server_addr 72 | 73 | def close(self): 74 | self._sock.close() 75 | 76 | 77 | @pytest_asyncio.fixture(scope="function") 78 | async def server(): 79 | async def app(scope, receive, send): 80 | while True: 81 | msg = await receive() 82 | await send(msg) 83 | 84 | server = Server(app, 10) 85 | yield server 86 | server.close() 87 | 88 | 89 | async def check_client_msg(client, expected_address, expected_msg): 90 | msg, server_addr = await asyncio.wait_for(client.get_msg(), timeout=1.0) 91 | assert msg == expected_msg 92 | assert server_addr == expected_address 93 | 94 | 95 | @pytest.mark.asyncio 96 | async def test_stateless_server(server): 97 | """StatelessServer can be instantiated with an ASGI 3 application.""" 98 | """Create a UDP Server can register instance based on name from message of client. 99 | Clients can communicate to other client by name through server""" 100 | 101 | client1 = Client(name="client1") 102 | client2 = Client(name="client2") 103 | 104 | async def check_client1_behavior(): 105 | await client1.register(server.address) 106 | await check_client_msg(client1, server.address, b"Welcome") 107 | await client1.send(server.address, "client2", "Hello") 108 | 109 | async def check_client2_behavior(): 110 | await client2.register(server.address) 111 | await check_client_msg(client2, server.address, b"Welcome") 112 | await check_client_msg(client2, server.address, b"Hello") 113 | 114 | class Done(Exception): 115 | pass 116 | 117 | async def do_test(): 118 | await asyncio.gather(check_client1_behavior(), check_client2_behavior()) 119 | raise Done 120 | 121 | try: 122 | await asyncio.gather(server.arun(), do_test()) 123 | except Done: 124 | pass 125 | 126 | 127 | @pytest.mark.asyncio 128 | async def test_server_delete_instance(server): 129 | """The max_applications of Server is 10. After 20 times register, application number should be 10.""" 130 | client1 = Client(name="client1") 131 | 132 | class Done(Exception): 133 | pass 134 | 135 | async def client1_multiple_register(): 136 | for i in range(20): 137 | await client1.register(server.address, name=f"client{i}") 138 | print(f"client{i}") 139 | await check_client_msg(client1, server.address, b"Welcome") 140 | raise Done 141 | 142 | try: 143 | await asyncio.gather(client1_multiple_register(), server.arun()) 144 | except Done: 145 | pass 146 | -------------------------------------------------------------------------------- /asgiref/local.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import contextlib 3 | import contextvars 4 | import threading 5 | from typing import Any, Dict, Union 6 | 7 | 8 | class _CVar: 9 | """Storage utility for Local.""" 10 | 11 | def __init__(self) -> None: 12 | self._data: "contextvars.ContextVar[Dict[str, Any]]" = contextvars.ContextVar( 13 | "asgiref.local" 14 | ) 15 | 16 | def __getattr__(self, key): 17 | storage_object = self._data.get({}) 18 | try: 19 | return storage_object[key] 20 | except KeyError: 21 | raise AttributeError(f"{self!r} object has no attribute {key!r}") 22 | 23 | def __setattr__(self, key: str, value: Any) -> None: 24 | if key == "_data": 25 | return super().__setattr__(key, value) 26 | 27 | storage_object = self._data.get({}).copy() 28 | storage_object[key] = value 29 | self._data.set(storage_object) 30 | 31 | def __delattr__(self, key: str) -> None: 32 | storage_object = self._data.get({}).copy() 33 | if key in storage_object: 34 | del storage_object[key] 35 | self._data.set(storage_object) 36 | else: 37 | raise AttributeError(f"{self!r} object has no attribute {key!r}") 38 | 39 | 40 | class Local: 41 | """Local storage for async tasks. 42 | 43 | This is a namespace object (similar to `threading.local`) where data is 44 | also local to the current async task (if there is one). 45 | 46 | In async threads, local means in the same sense as the `contextvars` 47 | module - i.e. a value set in an async frame will be visible: 48 | 49 | - to other async code `await`-ed from this frame. 50 | - to tasks spawned using `asyncio` utilities (`create_task`, `wait_for`, 51 | `gather` and probably others). 52 | - to code scheduled in a sync thread using `sync_to_async` 53 | 54 | In "sync" threads (a thread with no async event loop running), the 55 | data is thread-local, but additionally shared with async code executed 56 | via the `async_to_sync` utility, which schedules async code in a new thread 57 | and copies context across to that thread. 58 | 59 | If `thread_critical` is True, then the local will only be visible per-thread, 60 | behaving exactly like `threading.local` if the thread is sync, and as 61 | `contextvars` if the thread is async. This allows genuinely thread-sensitive 62 | code (such as DB handles) to be kept stricly to their initial thread and 63 | disable the sharing across `sync_to_async` and `async_to_sync` wrapped calls. 64 | 65 | Unlike plain `contextvars` objects, this utility is threadsafe. 66 | """ 67 | 68 | def __init__(self, thread_critical: bool = False) -> None: 69 | self._thread_critical = thread_critical 70 | self._thread_lock = threading.RLock() 71 | 72 | self._storage: "Union[threading.local, _CVar]" 73 | 74 | if thread_critical: 75 | # Thread-local storage 76 | self._storage = threading.local() 77 | else: 78 | # Contextvar storage 79 | self._storage = _CVar() 80 | 81 | @contextlib.contextmanager 82 | def _lock_storage(self): 83 | # Thread safe access to storage 84 | if self._thread_critical: 85 | is_async = True 86 | try: 87 | # this is a test for are we in a async or sync 88 | # thread - will raise RuntimeError if there is 89 | # no current loop 90 | asyncio.get_running_loop() 91 | except RuntimeError: 92 | is_async = False 93 | if not is_async: 94 | # We are in a sync thread, the storage is 95 | # just the plain thread local (i.e, "global within 96 | # this thread" - it doesn't matter where you are 97 | # in a call stack you see the same storage) 98 | yield self._storage 99 | else: 100 | # We are in an async thread - storage is still 101 | # local to this thread, but additionally should 102 | # behave like a context var (is only visible with 103 | # the same async call stack) 104 | 105 | # Ensure context exists in the current thread 106 | if not hasattr(self._storage, "cvar"): 107 | self._storage.cvar = _CVar() 108 | 109 | # self._storage is a thread local, so the members 110 | # can't be accessed in another thread (we don't 111 | # need any locks) 112 | yield self._storage.cvar 113 | else: 114 | # Lock for thread_critical=False as other threads 115 | # can access the exact same storage object 116 | with self._thread_lock: 117 | yield self._storage 118 | 119 | def __getattr__(self, key): 120 | with self._lock_storage() as storage: 121 | return getattr(storage, key) 122 | 123 | def __setattr__(self, key, value): 124 | if key in ("_local", "_storage", "_thread_critical", "_thread_lock"): 125 | return super().__setattr__(key, value) 126 | with self._lock_storage() as storage: 127 | setattr(storage, key, value) 128 | 129 | def __delattr__(self, key): 130 | with self._lock_storage() as storage: 131 | delattr(storage, key) 132 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from typing import Dict, List 3 | 4 | # 5 | # ASGI documentation build configuration file, created by 6 | # sphinx-quickstart on Thu May 17 21:22:10 2018. 7 | # 8 | # This file is execfile()d with the current directory set to its 9 | # containing dir. 10 | # 11 | # Note that not all possible configuration values are present in this 12 | # autogenerated file. 13 | # 14 | # All configuration values have a default; values that are commented out 15 | # serve to show the default. 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | # 21 | # import os 22 | # import sys 23 | # sys.path.insert(0, os.path.abspath('.')) 24 | 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | # 30 | # needs_sphinx = '1.0' 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions: List[str] = [] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path: List[str] = [] 39 | 40 | # The suffix(es) of source filenames. 41 | # You can specify multiple suffix as a list of string: 42 | # 43 | # source_suffix = ['.rst', '.md'] 44 | source_suffix = ".rst" 45 | 46 | # The master toctree document. 47 | master_doc = "index" 48 | 49 | # General information about the project. 50 | project = "ASGI" 51 | copyright = "2018, ASGI Team" 52 | author = "ASGI Team" 53 | 54 | # The version info for the project you're documenting, acts as replacement for 55 | # |version| and |release|, also used in various other places throughout the 56 | # built documents. 57 | # 58 | # The short X.Y version. 59 | version = "3.0" 60 | # The full version, including alpha/beta/rc tags. 61 | release = "3.0" 62 | 63 | # The language for content autogenerated by Sphinx. Refer to documentation 64 | # for a list of supported languages. 65 | # 66 | # This is also used if you do content translation via gettext catalogs. 67 | # Usually you set "language" from the command line for these cases. 68 | language = "en" 69 | 70 | # List of patterns, relative to source directory, that match files and 71 | # directories to ignore when looking for source files. 72 | # This patterns also effect to html_static_path and html_extra_path 73 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 74 | 75 | # The name of the Pygments (syntax highlighting) style to use. 76 | pygments_style = "sphinx" 77 | 78 | # If true, `todo` and `todoList` produce output, else they produce nothing. 79 | todo_include_todos = False 80 | 81 | 82 | # -- Options for HTML output ---------------------------------------------- 83 | 84 | # The theme to use for HTML and HTML Help pages. See the documentation for 85 | # a list of builtin themes. 86 | # 87 | html_theme = "sphinx_rtd_theme" 88 | 89 | # Theme options are theme-specific and customize the look and feel of a theme 90 | # further. For a list of options available for each theme, see the 91 | # documentation. 92 | # 93 | # html_theme_options = {} 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the builtin "default.css". 98 | html_static_path: List[str] = [] 99 | 100 | # Custom sidebar templates, must be a dictionary that maps document names 101 | # to template names. 102 | # 103 | # This is required for the alabaster theme 104 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 105 | # html_sidebars = { 106 | # '**': [ 107 | # 'about.html', 108 | # 'navigation.html', 109 | # 'relations.html', 110 | # 'searchbox.html', 111 | # 'donate.html', 112 | # ] 113 | # } 114 | 115 | 116 | # -- Options for HTMLHelp output ------------------------------------------ 117 | 118 | # Output file base name for HTML help builder. 119 | htmlhelp_basename = "ASGIdoc" 120 | 121 | 122 | # -- Options for LaTeX output --------------------------------------------- 123 | 124 | latex_elements: Dict[str, str] = { 125 | # The paper size ('letterpaper' or 'a4paper'). 126 | # 127 | # 'papersize': 'letterpaper', 128 | # The font size ('10pt', '11pt' or '12pt'). 129 | # 130 | # 'pointsize': '10pt', 131 | # Additional stuff for the LaTeX preamble. 132 | # 133 | # 'preamble': '', 134 | # Latex figure (float) alignment 135 | # 136 | # 'figure_align': 'htbp', 137 | } 138 | 139 | # Grouping the document tree into LaTeX files. List of tuples 140 | # (source start file, target name, title, 141 | # author, documentclass [howto, manual, or own class]). 142 | latex_documents = [ 143 | (master_doc, "ASGI.tex", "ASGI Documentation", "ASGI Team", "manual"), 144 | ] 145 | 146 | 147 | # -- Options for manual page output --------------------------------------- 148 | 149 | # One entry per manual page. List of tuples 150 | # (source start file, name, description, authors, manual section). 151 | man_pages = [(master_doc, "asgi", "ASGI Documentation", [author], 1)] 152 | 153 | 154 | # -- Options for Texinfo output ------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | ( 161 | master_doc, 162 | "ASGI", 163 | "ASGI Documentation", 164 | author, 165 | "ASGI", 166 | "One line description of project.", 167 | "Miscellaneous", 168 | ), 169 | ] 170 | -------------------------------------------------------------------------------- /specs/lifespan.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Lifespan Protocol 3 | ================= 4 | 5 | **Version**: 2.0 (2019-03-20) 6 | 7 | The Lifespan ASGI sub-specification outlines how to communicate 8 | lifespan events such as startup and shutdown within ASGI. 9 | 10 | The lifespan messages allow for an application to initialise and 11 | shutdown in the context of a running event loop. An example of this 12 | would be creating a connection pool and subsequently closing the 13 | connection pool to release the connections. 14 | 15 | Lifespans should be executed once per event loop that will be processing requests. 16 | In a multi-process environment there will be lifespan events in each process 17 | and in a multi-threaded environment there will be lifespans for each thread. 18 | The important part is that lifespans and requests are run in the same event loop 19 | to ensure that objects like database connection pools are not moved or shared across event loops. 20 | 21 | A possible implementation of this protocol is given below:: 22 | 23 | async def app(scope, receive, send): 24 | if scope['type'] == 'lifespan': 25 | while True: 26 | message = await receive() 27 | if message['type'] == 'lifespan.startup': 28 | ... # Do some startup here! 29 | await send({'type': 'lifespan.startup.complete'}) 30 | elif message['type'] == 'lifespan.shutdown': 31 | ... # Do some shutdown here! 32 | await send({'type': 'lifespan.shutdown.complete'}) 33 | return 34 | else: 35 | pass # Handle other types 36 | 37 | 38 | Scope 39 | ''''' 40 | 41 | The lifespan scope exists for the duration of the event loop. 42 | 43 | The scope information passed in ``scope`` contains basic metadata: 44 | 45 | * ``type`` (*Unicode string*) -- ``"lifespan"``. 46 | * ``asgi["version"]`` (*Unicode string*) -- The version of the ASGI spec. 47 | * ``asgi["spec_version"]`` (*Unicode string*) -- The version of this spec being 48 | used. Optional; if missing defaults to ``"1.0"``. 49 | * ``state`` Optional(*dict[Unicode string, Any]*) -- An empty namespace where 50 | the application can persist state to be used when handling subsequent requests. 51 | Optional; if missing the server does not support this feature. 52 | 53 | If an exception is raised when calling the application callable with a 54 | ``lifespan.startup`` message or a ``scope`` with type ``lifespan``, 55 | the server must continue but not send any lifespan events. 56 | 57 | This allows for compatibility with applications that do not support the 58 | lifespan protocol. If you want to log an error that occurs during lifespan 59 | startup and prevent the server from starting, then send back 60 | ``lifespan.startup.failed`` instead. 61 | 62 | Lifespan State 63 | -------------- 64 | 65 | Applications often want to persist data from the lifespan cycle to request/response handling. 66 | For example, a database connection can be established in the lifespan cycle and persisted to 67 | the request/response cycle. 68 | The ``scope["state"]`` namespace provides a place to store these sorts of things. 69 | The server will ensure that a *shallow copy* of the namespace is passed into each subsequent 70 | request/response call into the application. 71 | Since the server manages the application lifespan and often the event loop as well this 72 | ensures that the application is always accessing the database connection (or other stored object) 73 | that corresponds to the right event loop and lifecycle, without using context variables, 74 | global mutable state or having to worry about references to stale/closed connections. 75 | 76 | ASGI servers that implement this feature will provide 77 | ``state`` as part of the ``lifespan`` scope:: 78 | 79 | "scope": { 80 | ... 81 | "state": {}, 82 | } 83 | 84 | The namespace is controlled completely by the ASGI application, the server will not 85 | interact with it other than to copy it. 86 | Nonetheless applications should be cooperative by properly naming their keys such that they 87 | will not collide with other frameworks or middleware. 88 | 89 | Startup - ``receive`` event 90 | ''''''''''''''''''''''''''' 91 | 92 | Sent to the application when the server is ready to startup and receive connections, 93 | but before it has started to do so. 94 | 95 | Keys: 96 | 97 | * ``type`` (*Unicode string*) -- ``"lifespan.startup"``. 98 | 99 | 100 | Startup Complete - ``send`` event 101 | ''''''''''''''''''''''''''''''''' 102 | 103 | Sent by the application when it has completed its startup. A server 104 | must wait for this message before it starts processing connections. 105 | 106 | Keys: 107 | 108 | * ``type`` (*Unicode string*) -- ``"lifespan.startup.complete"``. 109 | 110 | 111 | Startup Failed - ``send`` event 112 | ''''''''''''''''''''''''''''''' 113 | 114 | Sent by the application when it has failed to complete its startup. If a server 115 | sees this it should log/print the message provided and then exit. 116 | 117 | Keys: 118 | 119 | * ``type`` (*Unicode string*) -- ``"lifespan.startup.failed"``. 120 | * ``message`` (*Unicode string*) -- Optional; if missing defaults to ``""``. 121 | 122 | 123 | Shutdown - ``receive`` event 124 | '''''''''''''''''''''''''''' 125 | 126 | Sent to the application when the server has stopped accepting connections and closed 127 | all active connections. 128 | 129 | Keys: 130 | 131 | * ``type`` (*Unicode string*) -- ``"lifespan.shutdown"``. 132 | 133 | 134 | Shutdown Complete - ``send`` event 135 | '''''''''''''''''''''''''''''''''' 136 | 137 | Sent by the application when it has completed its cleanup. A server 138 | must wait for this message before terminating. 139 | 140 | Keys: 141 | 142 | * ``type`` (*Unicode string*) -- ``"lifespan.shutdown.complete"``. 143 | 144 | 145 | Shutdown Failed - ``send`` event 146 | '''''''''''''''''''''''''''''''' 147 | 148 | Sent by the application when it has failed to complete its cleanup. If a server 149 | sees this it should log/print the message provided and then terminate. 150 | 151 | Keys: 152 | 153 | * ``type`` (*Unicode string*) -- ``"lifespan.shutdown.failed"``. 154 | * ``message`` (*Unicode string*) -- Optional; if missing defaults to ``""``. 155 | 156 | 157 | Version History 158 | ''''''''''''''' 159 | 160 | * 2.0 (2019-03-04): Added startup.failed and shutdown.failed, 161 | clarified exception handling during startup phase. 162 | * 1.0 (2018-09-06): Updated ASGI spec with a lifespan protocol. 163 | 164 | 165 | Copyright 166 | ''''''''' 167 | 168 | This document has been placed in the public domain. 169 | -------------------------------------------------------------------------------- /docs/implementations.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Implementations 3 | =============== 4 | 5 | Complete or upcoming implementations of ASGI - servers, frameworks, and other 6 | useful pieces. 7 | 8 | Servers 9 | ======= 10 | 11 | Daphne 12 | ------ 13 | 14 | *Stable* / http://github.com/django/daphne 15 | 16 | The current ASGI reference server, written in Twisted and maintained as part 17 | of the Django Channels project. Supports HTTP/1, HTTP/2, and WebSockets. 18 | 19 | 20 | Granian 21 | ------- 22 | 23 | *Beta* / https://github.com/emmett-framework/granian 24 | 25 | A Rust HTTP server for Python applications. 26 | Supports ASGI/3, RSGI and WSGI interface applications. 27 | 28 | 29 | Hypercorn 30 | --------- 31 | 32 | *Beta* / https://hypercorn.readthedocs.io/ 33 | 34 | An ASGI server based on the sans-io hyper, h11, h2, and wsproto libraries. 35 | Supports HTTP/1, HTTP/2, and WebSockets. 36 | 37 | 38 | NGINX Unit 39 | ---------- 40 | 41 | *Stable* / https://unit.nginx.org/configuration/#configuration-python 42 | 43 | Unit is a lightweight and versatile open-source server that has three core capabilities: it is a web server for static media assets, an application server that runs code in multiple languages, and a reverse proxy. 44 | 45 | 46 | Uvicorn 47 | ------- 48 | 49 | *Stable* / https://www.uvicorn.org/ 50 | 51 | A fast ASGI server based on uvloop and httptools. 52 | Supports HTTP/1 and WebSockets. 53 | 54 | 55 | Application Frameworks 56 | ====================== 57 | 58 | BlackSheep 59 | ---------- 60 | 61 | *Stable* / https://github.com/Neoteroi/BlackSheep 62 | 63 | BlackSheep is typed, fast, minimal web framework. It has performant HTTP client, 64 | flexible dependency injection model, OpenID Connect integration, automatic 65 | OpenAPI documentation, dedicated test client and excellent authentication and 66 | authorization policy implementation. Supports HTTP and WebSockets. 67 | 68 | 69 | Connexion 70 | --------- 71 | 72 | *Stable* / https://github.com/spec-first/connexion 73 | 74 | Connexion is a modern Python web framework that makes spec-first and API-first development 75 | easy. You describe your API in an OpenAPI (or Swagger) specification with as much detail 76 | as you want and Connexion will guarantee that it works as you specified. 77 | 78 | You can use Connexion either standalone, or in combination with any ASGI or WSGI-compatible 79 | framework! 80 | 81 | 82 | Django/Channels 83 | --------------- 84 | 85 | *Stable* / http://channels.readthedocs.io 86 | 87 | Channels is the Django project to add asynchronous support to Django and is the 88 | original driving force behind the ASGI project. Supports HTTP and WebSockets 89 | with Django integration, and any protocol with ASGI-native code. 90 | 91 | 92 | Esmerald 93 | -------- 94 | 95 | *Stable* / https://esmerald.dev/ 96 | 97 | Esmerald is a modern, powerful, flexible, high performant web framework designed to build not only APIs but also full scalable applications from the smallest to enterprise level. Modular, elagant and pluggable at its core. 98 | 99 | 100 | Falcon 101 | ------ 102 | 103 | *Stable* / https://falconframework.org/ 104 | 105 | Falcon is a no-magic web API and microservices framework, with a focus on 106 | reliability, correctness, and performance at scale. 107 | Supports both WSGI and ASGI, without any hard dependencies outside of the 108 | standard library. 109 | 110 | 111 | FastAPI 112 | ------- 113 | 114 | *Beta* / https://github.com/tiangolo/fastapi 115 | 116 | FastAPI is an ASGI web framework (made with Starlette) for building web APIs based on 117 | standard Python type annotations and standards like OpenAPI, JSON Schema, and OAuth2. 118 | Supports HTTP and WebSockets. 119 | 120 | 121 | Flama 122 | ----- 123 | 124 | *Stable* / https://github.com/vortico/flama 125 | 126 | Flama is a data-science oriented framework to rapidly build modern and robust machine 127 | learning (ML) APIs. The main aim of the framework is to make ridiculously simple the 128 | deployment of ML APIs. With Flama, data scientists can now quickly turn their ML models 129 | into asynchronous, auto-documented APIs with just a single line of code. All in just few 130 | seconds! 131 | 132 | Flama comes with an intuitive CLI, and provides an easy-to-learn philosophy to speed up 133 | the building of highly performant GraphQL, REST, and ML APIs. Besides, it comprises an 134 | ideal solution for the development of asynchronous and production-ready services, 135 | offering automatic deployment for ML models. 136 | 137 | 138 | Litestar 139 | -------- 140 | 141 | *Stable* / https://litestar.dev/ 142 | 143 | Litestar is a powerful, performant, flexible and opinionated ASGI framework, offering 144 | first class typing support and a full Pydantic integration. Effortlessly Build Performant 145 | APIs. 146 | 147 | 148 | Quart 149 | ----- 150 | 151 | *Beta* / https://github.com/pgjones/quart 152 | 153 | Quart is a Python ASGI web microframework. It is intended to provide the easiest 154 | way to use asyncio functionality in a web context, especially with existing Flask apps. 155 | Supports HTTP. 156 | 157 | 158 | Sanic 159 | ----- 160 | 161 | *Beta* / https://sanicframework.org 162 | 163 | Sanic is an unopinionated and flexible web application server and framework that also 164 | has the ability to operate as an ASGI compatible framework. Therefore, it can be run 165 | using any of the ASGI web servers. Supports HTTP and WebSockets. 166 | 167 | 168 | rpc.py 169 | ------ 170 | 171 | *Beta* / https://github.com/abersheeran/rpc.py 172 | 173 | An easy-to-use and powerful RPC framework. RPC server base on WSGI & ASGI, client base 174 | on ``httpx``. Supports synchronous functions, asynchronous functions, synchronous 175 | generator functions, and asynchronous generator functions. Optional use of Type hint 176 | for type conversion. Optional OpenAPI document generation. 177 | 178 | 179 | Starlette 180 | --------- 181 | 182 | *Beta* / https://github.com/encode/starlette 183 | 184 | Starlette is a minimalist ASGI library for writing against basic but powerful 185 | ``Request`` and ``Response`` classes. Supports HTTP and WebSockets. 186 | 187 | 188 | MicroPie 189 | -------- 190 | 191 | *Beta* / https://patx.github.io/micropie 192 | 193 | MicroPie is a fast, lightweight, modern Python web framework built on ASGI for 194 | asynchronous web applications. Designed for flexibility and simplicity, it 195 | enables high-concurrency web apps with built-in WebSockets, session management, 196 | middleware, and optional template rendering with method based routing. 197 | 198 | 199 | Tools 200 | ===== 201 | 202 | a2wsgi 203 | ------ 204 | 205 | *Stable* / https://github.com/abersheeran/a2wsgi 206 | 207 | Convert WSGI application to ASGI application or ASGI application to WSGI application. 208 | Pure Python. Only depend on the standard library. 209 | -------------------------------------------------------------------------------- /asgiref/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import time 4 | import traceback 5 | 6 | from .compatibility import guarantee_single_callable 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class StatelessServer: 12 | """ 13 | Base server class that handles basic concepts like application instance 14 | creation/pooling, exception handling, and similar, for stateless protocols 15 | (i.e. ones without actual incoming connections to the process) 16 | 17 | Your code should override the handle() method, doing whatever it needs to, 18 | and calling get_or_create_application_instance with a unique `scope_id` 19 | and `scope` for the scope it wants to get. 20 | 21 | If an application instance is found with the same `scope_id`, you are 22 | given its input queue, otherwise one is made for you with the scope provided 23 | and you are given that fresh new input queue. Either way, you should do 24 | something like: 25 | 26 | input_queue = self.get_or_create_application_instance( 27 | "user-123456", 28 | {"type": "testprotocol", "user_id": "123456", "username": "andrew"}, 29 | ) 30 | input_queue.put_nowait(message) 31 | 32 | If you try and create an application instance and there are already 33 | `max_application` instances, the oldest/least recently used one will be 34 | reclaimed and shut down to make space. 35 | 36 | Application coroutines that error will be found periodically (every 100ms 37 | by default) and have their exceptions printed to the console. Override 38 | application_exception() if you want to do more when this happens. 39 | 40 | If you override run(), make sure you handle things like launching the 41 | application checker. 42 | """ 43 | 44 | application_checker_interval = 0.1 45 | 46 | def __init__(self, application, max_applications=1000): 47 | # Parameters 48 | self.application = application 49 | self.max_applications = max_applications 50 | # Initialisation 51 | self.application_instances = {} 52 | 53 | ### Mainloop and handling 54 | 55 | def run(self): 56 | """ 57 | Runs the asyncio event loop with our handler loop. 58 | """ 59 | event_loop = asyncio.get_event_loop() 60 | try: 61 | event_loop.run_until_complete(self.arun()) 62 | except KeyboardInterrupt: 63 | logger.info("Exiting due to Ctrl-C/interrupt") 64 | 65 | async def arun(self): 66 | """ 67 | Runs the asyncio event loop with our handler loop. 68 | """ 69 | 70 | class Done(Exception): 71 | pass 72 | 73 | async def handle(): 74 | await self.handle() 75 | raise Done 76 | 77 | try: 78 | await asyncio.gather(self.application_checker(), handle()) 79 | except Done: 80 | pass 81 | 82 | async def handle(self): 83 | raise NotImplementedError("You must implement handle()") 84 | 85 | async def application_send(self, scope, message): 86 | """ 87 | Receives outbound sends from applications and handles them. 88 | """ 89 | raise NotImplementedError("You must implement application_send()") 90 | 91 | ### Application instance management 92 | 93 | def get_or_create_application_instance(self, scope_id, scope): 94 | """ 95 | Creates an application instance and returns its queue. 96 | """ 97 | if scope_id in self.application_instances: 98 | self.application_instances[scope_id]["last_used"] = time.time() 99 | return self.application_instances[scope_id]["input_queue"] 100 | # See if we need to delete an old one 101 | while len(self.application_instances) > self.max_applications: 102 | self.delete_oldest_application_instance() 103 | # Make an instance of the application 104 | input_queue = asyncio.Queue() 105 | application_instance = guarantee_single_callable(self.application) 106 | # Run it, and stash the future for later checking 107 | future = asyncio.ensure_future( 108 | application_instance( 109 | scope=scope, 110 | receive=input_queue.get, 111 | send=lambda message: self.application_send(scope, message), 112 | ), 113 | ) 114 | self.application_instances[scope_id] = { 115 | "input_queue": input_queue, 116 | "future": future, 117 | "scope": scope, 118 | "last_used": time.time(), 119 | } 120 | return input_queue 121 | 122 | def delete_oldest_application_instance(self): 123 | """ 124 | Finds and deletes the oldest application instance 125 | """ 126 | oldest_time = min( 127 | details["last_used"] for details in self.application_instances.values() 128 | ) 129 | for scope_id, details in self.application_instances.items(): 130 | if details["last_used"] == oldest_time: 131 | self.delete_application_instance(scope_id) 132 | # Return to make sure we only delete one in case two have 133 | # the same oldest time 134 | return 135 | 136 | def delete_application_instance(self, scope_id): 137 | """ 138 | Removes an application instance (makes sure its task is stopped, 139 | then removes it from the current set) 140 | """ 141 | details = self.application_instances[scope_id] 142 | del self.application_instances[scope_id] 143 | if not details["future"].done(): 144 | details["future"].cancel() 145 | 146 | async def application_checker(self): 147 | """ 148 | Goes through the set of current application instance Futures and cleans up 149 | any that are done/prints exceptions for any that errored. 150 | """ 151 | while True: 152 | await asyncio.sleep(self.application_checker_interval) 153 | for scope_id, details in list(self.application_instances.items()): 154 | if details["future"].done(): 155 | exception = details["future"].exception() 156 | if exception: 157 | await self.application_exception(exception, details) 158 | try: 159 | del self.application_instances[scope_id] 160 | except KeyError: 161 | # Exception handling might have already got here before us. That's fine. 162 | pass 163 | 164 | async def application_exception(self, exception, application_details): 165 | """ 166 | Called whenever an application coroutine has an exception. 167 | """ 168 | logging.error( 169 | "Exception inside application: %s\n%s%s", 170 | exception, 171 | "".join(traceback.format_tb(exception.__traceback__)), 172 | f" {exception}", 173 | ) 174 | -------------------------------------------------------------------------------- /asgiref/wsgi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from tempfile import SpooledTemporaryFile 3 | 4 | from asgiref.sync import AsyncToSync, sync_to_async 5 | 6 | 7 | class WsgiToAsgi: 8 | """ 9 | Wraps a WSGI application to make it into an ASGI application. 10 | """ 11 | 12 | def __init__(self, wsgi_application): 13 | self.wsgi_application = wsgi_application 14 | 15 | async def __call__(self, scope, receive, send): 16 | """ 17 | ASGI application instantiation point. 18 | We return a new WsgiToAsgiInstance here with the WSGI app 19 | and the scope, ready to respond when it is __call__ed. 20 | """ 21 | await WsgiToAsgiInstance(self.wsgi_application)(scope, receive, send) 22 | 23 | 24 | class WsgiToAsgiInstance: 25 | """ 26 | Per-socket instance of a wrapped WSGI application 27 | """ 28 | 29 | def __init__(self, wsgi_application): 30 | self.wsgi_application = wsgi_application 31 | self.response_started = False 32 | self.response_content_length = None 33 | 34 | async def __call__(self, scope, receive, send): 35 | if scope["type"] != "http": 36 | raise ValueError("WSGI wrapper received a non-HTTP scope") 37 | self.scope = scope 38 | with SpooledTemporaryFile(max_size=65536) as body: 39 | # Alright, wait for the http.request messages 40 | while True: 41 | message = await receive() 42 | if message["type"] != "http.request": 43 | raise ValueError("WSGI wrapper received a non-HTTP-request message") 44 | body.write(message.get("body", b"")) 45 | if not message.get("more_body"): 46 | break 47 | body.seek(0) 48 | # Wrap send so it can be called from the subthread 49 | self.sync_send = AsyncToSync(send) 50 | # Call the WSGI app 51 | await self.run_wsgi_app(body) 52 | 53 | def build_environ(self, scope, body): 54 | """ 55 | Builds a scope and request body into a WSGI environ object. 56 | """ 57 | script_name = scope.get("root_path", "").encode("utf8").decode("latin1") 58 | path_info = scope["path"].encode("utf8").decode("latin1") 59 | if path_info.startswith(script_name): 60 | path_info = path_info[len(script_name) :] 61 | environ = { 62 | "REQUEST_METHOD": scope["method"], 63 | "SCRIPT_NAME": script_name, 64 | "PATH_INFO": path_info, 65 | "QUERY_STRING": scope["query_string"].decode("ascii"), 66 | "SERVER_PROTOCOL": "HTTP/%s" % scope["http_version"], 67 | "wsgi.version": (1, 0), 68 | "wsgi.url_scheme": scope.get("scheme", "http"), 69 | "wsgi.input": body, 70 | "wsgi.errors": sys.stderr, 71 | "wsgi.multithread": True, 72 | "wsgi.multiprocess": True, 73 | "wsgi.run_once": False, 74 | } 75 | # Get server name and port - required in WSGI, not in ASGI 76 | if "server" in scope: 77 | environ["SERVER_NAME"] = scope["server"][0] 78 | environ["SERVER_PORT"] = str(scope["server"][1]) 79 | else: 80 | environ["SERVER_NAME"] = "localhost" 81 | environ["SERVER_PORT"] = "80" 82 | 83 | if scope.get("client") is not None: 84 | environ["REMOTE_ADDR"] = scope["client"][0] 85 | 86 | # Go through headers and make them into environ entries 87 | for name, value in self.scope.get("headers", []): 88 | name = name.decode("latin1") 89 | if name == "content-length": 90 | corrected_name = "CONTENT_LENGTH" 91 | elif name == "content-type": 92 | corrected_name = "CONTENT_TYPE" 93 | else: 94 | corrected_name = "HTTP_%s" % name.upper().replace("-", "_") 95 | # HTTPbis say only ASCII chars are allowed in headers, but we latin1 just in case 96 | value = value.decode("latin1") 97 | if corrected_name in environ: 98 | value = environ[corrected_name] + "," + value 99 | environ[corrected_name] = value 100 | return environ 101 | 102 | def start_response(self, status, response_headers, exc_info=None): 103 | """ 104 | WSGI start_response callable. 105 | """ 106 | # Don't allow re-calling once response has begun 107 | if self.response_started: 108 | raise exc_info[1].with_traceback(exc_info[2]) 109 | # Don't allow re-calling without exc_info 110 | if hasattr(self, "response_start") and exc_info is None: 111 | raise ValueError( 112 | "You cannot call start_response a second time without exc_info" 113 | ) 114 | # Extract status code 115 | status_code, _ = status.split(" ", 1) 116 | status_code = int(status_code) 117 | # Extract headers 118 | headers = [ 119 | (name.lower().encode("ascii"), value.encode("ascii")) 120 | for name, value in response_headers 121 | ] 122 | # Extract content-length 123 | self.response_content_length = None 124 | for name, value in response_headers: 125 | if name.lower() == "content-length": 126 | self.response_content_length = int(value) 127 | # Build and send response start message. 128 | self.response_start = { 129 | "type": "http.response.start", 130 | "status": status_code, 131 | "headers": headers, 132 | } 133 | 134 | @sync_to_async 135 | def run_wsgi_app(self, body): 136 | """ 137 | Called in a subthread to run the WSGI app. We encapsulate like 138 | this so that the start_response callable is called in the same thread. 139 | """ 140 | # Translate the scope and incoming request body into a WSGI environ 141 | environ = self.build_environ(self.scope, body) 142 | # Run the WSGI app 143 | bytes_sent = 0 144 | for output in self.wsgi_application(environ, self.start_response): 145 | # If this is the first response, include the response headers 146 | if not self.response_started: 147 | self.response_started = True 148 | self.sync_send(self.response_start) 149 | # If the application supplies a Content-Length header 150 | if self.response_content_length is not None: 151 | # The server should not transmit more bytes to the client than the header allows 152 | bytes_allowed = self.response_content_length - bytes_sent 153 | if len(output) > bytes_allowed: 154 | output = output[:bytes_allowed] 155 | self.sync_send( 156 | {"type": "http.response.body", "body": output, "more_body": True} 157 | ) 158 | bytes_sent += len(output) 159 | # The server should stop iterating over the response when enough data has been sent 160 | if bytes_sent == self.response_content_length: 161 | break 162 | # Close connection 163 | if not self.response_started: 164 | self.response_started = True 165 | self.sync_send(self.response_start) 166 | self.sync_send({"type": "http.response.body"}) 167 | -------------------------------------------------------------------------------- /asgiref/typing.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import ( 3 | Any, 4 | Awaitable, 5 | Callable, 6 | Dict, 7 | Iterable, 8 | Literal, 9 | Optional, 10 | Protocol, 11 | Tuple, 12 | Type, 13 | TypedDict, 14 | Union, 15 | ) 16 | 17 | if sys.version_info >= (3, 11): 18 | from typing import NotRequired 19 | else: 20 | from typing_extensions import NotRequired 21 | 22 | __all__ = ( 23 | "ASGIVersions", 24 | "HTTPScope", 25 | "WebSocketScope", 26 | "LifespanScope", 27 | "WWWScope", 28 | "Scope", 29 | "HTTPRequestEvent", 30 | "HTTPResponseStartEvent", 31 | "HTTPResponseBodyEvent", 32 | "HTTPResponseTrailersEvent", 33 | "HTTPResponsePathsendEvent", 34 | "HTTPServerPushEvent", 35 | "HTTPDisconnectEvent", 36 | "WebSocketConnectEvent", 37 | "WebSocketAcceptEvent", 38 | "WebSocketReceiveEvent", 39 | "WebSocketSendEvent", 40 | "WebSocketResponseStartEvent", 41 | "WebSocketResponseBodyEvent", 42 | "WebSocketDisconnectEvent", 43 | "WebSocketCloseEvent", 44 | "LifespanStartupEvent", 45 | "LifespanShutdownEvent", 46 | "LifespanStartupCompleteEvent", 47 | "LifespanStartupFailedEvent", 48 | "LifespanShutdownCompleteEvent", 49 | "LifespanShutdownFailedEvent", 50 | "ASGIReceiveEvent", 51 | "ASGISendEvent", 52 | "ASGIReceiveCallable", 53 | "ASGISendCallable", 54 | "ASGI2Protocol", 55 | "ASGI2Application", 56 | "ASGI3Application", 57 | "ASGIApplication", 58 | ) 59 | 60 | 61 | class ASGIVersions(TypedDict): 62 | spec_version: str 63 | version: Union[Literal["2.0"], Literal["3.0"]] 64 | 65 | 66 | class HTTPScope(TypedDict): 67 | type: Literal["http"] 68 | asgi: ASGIVersions 69 | http_version: str 70 | method: str 71 | scheme: str 72 | path: str 73 | raw_path: bytes 74 | query_string: bytes 75 | root_path: str 76 | headers: Iterable[Tuple[bytes, bytes]] 77 | client: Optional[Tuple[str, int]] 78 | server: Optional[Tuple[str, Optional[int]]] 79 | state: NotRequired[Dict[str, Any]] 80 | extensions: Optional[Dict[str, Dict[object, object]]] 81 | 82 | 83 | class WebSocketScope(TypedDict): 84 | type: Literal["websocket"] 85 | asgi: ASGIVersions 86 | http_version: str 87 | scheme: str 88 | path: str 89 | raw_path: bytes 90 | query_string: bytes 91 | root_path: str 92 | headers: Iterable[Tuple[bytes, bytes]] 93 | client: Optional[Tuple[str, int]] 94 | server: Optional[Tuple[str, Optional[int]]] 95 | subprotocols: Iterable[str] 96 | state: NotRequired[Dict[str, Any]] 97 | extensions: Optional[Dict[str, Dict[object, object]]] 98 | 99 | 100 | class LifespanScope(TypedDict): 101 | type: Literal["lifespan"] 102 | asgi: ASGIVersions 103 | state: NotRequired[Dict[str, Any]] 104 | 105 | 106 | WWWScope = Union[HTTPScope, WebSocketScope] 107 | Scope = Union[HTTPScope, WebSocketScope, LifespanScope] 108 | 109 | 110 | class HTTPRequestEvent(TypedDict): 111 | type: Literal["http.request"] 112 | body: bytes 113 | more_body: bool 114 | 115 | 116 | class HTTPResponseDebugEvent(TypedDict): 117 | type: Literal["http.response.debug"] 118 | info: Dict[str, object] 119 | 120 | 121 | class HTTPResponseStartEvent(TypedDict): 122 | type: Literal["http.response.start"] 123 | status: int 124 | headers: Iterable[Tuple[bytes, bytes]] 125 | trailers: bool 126 | 127 | 128 | class HTTPResponseBodyEvent(TypedDict): 129 | type: Literal["http.response.body"] 130 | body: bytes 131 | more_body: bool 132 | 133 | 134 | class HTTPResponseTrailersEvent(TypedDict): 135 | type: Literal["http.response.trailers"] 136 | headers: Iterable[Tuple[bytes, bytes]] 137 | more_trailers: bool 138 | 139 | 140 | class HTTPResponsePathsendEvent(TypedDict): 141 | type: Literal["http.response.pathsend"] 142 | path: str 143 | 144 | 145 | class HTTPServerPushEvent(TypedDict): 146 | type: Literal["http.response.push"] 147 | path: str 148 | headers: Iterable[Tuple[bytes, bytes]] 149 | 150 | 151 | class HTTPDisconnectEvent(TypedDict): 152 | type: Literal["http.disconnect"] 153 | 154 | 155 | class WebSocketConnectEvent(TypedDict): 156 | type: Literal["websocket.connect"] 157 | 158 | 159 | class WebSocketAcceptEvent(TypedDict): 160 | type: Literal["websocket.accept"] 161 | subprotocol: Optional[str] 162 | headers: Iterable[Tuple[bytes, bytes]] 163 | 164 | 165 | class WebSocketReceiveEvent(TypedDict): 166 | type: Literal["websocket.receive"] 167 | bytes: Optional[bytes] 168 | text: Optional[str] 169 | 170 | 171 | class WebSocketSendEvent(TypedDict): 172 | type: Literal["websocket.send"] 173 | bytes: Optional[bytes] 174 | text: Optional[str] 175 | 176 | 177 | class WebSocketResponseStartEvent(TypedDict): 178 | type: Literal["websocket.http.response.start"] 179 | status: int 180 | headers: Iterable[Tuple[bytes, bytes]] 181 | 182 | 183 | class WebSocketResponseBodyEvent(TypedDict): 184 | type: Literal["websocket.http.response.body"] 185 | body: bytes 186 | more_body: bool 187 | 188 | 189 | class WebSocketDisconnectEvent(TypedDict): 190 | type: Literal["websocket.disconnect"] 191 | code: int 192 | reason: Optional[str] 193 | 194 | 195 | class WebSocketCloseEvent(TypedDict): 196 | type: Literal["websocket.close"] 197 | code: int 198 | reason: Optional[str] 199 | 200 | 201 | class LifespanStartupEvent(TypedDict): 202 | type: Literal["lifespan.startup"] 203 | 204 | 205 | class LifespanShutdownEvent(TypedDict): 206 | type: Literal["lifespan.shutdown"] 207 | 208 | 209 | class LifespanStartupCompleteEvent(TypedDict): 210 | type: Literal["lifespan.startup.complete"] 211 | 212 | 213 | class LifespanStartupFailedEvent(TypedDict): 214 | type: Literal["lifespan.startup.failed"] 215 | message: str 216 | 217 | 218 | class LifespanShutdownCompleteEvent(TypedDict): 219 | type: Literal["lifespan.shutdown.complete"] 220 | 221 | 222 | class LifespanShutdownFailedEvent(TypedDict): 223 | type: Literal["lifespan.shutdown.failed"] 224 | message: str 225 | 226 | 227 | ASGIReceiveEvent = Union[ 228 | HTTPRequestEvent, 229 | HTTPDisconnectEvent, 230 | WebSocketConnectEvent, 231 | WebSocketReceiveEvent, 232 | WebSocketDisconnectEvent, 233 | LifespanStartupEvent, 234 | LifespanShutdownEvent, 235 | ] 236 | 237 | 238 | ASGISendEvent = Union[ 239 | HTTPResponseStartEvent, 240 | HTTPResponseBodyEvent, 241 | HTTPResponseTrailersEvent, 242 | HTTPServerPushEvent, 243 | HTTPDisconnectEvent, 244 | WebSocketAcceptEvent, 245 | WebSocketSendEvent, 246 | WebSocketResponseStartEvent, 247 | WebSocketResponseBodyEvent, 248 | WebSocketCloseEvent, 249 | LifespanStartupCompleteEvent, 250 | LifespanStartupFailedEvent, 251 | LifespanShutdownCompleteEvent, 252 | LifespanShutdownFailedEvent, 253 | ] 254 | 255 | 256 | ASGIReceiveCallable = Callable[[], Awaitable[ASGIReceiveEvent]] 257 | ASGISendCallable = Callable[[ASGISendEvent], Awaitable[None]] 258 | 259 | 260 | class ASGI2Protocol(Protocol): 261 | def __init__(self, scope: Scope) -> None: 262 | ... 263 | 264 | async def __call__( 265 | self, receive: ASGIReceiveCallable, send: ASGISendCallable 266 | ) -> None: 267 | ... 268 | 269 | 270 | ASGI2Application = Type[ASGI2Protocol] 271 | ASGI3Application = Callable[ 272 | [ 273 | Scope, 274 | ASGIReceiveCallable, 275 | ASGISendCallable, 276 | ], 277 | Awaitable[None], 278 | ] 279 | ASGIApplication = Union[ASGI2Application, ASGI3Application] 280 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | asgiref 2 | ======= 3 | 4 | .. image:: https://github.com/django/asgiref/actions/workflows/tests.yml/badge.svg 5 | :target: https://github.com/django/asgiref/actions/workflows/tests.yml 6 | 7 | .. image:: https://img.shields.io/pypi/v/asgiref.svg 8 | :target: https://pypi.python.org/pypi/asgiref 9 | 10 | ASGI is a standard for Python asynchronous web apps and servers to communicate 11 | with each other, and positioned as an asynchronous successor to WSGI. You can 12 | read more at https://asgi.readthedocs.io/en/latest/ 13 | 14 | This package includes ASGI base libraries, such as: 15 | 16 | * Sync-to-async and async-to-sync function wrappers, ``asgiref.sync`` 17 | * Server base classes, ``asgiref.server`` 18 | * A WSGI-to-ASGI adapter, in ``asgiref.wsgi`` 19 | 20 | 21 | Function wrappers 22 | ----------------- 23 | 24 | These allow you to wrap or decorate async or sync functions to call them from 25 | the other style (so you can call async functions from a synchronous thread, 26 | or vice-versa). 27 | 28 | In particular: 29 | 30 | * AsyncToSync lets a synchronous subthread stop and wait while the async 31 | function is called on the main thread's event loop, and then control is 32 | returned to the thread when the async function is finished. 33 | 34 | * SyncToAsync lets async code call a synchronous function, which is run in 35 | a threadpool and control returned to the async coroutine when the synchronous 36 | function completes. 37 | 38 | The idea is to make it easier to call synchronous APIs from async code and 39 | asynchronous APIs from synchronous code so it's easier to transition code from 40 | one style to the other. In the case of Channels, we wrap the (synchronous) 41 | Django view system with SyncToAsync to allow it to run inside the (asynchronous) 42 | ASGI server. 43 | 44 | Note that exactly what threads things run in is very specific, and aimed to 45 | keep maximum compatibility with old synchronous code. See 46 | "Synchronous code & Threads" below for a full explanation. By default, 47 | ``sync_to_async`` will run all synchronous code in the program in the same 48 | thread for safety reasons; you can disable this for more performance with 49 | ``@sync_to_async(thread_sensitive=False)``, but make sure that your code does 50 | not rely on anything bound to threads (like database connections) when you do. 51 | 52 | 53 | Threadlocal replacement 54 | ----------------------- 55 | 56 | This is a drop-in replacement for ``threading.local`` that works with both 57 | threads and asyncio Tasks. Even better, it will proxy values through from a 58 | task-local context to a thread-local context when you use ``sync_to_async`` 59 | to run things in a threadpool, and vice-versa for ``async_to_sync``. 60 | 61 | If you instead want true thread- and task-safety, you can set 62 | ``thread_critical`` on the Local object to ensure this instead. 63 | 64 | 65 | Server base classes 66 | ------------------- 67 | 68 | Includes a ``StatelessServer`` class which provides all the hard work of 69 | writing a stateless server (as in, does not handle direct incoming sockets 70 | but instead consumes external streams or sockets to work out what is happening). 71 | 72 | An example of such a server would be a chatbot server that connects out to 73 | a central chat server and provides a "connection scope" per user chatting to 74 | it. There's only one actual connection, but the server has to separate things 75 | into several scopes for easier writing of the code. 76 | 77 | You can see an example of this being used in `frequensgi `_. 78 | 79 | 80 | WSGI-to-ASGI adapter 81 | -------------------- 82 | 83 | Allows you to wrap a WSGI application so it appears as a valid ASGI application. 84 | 85 | Simply wrap it around your WSGI application like so:: 86 | 87 | asgi_application = WsgiToAsgi(wsgi_application) 88 | 89 | The WSGI application will be run in a synchronous threadpool, and the wrapped 90 | ASGI application will be one that accepts ``http`` class messages. 91 | 92 | Please note that not all extended features of WSGI may be supported (such as 93 | file handles for incoming POST bodies). 94 | 95 | 96 | Dependencies 97 | ------------ 98 | 99 | ``asgiref`` requires Python 3.9 or higher. 100 | 101 | 102 | Contributing 103 | ------------ 104 | 105 | Please refer to the 106 | `main Channels contributing docs `_. 107 | 108 | 109 | Testing 110 | ''''''' 111 | 112 | To run tests, make sure you have installed the ``tests`` extra with the package:: 113 | 114 | cd asgiref/ 115 | pip install -e .[tests] 116 | pytest 117 | 118 | 119 | Building the documentation 120 | '''''''''''''''''''''''''' 121 | 122 | The documentation uses `Sphinx `_:: 123 | 124 | cd asgiref/docs/ 125 | pip install sphinx 126 | 127 | To build the docs, you can use the default tools:: 128 | 129 | sphinx-build -b html . _build/html # or `make html`, if you've got make set up 130 | cd _build/html 131 | python -m http.server 132 | 133 | ...or you can use ``sphinx-autobuild`` to run a server and rebuild/reload 134 | your documentation changes automatically:: 135 | 136 | pip install sphinx-autobuild 137 | sphinx-autobuild . _build/html 138 | 139 | 140 | Releasing 141 | ''''''''' 142 | 143 | To release, first add details to CHANGELOG.txt and update the version number in ``asgiref/__init__.py``. 144 | 145 | Then, build and push the packages:: 146 | 147 | python -m build 148 | twine upload dist/* 149 | rm -r asgiref.egg-info dist 150 | 151 | 152 | Implementation Details 153 | ---------------------- 154 | 155 | Synchronous code & threads 156 | '''''''''''''''''''''''''' 157 | 158 | The ``asgiref.sync`` module provides two wrappers that let you go between 159 | asynchronous and synchronous code at will, while taking care of the rough edges 160 | for you. 161 | 162 | Unfortunately, the rough edges are numerous, and the code has to work especially 163 | hard to keep things in the same thread as much as possible. Notably, the 164 | restrictions we are working with are: 165 | 166 | * All synchronous code called through ``SyncToAsync`` and marked with 167 | ``thread_sensitive`` should run in the same thread as each other (and if the 168 | outer layer of the program is synchronous, the main thread) 169 | 170 | * If a thread already has a running async loop, ``AsyncToSync`` can't run things 171 | on that loop if it's blocked on synchronous code that is above you in the 172 | call stack. 173 | 174 | The first compromise you get to might be that ``thread_sensitive`` code should 175 | just run in the same thread and not spawn in a sub-thread, fulfilling the first 176 | restriction, but that immediately runs you into the second restriction. 177 | 178 | The only real solution is to essentially have a variant of ThreadPoolExecutor 179 | that executes any ``thread_sensitive`` code on the outermost synchronous 180 | thread - either the main thread, or a single spawned subthread. 181 | 182 | This means you now have two basic states: 183 | 184 | * If the outermost layer of your program is synchronous, then all async code 185 | run through ``AsyncToSync`` will run in a per-call event loop in arbitrary 186 | sub-threads, while all ``thread_sensitive`` code will run in the main thread. 187 | 188 | * If the outermost layer of your program is asynchronous, then all async code 189 | runs on the main thread's event loop, and all ``thread_sensitive`` synchronous 190 | code will run in a single shared sub-thread. 191 | 192 | Crucially, this means that in both cases there is a thread which is a shared 193 | resource that all ``thread_sensitive`` code must run on, and there is a chance 194 | that this thread is currently blocked on its own ``AsyncToSync`` call. Thus, 195 | ``AsyncToSync`` needs to act as an executor for thread code while it's blocking. 196 | 197 | The ``CurrentThreadExecutor`` class provides this functionality; rather than 198 | simply waiting on a Future, you can call its ``run_until_future`` method and 199 | it will run submitted code until that Future is done. This means that code 200 | inside the call can then run code on your thread. 201 | 202 | 203 | Maintenance and Security 204 | ------------------------ 205 | 206 | To report security issues, please contact security@djangoproject.com. For GPG 207 | signatures and more security process information, see 208 | https://docs.djangoproject.com/en/dev/internals/security/. 209 | 210 | To report bugs or request new features, please open a new GitHub issue. 211 | 212 | This repository is part of the Channels project. For the shepherd and maintenance team, please see the 213 | `main Channels readme `_. 214 | -------------------------------------------------------------------------------- /tests/test_wsgi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | 5 | from asgiref.testing import ApplicationCommunicator 6 | from asgiref.wsgi import WsgiToAsgi, WsgiToAsgiInstance 7 | 8 | 9 | @pytest.mark.asyncio 10 | async def test_basic_wsgi(): 11 | """ 12 | Makes sure the WSGI wrapper has basic functionality. 13 | """ 14 | 15 | # Define WSGI app 16 | def wsgi_application(environ, start_response): 17 | assert environ["HTTP_TEST_HEADER"] == "test value 1,test value 2" 18 | start_response("200 OK", [["X-Colour", "Blue"]]) 19 | yield b"first chunk " 20 | yield b"second chunk" 21 | 22 | # Wrap it 23 | application = WsgiToAsgi(wsgi_application) 24 | # Launch it as a test application 25 | instance = ApplicationCommunicator( 26 | application, 27 | { 28 | "type": "http", 29 | "http_version": "1.0", 30 | "method": "GET", 31 | "path": "/foo/", 32 | "query_string": b"bar=baz", 33 | "headers": [ 34 | [b"test-header", b"test value 1"], 35 | [b"test-header", b"test value 2"], 36 | ], 37 | }, 38 | ) 39 | await instance.send_input({"type": "http.request"}) 40 | # Check they send stuff 41 | assert (await instance.receive_output(1)) == { 42 | "type": "http.response.start", 43 | "status": 200, 44 | "headers": [(b"x-colour", b"Blue")], 45 | } 46 | assert (await instance.receive_output(1)) == { 47 | "type": "http.response.body", 48 | "body": b"first chunk ", 49 | "more_body": True, 50 | } 51 | assert (await instance.receive_output(1)) == { 52 | "type": "http.response.body", 53 | "body": b"second chunk", 54 | "more_body": True, 55 | } 56 | assert (await instance.receive_output(1)) == {"type": "http.response.body"} 57 | 58 | 59 | def test_script_name(): 60 | scope = { 61 | "type": "http", 62 | "http_version": "1.0", 63 | "method": "GET", 64 | "root_path": "/base", 65 | "path": "/base/foo/", 66 | "query_string": b"bar=baz", 67 | "headers": [ 68 | [b"test-header", b"test value 1"], 69 | [b"test-header", b"test value 2"], 70 | ], 71 | } 72 | adapter = WsgiToAsgiInstance(None) 73 | adapter.scope = scope 74 | environ = adapter.build_environ(scope, None) 75 | assert environ["SCRIPT_NAME"] == "/base" 76 | assert environ["PATH_INFO"] == "/foo/" 77 | 78 | 79 | @pytest.mark.asyncio 80 | async def test_wsgi_path_encoding(): 81 | """ 82 | Makes sure the WSGI wrapper has basic functionality. 83 | """ 84 | 85 | # Define WSGI app 86 | def wsgi_application(environ, start_response): 87 | assert environ["SCRIPT_NAME"] == "/中国".encode().decode("latin-1") 88 | assert environ["PATH_INFO"] == "/中文".encode().decode("latin-1") 89 | start_response("200 OK", []) 90 | yield b"" 91 | 92 | # Wrap it 93 | application = WsgiToAsgi(wsgi_application) 94 | # Launch it as a test application 95 | instance = ApplicationCommunicator( 96 | application, 97 | { 98 | "type": "http", 99 | "http_version": "1.0", 100 | "method": "GET", 101 | "path": "/中文", 102 | "root_path": "/中国", 103 | "query_string": b"bar=baz", 104 | "headers": [], 105 | }, 106 | ) 107 | await instance.send_input({"type": "http.request"}) 108 | # Check they send stuff 109 | assert (await instance.receive_output(1)) == { 110 | "type": "http.response.start", 111 | "status": 200, 112 | "headers": [], 113 | } 114 | assert (await instance.receive_output(1)) == { 115 | "type": "http.response.body", 116 | "body": b"", 117 | "more_body": True, 118 | } 119 | assert (await instance.receive_output(1)) == {"type": "http.response.body"} 120 | 121 | 122 | @pytest.mark.asyncio 123 | async def test_wsgi_empty_body(): 124 | """ 125 | Makes sure WsgiToAsgi handles an empty body response correctly 126 | """ 127 | 128 | def wsgi_application(environ, start_response): 129 | start_response("200 OK", []) 130 | return [] 131 | 132 | application = WsgiToAsgi(wsgi_application) 133 | instance = ApplicationCommunicator( 134 | application, 135 | { 136 | "type": "http", 137 | "http_version": "1.0", 138 | "method": "GET", 139 | "path": "/", 140 | "query_string": b"", 141 | "headers": [], 142 | }, 143 | ) 144 | await instance.send_input({"type": "http.request"}) 145 | 146 | # response.start should always be send 147 | assert (await instance.receive_output(1)) == { 148 | "type": "http.response.start", 149 | "status": 200, 150 | "headers": [], 151 | } 152 | 153 | assert (await instance.receive_output(1)) == {"type": "http.response.body"} 154 | 155 | 156 | @pytest.mark.asyncio 157 | async def test_wsgi_clamped_body(): 158 | """ 159 | Makes sure WsgiToAsgi clamps a body response longer than Content-Length 160 | """ 161 | 162 | def wsgi_application(environ, start_response): 163 | start_response("200 OK", [("Content-Length", "8")]) 164 | return [b"0123", b"45", b"6789"] 165 | 166 | application = WsgiToAsgi(wsgi_application) 167 | instance = ApplicationCommunicator( 168 | application, 169 | { 170 | "type": "http", 171 | "http_version": "1.0", 172 | "method": "GET", 173 | "path": "/", 174 | "query_string": b"", 175 | "headers": [], 176 | }, 177 | ) 178 | await instance.send_input({"type": "http.request"}) 179 | assert (await instance.receive_output(1)) == { 180 | "type": "http.response.start", 181 | "status": 200, 182 | "headers": [(b"content-length", b"8")], 183 | } 184 | assert (await instance.receive_output(1)) == { 185 | "type": "http.response.body", 186 | "body": b"0123", 187 | "more_body": True, 188 | } 189 | assert (await instance.receive_output(1)) == { 190 | "type": "http.response.body", 191 | "body": b"45", 192 | "more_body": True, 193 | } 194 | assert (await instance.receive_output(1)) == { 195 | "type": "http.response.body", 196 | "body": b"67", 197 | "more_body": True, 198 | } 199 | assert (await instance.receive_output(1)) == {"type": "http.response.body"} 200 | 201 | 202 | @pytest.mark.asyncio 203 | async def test_wsgi_stops_iterating_after_content_length_bytes(): 204 | """ 205 | Makes sure WsgiToAsgi does not iterate after than Content-Length bytes 206 | """ 207 | 208 | def wsgi_application(environ, start_response): 209 | start_response("200 OK", [("Content-Length", "4")]) 210 | yield b"0123" 211 | pytest.fail("WsgiToAsgi should not iterate after Content-Length bytes") 212 | yield b"4567" 213 | 214 | application = WsgiToAsgi(wsgi_application) 215 | instance = ApplicationCommunicator( 216 | application, 217 | { 218 | "type": "http", 219 | "http_version": "1.0", 220 | "method": "GET", 221 | "path": "/", 222 | "query_string": b"", 223 | "headers": [], 224 | }, 225 | ) 226 | await instance.send_input({"type": "http.request"}) 227 | assert (await instance.receive_output(1)) == { 228 | "type": "http.response.start", 229 | "status": 200, 230 | "headers": [(b"content-length", b"4")], 231 | } 232 | assert (await instance.receive_output(1)) == { 233 | "type": "http.response.body", 234 | "body": b"0123", 235 | "more_body": True, 236 | } 237 | assert (await instance.receive_output(1)) == {"type": "http.response.body"} 238 | 239 | 240 | @pytest.mark.asyncio 241 | async def test_wsgi_multiple_start_response(): 242 | """ 243 | Makes sure WsgiToAsgi only keep Content-Length from the last call to start_response 244 | """ 245 | 246 | def wsgi_application(environ, start_response): 247 | start_response("200 OK", [("Content-Length", "5")]) 248 | try: 249 | raise ValueError("Application Error") 250 | except ValueError: 251 | start_response("500 Server Error", [], sys.exc_info()) 252 | return [b"Some long error message"] 253 | 254 | application = WsgiToAsgi(wsgi_application) 255 | instance = ApplicationCommunicator( 256 | application, 257 | { 258 | "type": "http", 259 | "http_version": "1.0", 260 | "method": "GET", 261 | "path": "/", 262 | "query_string": b"", 263 | "headers": [], 264 | }, 265 | ) 266 | await instance.send_input({"type": "http.request"}) 267 | assert (await instance.receive_output(1)) == { 268 | "type": "http.response.start", 269 | "status": 500, 270 | "headers": [], 271 | } 272 | assert (await instance.receive_output(1)) == { 273 | "type": "http.response.body", 274 | "body": b"Some long error message", 275 | "more_body": True, 276 | } 277 | assert (await instance.receive_output(1)) == {"type": "http.response.body"} 278 | 279 | 280 | @pytest.mark.asyncio 281 | async def test_wsgi_multi_body(): 282 | """ 283 | Verify that multiple http.request events with body parts are all delivered 284 | to the WSGI application. 285 | """ 286 | 287 | def wsgi_application(environ, start_response): 288 | infp = environ["wsgi.input"] 289 | body = infp.read(12) 290 | assert body == b"Hello World!" 291 | start_response("200 OK", []) 292 | return [] 293 | 294 | application = WsgiToAsgi(wsgi_application) 295 | instance = ApplicationCommunicator( 296 | application, 297 | { 298 | "type": "http", 299 | "http_version": "1.0", 300 | "method": "POST", 301 | "path": "/", 302 | "query_string": b"", 303 | "headers": [[b"content-length", b"12"]], 304 | }, 305 | ) 306 | await instance.send_input( 307 | {"type": "http.request", "body": b"Hello ", "more_body": True} 308 | ) 309 | await instance.send_input({"type": "http.request", "body": b"World!"}) 310 | 311 | assert (await instance.receive_output(1)) == { 312 | "type": "http.response.start", 313 | "status": 200, 314 | "headers": [], 315 | } 316 | 317 | assert (await instance.receive_output(1)) == {"type": "http.response.body"} 318 | -------------------------------------------------------------------------------- /specs/tls.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | ASGI TLS Extension 3 | ================== 4 | 5 | **Version**: 0.2 (2020-10-02) 6 | 7 | This specification outlines how to report TLS (or SSL) connection information 8 | in the ASGI *connection scope* object. 9 | 10 | The Base Protocol 11 | ----------------- 12 | 13 | TLS is not usable on its own, it always wraps another protocol. 14 | So this specification is not designed to be usable on its own, 15 | it must be used as an extension to another ASGI specification. 16 | That other ASGI specification is referred to as the *base protocol 17 | specification*. 18 | 19 | For HTTP-over-TLS (HTTPS), use this TLS specification and the 20 | ASGI HTTP specification. The *base protocol specification* is the 21 | ASGI HTTP specification. (See :doc:`www`) 22 | 23 | For WebSockets-over-TLS (wss:// protocol), use this TLS specification 24 | and the ASGI WebSockets specification. The *base protocol specification* 25 | is the ASGI WebSockets specification. (See :doc:`www`) 26 | 27 | If using this extension with other protocols (not HTTPS or WebSockets), note 28 | that the *base protocol specification* must define the *connection scope* in a 29 | way that ensures it covers at most one TLS connection. If not, you cannot use 30 | this extension. 31 | 32 | When to use this extension 33 | -------------------------- 34 | 35 | This extension must only be used for TLS connections. 36 | 37 | For non-TLS connections, the ASGI server is forbidden from providing this 38 | extension. 39 | 40 | An ASGI application can check for the presence of the ``"tls"`` extension in 41 | the ``extensions`` dictionary in the connection scope. If present, the server 42 | supports this extension and the connection is over TLS. If not present, 43 | either the server does not support this extension or the connection is not 44 | over TLS. 45 | 46 | TLS Connection Scope 47 | -------------------- 48 | 49 | The *connection scope* information passed in ``scope`` contains an 50 | ``"extensions"`` key, which contains a dictionary of extensions. Inside that 51 | dictionary, the key ``"tls"`` identifies the extension specified in this 52 | document. The value will be a dictionary with the following entries: 53 | 54 | * ``server_cert`` (*Unicode string or None*) -- The PEM-encoded conversion 55 | of the x509 certificate sent by the server when establishing the TLS 56 | connection. Some web server implementations may be unable to provide this 57 | (e.g. if TLS is terminated by a separate proxy or load balancer); in that 58 | case this shall be ``None``. Mandatory. 59 | 60 | * ``client_cert_chain`` (*Iterable[Unicode string]*) -- An iterable of 61 | Unicode strings, where each string is a PEM-encoded x509 certificate. 62 | The first certificate is the client certificate. Any subsequent certificates 63 | are part of the certificate chain sent by the client, with each certificate 64 | signing the preceding one. If the client did not provide a client 65 | certificate then it will be an empty iterable. Some web server 66 | implementations may be unable to provide this (e.g. if TLS is terminated by a 67 | separate proxy or load balancer); in that case this shall be an empty 68 | iterable. Optional; if missing defaults to empty iterable. 69 | 70 | * ``client_cert_name`` (*Unicode string or None*) -- The x509 Distinguished 71 | Name of the Subject of the client certificate, as a single string encoded as 72 | defined in `RFC4514 `_. If the client 73 | did not provide a client certificate then it will be ``None``. Some web 74 | server implementations may be unable to provide this (e.g. if TLS is 75 | terminated by a separate proxy or load balancer); in that case this shall be 76 | ``None``. If ``client_cert_chain`` is provided and non-empty then this field 77 | must be provided and must contain information that is consistent with 78 | ``client_cert_chain[0]``. Note that under some setups, (e.g. where TLS is 79 | terminated by a separate proxy or load balancer and that device forwards the 80 | client certificate name to the web server), this field may be set even where 81 | ``client_cert_chain`` is not set. Optional; if missing defaults to ``None``. 82 | 83 | * ``client_cert_error`` (*Unicode string or None*) -- ``None`` if a client 84 | certificate was provided and successfully verified, or was not provided. 85 | If a client certificate was provided but verification failed, this is a 86 | non-empty string containing an error message or error code indicating why 87 | validation failed; the details are web server specific. Most web server 88 | implementations will reject the connection if the client certificate 89 | verification failed, instead of setting this value. However, some may be 90 | configured to allow the connection anyway. This is especially useful when 91 | testing that client certificates are supported properly by the client - it 92 | allows a response containing an error message that can be presented to a 93 | human, instead of just refusing the connection. Optional; if missing defaults 94 | to ``None``. 95 | 96 | * ``tls_version`` (*integer or None*) -- The TLS version in use. This is one of 97 | the version numbers as defined in the TLS specifications, which is an 98 | unsigned integer. Common values include ``0x0303`` for TLS 1.2 or ``0x0304`` 99 | for TLS 1.3. If TLS is not in use, set to ``None``. Some web server 100 | implementations may be unable to provide this (e.g. if TLS is terminated by a 101 | separate proxy or load balancer); in that case set to ``None``. Mandatory. 102 | 103 | * ``cipher_suite`` (*integer or None*) -- The TLS cipher suite that is being 104 | used. This is a 16-bit unsigned integer that encodes the pair of 8-bit 105 | integers specified in the relevant RFC, in network byte order. For example 106 | `RFC8446 section B.4 `_ 107 | defines that the cipher suite ``TLS_AES_128_GCM_SHA256`` is ``{0x13, 0x01}``; 108 | that is encoded as a ``cipher_suite`` value of ``0x1301`` (equal to 4865 109 | decimal). Some web server implementations may be unable to provide this 110 | (e.g. if TLS is terminated by a separate proxy or load balancer); in that case 111 | set to ``None``. Mandatory. 112 | 113 | Events 114 | ------ 115 | 116 | All events are as defined in the *base protocol specification*. 117 | 118 | Rationale (Informative) 119 | ----------------------- 120 | 121 | This section explains the choices that led to this specification. 122 | 123 | Providing the entire TLS certificates in ``client_cert_chain``, rather than a 124 | parsed subset: 125 | 126 | * Makes it easier for web servers to implement, as they do not have to 127 | include a parser for the entirety of the x509 certificate specifications 128 | (which are huge and complicated). They just have to convert the binary 129 | DER format certificate from the wire, to the text PEM format. That is 130 | supported by many off-the-shelf libraries. 131 | * Makes it easier for web servers to maintain, as they do not have to update 132 | their parser when new certificate fields are defined. 133 | * Makes it easier for clients as there are plenty of existing x509 libraries 134 | available that they can use to parse the certificate; they don't need to 135 | do some special ASGI-specific thing. 136 | * Improves interoperability as this is a simple, well-defined encoding, that 137 | clients and servers are unlikely to get wrong. 138 | * Makes it much easier to write this specification. There is no standard 139 | documented format for a parsed certificate in Python, and we would need to 140 | write one. 141 | * Makes it much easier to maintain this specification. There is no need 142 | to update a parsed certificate specification when new certificate fields 143 | are defined. 144 | * Allows the client to support new certificate fields without requiring 145 | any server changes, so long as the fields are marked as "non-critical" in 146 | the certificate. (A x509 parser is allowed to ignore non-critical fields 147 | it does not understand. Critical fields that are not understood cause 148 | certificate parsing to fail). 149 | * Allows the client to do weird and wonderful things with the raw certificate, 150 | instead of placing arbitrary limits on it. 151 | 152 | Specifying ``tls_version`` as an integer, not a string or float: 153 | 154 | * Avoids maintenance effort in this specification. If a new version of TLS is 155 | defined, then no changes are needed in this specification. 156 | * Does not significantly affect servers. Whatever format we specified, servers 157 | would likely need a lookup table from what their TLS library reports to what 158 | this API needs. (Unless their TLS library provides access to the raw value, 159 | in which case it can be reported via this API directly). 160 | * Does not significantly affect clients. Whatever format we specified, clients 161 | would likely need a lookup table from what this API reports to the values 162 | they support and wish to use internally. 163 | 164 | Specifying ``cipher_suite`` as an integer, not a string: 165 | 166 | * Avoids significant effort to compile a list of cipher suites in this 167 | specification. There are a huge number of existing TLS cipher suites, many 168 | of which are not widely used, even listing them all would be a huge effort. 169 | * Avoids maintenance effort in this specification. If a new cipher suite is 170 | defined, then no changes are needed in this specification. 171 | * Avoids dependencies on nonstandard TLS-library-specific names. E.g. the 172 | cipher names used by OpenSSL are different from the cipher names used by the 173 | RFCs. 174 | * Does not significantly affect servers. Whatever format we specified, (unless 175 | it was a nonstandard library-specific name and the server happened to use 176 | that library), servers would likely need a lookup table from what their 177 | TLS library reports to what this API needs. (Unless their TLS library 178 | provides access to the raw value, in which case it can be reported via this 179 | API directly). 180 | * Does not significantly affect clients. Whatever format we specified, clients 181 | would likely need a lookup table from what this API reports to the values 182 | they support and wish to use internally. 183 | * Using a single integer, rather than a pair of integers, makes handling this 184 | value simpler and faster. 185 | 186 | ``client_cert_name`` duplicates information that is also available in 187 | ``client_cert_chain``. However, many ASGI applications will probably find 188 | that information is sufficient for their application - it provides a simple 189 | string that identifies the user. It is simpler to use than parsing the x509 190 | certificate. For the server, this information is readily available. 191 | 192 | There are theoretical interoperability problems with ``client_cert_name``, 193 | since it depends on a list of object ID names that is maintained by IANA and 194 | theoretically can change. In practice, this is not a real problem, since the 195 | object IDs that are actually used in certificates have not changed in many 196 | years. So in practice it will be fine. 197 | 198 | 199 | Copyright 200 | --------- 201 | 202 | This document has been placed in the public domain. 203 | -------------------------------------------------------------------------------- /docs/extensions.rst: -------------------------------------------------------------------------------- 1 | Extensions 2 | ========== 3 | 4 | The ASGI specification provides for server-specific extensions to be 5 | used outside of the core ASGI specification. This document specifies 6 | some common extensions. 7 | 8 | 9 | Websocket Denial Response 10 | ------------------------- 11 | 12 | Websocket connections start with the client sending a HTTP request 13 | containing the appropriate upgrade headers. On receipt of this request 14 | a server can choose to either upgrade the connection or respond with an 15 | HTTP response (denying the upgrade). The core ASGI specification does 16 | not allow for any control over the denial response, instead specifying 17 | that the HTTP status code ``403`` should be returned, whereas this 18 | extension allows an ASGI framework to control the 19 | denial response. Rather than being a core part of 20 | ASGI, this is an extension for what is considered a niche feature as most 21 | clients do not utilise the denial response. 22 | 23 | ASGI Servers that implement this extension will provide 24 | ``websocket.http.response`` in the extensions part of the scope:: 25 | 26 | "scope": { 27 | ... 28 | "extensions": { 29 | "websocket.http.response": {}, 30 | }, 31 | } 32 | 33 | This will allow the ASGI Framework to send HTTP response messages 34 | after the ``websocket.connect`` message. These messages cannot be 35 | followed by any other websocket messages as the server should send a 36 | HTTP response and then close the connection. 37 | 38 | The messages themselves should be ``websocket.http.response.start`` 39 | and ``websocket.http.response.body`` with a structure that matches the 40 | ``http.response.start`` and ``http.response.body`` messages defined in 41 | the HTTP part of the core ASGI specification. 42 | 43 | HTTP/2 Server Push 44 | ------------------ 45 | 46 | HTTP/2 allows for a server to push a resource to a client by sending a 47 | push promise. ASGI servers that implement this extension will provide 48 | ``http.response.push`` in the extensions part of the scope:: 49 | 50 | "scope": { 51 | ... 52 | "extensions": { 53 | "http.response.push": {}, 54 | }, 55 | } 56 | 57 | An ASGI framework can initiate a server push by sending a message with 58 | the following keys. This message can be sent at any time after the 59 | *Response Start* message but before the final *Response Body* message. 60 | 61 | Keys: 62 | 63 | * ``type`` (*Unicode string*): ``"http.response.push"`` 64 | 65 | * ``path`` (*Unicode string*): HTTP path from URL, with percent-encoded 66 | sequences and UTF-8 byte sequences decoded into characters. 67 | 68 | * ``headers`` (*Iterable[[byte string, byte string]]*): An iterable of 69 | ``[name, value]`` two-item iterables, where ``name`` is the header name, and 70 | ``value`` is the header value. Header names must be lowercased. Pseudo 71 | headers (present in HTTP/2 and HTTP/3) must not be present. 72 | 73 | The ASGI server should then attempt to send a server push (or push 74 | promise) to the client. If the client supports server push, the server 75 | should create a new connection to a new instance of the application 76 | and treat it as if the client had made a request. 77 | 78 | The ASGI server should set the pseudo ``:authority`` header value to 79 | be the same value as the request that triggered the push promise. 80 | 81 | Zero Copy Send 82 | -------------- 83 | 84 | Zero Copy Send allows you to send the contents of a file descriptor to the 85 | HTTP client with zero copy (where the underlying OS directly handles the data 86 | transfer from a source file or socket without loading it into Python and 87 | writing it out again). 88 | 89 | ASGI servers that implement this extension will provide 90 | ``http.response.zerocopysend`` in the extensions part of the scope:: 91 | 92 | "scope": { 93 | ... 94 | "extensions": { 95 | "http.response.zerocopysend": {}, 96 | }, 97 | } 98 | 99 | The ASGI framework can initiate a zero-copy send by sending a message with 100 | the following keys. This message can be sent at any time after the 101 | *Response Start* message but before the final *Response Body* message, 102 | and can be mixed with ``http.response.body``. It can also be called 103 | multiple times in one response. Except for the characteristics of 104 | zero-copy, it should behave the same as ordinary ``http.response.body``. 105 | 106 | Keys: 107 | 108 | * ``type`` (*Unicode string*): ``"http.response.zerocopysend"`` 109 | 110 | * ``file`` (*file descriptor object*): An opened file descriptor object 111 | with an underlying OS file descriptor that can be used to call 112 | ``os.sendfile``. (e.g. not BytesIO) 113 | 114 | * ``offset`` (*int*): Optional. If this value exists, it will specify 115 | the offset at which sendfile starts to read data from ``file``. 116 | Otherwise, it will be read from the current position of ``file``. 117 | 118 | * ``count`` (*int*): Optional. ``count`` is the number of bytes to 119 | copy between the file descriptors. If omitted, the file will be read until 120 | its end. 121 | 122 | * ``more_body`` (*bool*): Signifies if there is additional content 123 | to come (as part of a Response Body message). If ``False``, response 124 | will be taken as complete and closed, and any further messages on 125 | the channel will be ignored. Optional; if missing defaults to 126 | ``False``. 127 | 128 | After calling this extension to respond, the ASGI application itself should 129 | actively close the used file descriptor - ASGI servers are not responsible for 130 | closing descriptors. 131 | 132 | Path Send 133 | --------- 134 | 135 | Path Send allows you to send the contents of a file path to the 136 | HTTP client without handling file descriptors, offloading the operation 137 | directly to the server. 138 | 139 | ASGI servers that implement this extension will provide 140 | ``http.response.pathsend`` in the extensions part of the scope:: 141 | 142 | "scope": { 143 | ... 144 | "extensions": { 145 | "http.response.pathsend": {}, 146 | }, 147 | } 148 | 149 | The ASGI framework can initiate a path-send by sending a message with 150 | the following keys. This message can be sent at any time after the 151 | *Response Start* message, and cannot be mixed with ``http.response.body``. 152 | It can be called just one time in one response. 153 | Except for the characteristics of path-send, it should behave the same 154 | as ordinary ``http.response.body``. 155 | 156 | Keys: 157 | 158 | * ``type`` (*Unicode string*): ``"http.response.pathsend"`` 159 | 160 | * ``path`` (*Unicode string*): The string representation of the absolute 161 | file path to be sent by the server, platform specific. 162 | 163 | The ASGI application itself is responsible to send the relevant headers 164 | in the *Response Start* message, like the ``Content-Type`` and 165 | ``Content-Length`` headers for the file to be sent. 166 | 167 | TLS 168 | --- 169 | 170 | See :doc:`specs/tls`. 171 | 172 | Early Hints 173 | ----------- 174 | 175 | An informational response with the status code 103 is an Early Hint, 176 | indicating to the client that resources are associated with the 177 | subsequent response, see ``RFC 8297``. ASGI servers that implement 178 | this extension will allow early hints to be sent. These servers will 179 | provide ``http.response.early_hint`` in the extensions part of the 180 | scope:: 181 | 182 | "scope": { 183 | ... 184 | "extensions": { 185 | "http.response.early_hint": {}, 186 | }, 187 | } 188 | 189 | An ASGI framework can send an early hint by sending a message with the 190 | following keys. This message can be sent at any time (and multiple 191 | times) after the *Response Start* message but before the final 192 | *Response Body* message. 193 | 194 | Keys: 195 | 196 | * ``type`` (*Unicode string*): ``"http.response.early_hint"`` 197 | 198 | * ``links`` (*Iterable[byte string]*): An iterable of link header field 199 | values, see ``RFC 8288``. 200 | 201 | The ASGI server should then attempt to send an informational response 202 | to the client with the provided links as ``Link`` headers. The server 203 | may decide to ignore this message, for example if the HTTP/1.1 204 | protocol is used and the server has security concerns. 205 | 206 | HTTP Trailers 207 | ------------- 208 | 209 | The Trailer response header allows the sender to include additional fields at the 210 | end of chunked messages in order to supply metadata that might be dynamically 211 | generated while the message body is sent, such as a message integrity check, 212 | digital signature, or post-processing status. 213 | 214 | ASGI servers that implement this extension will provide 215 | ``http.response.trailers`` in the extensions part of the scope:: 216 | 217 | "scope": { 218 | ... 219 | "extensions": { 220 | "http.response.trailers": {}, 221 | }, 222 | } 223 | 224 | An ASGI framework interested in sending trailing headers to the client, must set the 225 | field ``trailers`` in *Response Start* as ``True``. That will allow the ASGI server 226 | to know that after the last ``http.response.body`` message (``more_body`` being ``False``), 227 | the ASGI framework will send a ``http.response.trailers`` message. 228 | 229 | The ASGI framework is in charge of sending the ``Trailer`` headers to let the client know 230 | which trailing headers the server will send. The ASGI server is not responsible for validating 231 | the ``Trailer`` headers provided. 232 | 233 | Keys: 234 | 235 | * ``type`` (*Unicode string*): ``"http.response.trailers"`` 236 | 237 | * ``headers`` (*Iterable[[byte string, byte string]]*): An iterable of 238 | ``[name, value]`` two-item iterables, where ``name`` is the header name, and 239 | ``value`` is the header value. Header names must be lowercased. Pseudo 240 | headers (present in HTTP/2 and HTTP/3) must not be present. 241 | 242 | * ``more_trailers`` (*bool*): Signifies if there is additional content 243 | to come (as part of a *HTTP Trailers* message). If ``False``, response 244 | will be taken as complete and closed, and any further messages on 245 | the channel will be ignored. Optional; if missing defaults to 246 | ``False``. 247 | 248 | 249 | The ASGI server will only send the trailing headers in case the client has sent the 250 | ``TE: trailers`` header in the request. 251 | 252 | Debug 253 | ----- 254 | 255 | The debug extension allows a way to send debug information from an ASGI framework in 256 | its responses. This extension is not meant to be used in production, only for testing purposes, 257 | and ASGI servers should not implement it. 258 | 259 | The ASGI context sent to the framework will provide ``http.response.debug`` in the extensions 260 | part of the scope:: 261 | 262 | "scope": { 263 | ... 264 | "extensions": { 265 | "http.response.debug": {}, 266 | }, 267 | } 268 | 269 | The ASGI framework can send debug information by sending a message with the following 270 | keys. This message must be sent once, before the *Response Start* message. 271 | 272 | Keys: 273 | 274 | * ``type`` (*Unicode string*): ``"http.response.debug"`` 275 | 276 | * ``info`` (*Dict[Unicode string, Any]*): A dictionary containing the debug information. 277 | The keys and values of this dictionary are not defined by the ASGI specification, and 278 | are left to the ASGI framework to define. 279 | -------------------------------------------------------------------------------- /tests/test_local.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import gc 3 | import threading 4 | from threading import Thread 5 | 6 | import pytest 7 | 8 | from asgiref.local import Local 9 | from asgiref.sync import async_to_sync, sync_to_async 10 | 11 | 12 | @pytest.mark.asyncio 13 | async def test_local_task(): 14 | """ 15 | Tests that local works just inside a normal task context 16 | """ 17 | 18 | test_local = Local() 19 | # Unassigned should be an error 20 | with pytest.raises(AttributeError): 21 | test_local.foo 22 | # Assign and check it persists 23 | test_local.foo = 1 24 | assert test_local.foo == 1 25 | # Delete and check it errors again 26 | del test_local.foo 27 | with pytest.raises(AttributeError): 28 | test_local.foo 29 | 30 | 31 | def test_local_thread(): 32 | """ 33 | Tests that local works just inside a normal thread context 34 | """ 35 | 36 | test_local = Local() 37 | # Unassigned should be an error 38 | with pytest.raises(AttributeError): 39 | test_local.foo 40 | # Assign and check it persists 41 | test_local.foo = 2 42 | assert test_local.foo == 2 43 | # Delete and check it errors again 44 | del test_local.foo 45 | with pytest.raises(AttributeError): 46 | test_local.foo 47 | 48 | 49 | def test_local_thread_nested(): 50 | """ 51 | Tests that local does not leak across threads 52 | """ 53 | 54 | test_local = Local() 55 | # Unassigned should be an error 56 | with pytest.raises(AttributeError): 57 | test_local.foo 58 | 59 | # Assign and check it does not persist inside the thread 60 | class TestThread(threading.Thread): 61 | # Failure reason 62 | failed = "unknown" 63 | 64 | def run(self): 65 | # Make sure the attribute is not there 66 | try: 67 | test_local.foo 68 | self.failed = "leak inside" 69 | return 70 | except AttributeError: 71 | pass 72 | # Check the value is good 73 | self.failed = "set inside" 74 | test_local.foo = 123 75 | assert test_local.foo == 123 76 | # Binary signal that these tests passed to the outer thread 77 | self.failed = "" 78 | 79 | test_local.foo = 8 80 | thread = TestThread() 81 | thread.start() 82 | thread.join() 83 | assert thread.failed == "" 84 | # Check it didn't leak back out 85 | assert test_local.foo == 8 86 | 87 | 88 | def test_local_cycle(): 89 | """ 90 | Tests that Local can handle cleanup up a cycle to itself 91 | (Borrowed and modified from the CPython threadlocal tests) 92 | """ 93 | 94 | locals = None 95 | matched = 0 96 | e1 = threading.Event() 97 | e2 = threading.Event() 98 | 99 | def f(): 100 | nonlocal matched 101 | # Involve Local in a cycle 102 | cycle = [Local()] 103 | cycle.append(cycle) 104 | cycle[0].foo = "bar" 105 | 106 | # GC the cycle 107 | del cycle 108 | gc.collect() 109 | 110 | # Trigger the local creation outside 111 | e1.set() 112 | e2.wait() 113 | 114 | # New Locals should be empty 115 | matched = len( 116 | [local for local in locals if getattr(local, "foo", None) == "bar"] 117 | ) 118 | 119 | t = threading.Thread(target=f) 120 | t.start() 121 | e1.wait() 122 | # Creates locals outside of the inner thread 123 | locals = [Local() for i in range(100)] 124 | e2.set() 125 | t.join() 126 | 127 | assert matched == 0 128 | 129 | 130 | @pytest.mark.asyncio 131 | async def test_local_task_to_sync(): 132 | """ 133 | Tests that local carries through sync_to_async 134 | """ 135 | # Set up the local 136 | test_local = Local() 137 | test_local.foo = 3 138 | 139 | # Look at it in a sync context 140 | def sync_function(): 141 | assert test_local.foo == 3 142 | test_local.foo = "phew, done" 143 | 144 | await sync_to_async(sync_function)() 145 | # Check the value passed out again 146 | assert test_local.foo == "phew, done" 147 | 148 | 149 | def test_local_thread_to_async(): 150 | """ 151 | Tests that local carries through async_to_sync 152 | """ 153 | # Set up the local 154 | test_local = Local() 155 | test_local.foo = 12 156 | 157 | # Look at it in an async context 158 | async def async_function(): 159 | assert test_local.foo == 12 160 | test_local.foo = "inside" 161 | 162 | async_to_sync(async_function)() 163 | # Check the value passed out again 164 | assert test_local.foo == "inside" 165 | 166 | 167 | @pytest.mark.asyncio 168 | async def test_local_task_to_sync_to_task(): 169 | """ 170 | Tests that local carries through sync_to_async and then back through 171 | async_to_sync 172 | """ 173 | # Set up the local 174 | test_local = Local() 175 | test_local.foo = 756 176 | 177 | # Look at it in an async context inside a sync context 178 | def sync_function(): 179 | async def async_function(): 180 | assert test_local.foo == 756 181 | test_local.foo = "dragons" 182 | 183 | async_to_sync(async_function)() 184 | 185 | await sync_to_async(sync_function)() 186 | # Check the value passed out again 187 | assert test_local.foo == "dragons" 188 | 189 | 190 | @pytest.mark.asyncio 191 | async def test_local_many_layers(): 192 | """ 193 | Tests that local carries through a lot of layers of sync/async 194 | """ 195 | # Set up the local 196 | test_local = Local() 197 | test_local.foo = 8374 198 | 199 | # Make sure we go between each world at least twice 200 | def sync_function(): 201 | async def async_function(): 202 | def sync_function_2(): 203 | async def async_function_2(): 204 | assert test_local.foo == 8374 205 | test_local.foo = "miracles" 206 | 207 | async_to_sync(async_function_2)() 208 | 209 | await sync_to_async(sync_function_2)() 210 | 211 | async_to_sync(async_function)() 212 | 213 | await sync_to_async(sync_function)() 214 | # Check the value passed out again 215 | assert test_local.foo == "miracles" 216 | 217 | 218 | @pytest.mark.asyncio 219 | async def test_local_critical_no_task_to_thread(): 220 | """ 221 | Tests that local doesn't go through sync_to_async when the local is set 222 | as thread-critical. 223 | """ 224 | # Set up the local 225 | test_local = Local(thread_critical=True) 226 | test_local.foo = 86 227 | 228 | # Look at it in a sync context 229 | def sync_function(): 230 | with pytest.raises(AttributeError): 231 | test_local.foo 232 | test_local.foo = "secret" 233 | assert test_local.foo == "secret" 234 | 235 | await sync_to_async(sync_function)() 236 | # Check the value outside is not touched 237 | assert test_local.foo == 86 238 | 239 | 240 | def test_local_critical_no_thread_to_task(): 241 | """ 242 | Tests that local does not go through async_to_sync when the local is set 243 | as thread-critical 244 | """ 245 | # Set up the local 246 | test_local = Local(thread_critical=True) 247 | test_local.foo = 89 248 | 249 | # Look at it in an async context 250 | async def async_function(): 251 | with pytest.raises(AttributeError): 252 | test_local.foo 253 | test_local.foo = "numbers" 254 | assert test_local.foo == "numbers" 255 | 256 | async_to_sync(async_function)() 257 | # Check the value outside 258 | assert test_local.foo == 89 259 | 260 | 261 | @pytest.mark.asyncio 262 | async def test_local_threads_and_tasks(): 263 | """ 264 | Tests that local and threads don't interfere with each other. 265 | """ 266 | 267 | test_local = Local() 268 | test_local.counter = 0 269 | 270 | def sync_function(expected): 271 | assert test_local.counter == expected 272 | test_local.counter += 1 273 | 274 | async def async_function(expected): 275 | assert test_local.counter == expected 276 | test_local.counter += 1 277 | 278 | await sync_to_async(sync_function)(0) 279 | assert test_local.counter == 1 280 | 281 | await async_function(1) 282 | assert test_local.counter == 2 283 | 284 | class TestThread(threading.Thread): 285 | def run(self): 286 | with pytest.raises(AttributeError): 287 | test_local.counter 288 | test_local.counter = -1 289 | 290 | await sync_to_async(sync_function)(2) 291 | assert test_local.counter == 3 292 | 293 | threads = [TestThread() for _ in range(5)] 294 | for thread in threads: 295 | thread.start() 296 | 297 | threads[0].join() 298 | 299 | await sync_to_async(sync_function)(3) 300 | assert test_local.counter == 4 301 | 302 | await async_function(4) 303 | assert test_local.counter == 5 304 | 305 | for thread in threads[1:]: 306 | thread.join() 307 | 308 | await sync_to_async(sync_function)(5) 309 | assert test_local.counter == 6 310 | 311 | 312 | def test_thread_critical_local_not_context_dependent_in_sync_thread(): 313 | # Test function is sync, thread critical local should 314 | # be visible everywhere in the sync thread, even if set 315 | # from inside a sync_to_async/async_to_sync stack (so 316 | # long as it was set in sync code) 317 | test_local_tc = Local(thread_critical=True) 318 | test_local_not_tc = Local(thread_critical=False) 319 | test_thread = threading.current_thread() 320 | 321 | @sync_to_async 322 | def inner_sync_function(): 323 | # sync_to_async should run this code inside the original 324 | # sync thread, confirm this here 325 | assert test_thread == threading.current_thread() 326 | test_local_tc.test_value = "_123_" 327 | test_local_not_tc.test_value = "_456_" 328 | 329 | @async_to_sync 330 | async def async_function(): 331 | await asyncio.create_task(inner_sync_function()) 332 | 333 | async_function() 334 | 335 | # assert: the inner_sync_function should have set a value 336 | # visible here 337 | assert test_local_tc.test_value == "_123_" 338 | # however, if the local was non-thread-critical, then the 339 | # inner value was set inside a new async context, meaning that 340 | # we do not see it, as context vars don't propagate up the stack 341 | assert not hasattr(test_local_not_tc, "test_value") 342 | 343 | 344 | def test_visibility_thread_asgiref() -> None: 345 | """Check visibility with subthreads.""" 346 | test_local = Local() 347 | test_local.value = 0 348 | 349 | def _test() -> None: 350 | # Local() is cleared when changing thread 351 | assert not hasattr(test_local, "value") 352 | setattr(test_local, "value", 1) 353 | assert test_local.value == 1 354 | 355 | thread = Thread(target=_test) 356 | thread.start() 357 | thread.join() 358 | 359 | assert test_local.value == 0 360 | 361 | 362 | @pytest.mark.asyncio 363 | async def test_visibility_task() -> None: 364 | """Check visibility with asyncio tasks.""" 365 | test_local = Local() 366 | test_local.value = 0 367 | 368 | async def _test() -> None: 369 | # Local is inherited when changing task 370 | assert test_local.value == 0 371 | test_local.value = 1 372 | assert test_local.value == 1 373 | 374 | await asyncio.create_task(_test()) 375 | 376 | # Changes should not leak to the caller 377 | assert test_local.value == 0 378 | 379 | 380 | @pytest.mark.asyncio 381 | async def test_deletion() -> None: 382 | """Check visibility with asyncio tasks.""" 383 | test_local = Local() 384 | test_local.value = 123 385 | 386 | async def _test() -> None: 387 | # Local is inherited when changing task 388 | assert test_local.value == 123 389 | del test_local.value 390 | assert not hasattr(test_local, "value") 391 | 392 | await asyncio.create_task(_test()) 393 | assert test_local.value == 123 394 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 3.11.0 (2025-11-19) 2 | ------------------- 3 | 4 | * ``sync_to_async`` gains a ``context`` parameter, similar to those for 5 | ``asyncio.create_task``, ``TaskGroup`` &co, that can be used on Python 3.11+ to 6 | control the context used by the underlying task. 7 | 8 | The parent context is already propagated by default but the additional 9 | control is useful if multiple ``sync_to_async`` calls need to share the same 10 | context, e.g. when used with ``asyncio.gather()``. 11 | 12 | 3.10.0 (2025-10-05) 13 | ------------------- 14 | 15 | * Added AsyncSingleThreadContext context manager to ensure multiple AsyncToSync 16 | invocations use the same thread. (#511) 17 | 18 | 3.9.2 (2025-09-23) 19 | ------------------ 20 | 21 | * Adds support for Python 3.14. 22 | 23 | * Fixes wsgi.errors file descriptor in WsgiToAsgi adapter. 24 | 25 | 3.9.1 (2025-07-08) 26 | ------------------ 27 | 28 | * Fixed deletion of Local values affecting other contexts. (#523) 29 | 30 | * Skip CPython specific garbage collection test on pypy. (#521) 31 | 32 | 3.9.0 (2025-07-03) 33 | ------------------ 34 | 35 | * Adds support for Python 3.13. 36 | 37 | * Drops support for (end-of-life) Python 3.8. 38 | 39 | * Fixes an error with conflicting kwargs between AsyncToSync and the wrapped 40 | function. (#471) 41 | 42 | * Fixes Local isolation between asyncio Tasks. (#478) 43 | 44 | * Fixes a reference cycle in Local (#508) 45 | 46 | * Fixes a deadlock in CurrentThreadExecutor with nested async_to_sync → 47 | sync_to_async → async_to_sync → create_task calls. (#494) 48 | 49 | * The ApplicationCommunicator testing utility will now return the task result 50 | if it's already completed on send_input and receive_nothing. You may need to 51 | catch (e.g.) the asyncio.exceptions.CancelledError if sending messages to 52 | already finished consumers in your tests. (#505) 53 | 54 | 3.8.1 (2024-03-22) 55 | ------------------ 56 | 57 | * Fixes a regression in 3.8.0 affecting nested task cancellation inside 58 | sync_to_async. 59 | 60 | 3.8.0 (2024-03-20) 61 | ------------------ 62 | 63 | * Adds support for Python 3.12. 64 | 65 | * Drops support for (end-of-life) Python 3.7. 66 | 67 | * Fixes task cancellation propagation to subtasks when using synchronous Django 68 | middleware. 69 | 70 | * Allows nesting ``sync_to_async`` via ``asyncio.wait_for``. 71 | 72 | * Corrects WSGI adapter handling of root path. 73 | 74 | * Handles case where `"client"` is ``None`` in WsgiToAsgi adapter. 75 | 76 | 3.7.2 (2023-05-27) 77 | ------------------ 78 | 79 | * The type annotations for SyncToAsync and AsyncToSync have been changed to 80 | more accurately reflect the kind of callables they return. 81 | 82 | 3.7.1 (2023-05-24) 83 | ------------------ 84 | 85 | * On Python 3.10 and below, the version of the "typing_extensions" package 86 | is now constrained to be at least version 4 (as we depend on functionality 87 | in that version and above) 88 | 89 | 3.7.0 (2023-05-23) 90 | ------------------ 91 | 92 | * Contextvars are now required for the implementation of `sync` as Python 3.6 93 | is now no longer a supported version. 94 | 95 | * sync_to_async and async_to_sync now pass-through 96 | 97 | * Debug and Lifespan State extensions have resulted in a typing change for some 98 | request and response types. This change should be backwards-compatible. 99 | 100 | * ``asgiref`` frames will now be hidden in Django tracebacks by default. 101 | 102 | * Raw performance and garbage collection improvements in Local, SyncToAsync, 103 | and AsyncToSync. 104 | 105 | 3.6.0 (2022-12-20) 106 | ------------------ 107 | 108 | * Two new functions are added to the ``asgiref.sync`` module: ``iscoroutinefunction()`` 109 | and ``markcoroutinefunction()``. 110 | 111 | Python 3.12 deprecates ``asyncio.iscoroutinefunction()`` as an alias for 112 | ``inspect.iscoroutinefunction()``, whilst also removing the ``_is_coroutine`` marker. 113 | The latter is replaced with the ``inspect.markcoroutinefunction`` decorator. 114 | 115 | The new ``asgiref.sync`` functions are compatibility shims for these 116 | functions that can be used until Python 3.12 is the minimum supported 117 | version. 118 | 119 | **Note** that these functions are considered **beta**, and as such, whilst 120 | not likely, are subject to change in a point release, until the final release 121 | of Python 3.12. They are included in ``asgiref`` now so that they can be 122 | adopted by Django 4.2, in preparation for support of Python 3.12. 123 | 124 | * The ``loop`` argument to ``asgiref.timeout.timeout`` is deprecated. As per other 125 | ``asyncio`` based APIs, the running event loop is used by default. Note that 126 | ``asyncio`` provides timeout utilities from Python 3.11, and these should be 127 | preferred where available. 128 | 129 | * Support for the ``ASGI_THREADS`` environment variable, used by 130 | ``SyncToAsync``, is removed. In general, a running event-loop is not 131 | available to `asgiref` at import time, and so the default thread pool 132 | executor cannot be configured. Protocol servers, or applications, should set 133 | the default executor as required when configuring the event loop at 134 | application startup. 135 | 136 | 3.5.2 (2022-05-16) 137 | ------------------ 138 | 139 | * Allow async-callables class instances to be passed to AsyncToSync 140 | without warning 141 | 142 | * Prevent giving async-callable class instances to SyncToAsync 143 | 144 | 145 | 3.5.1 (2022-04-30) 146 | ------------------ 147 | 148 | * sync_to_async in thread-sensitive mode now works corectly when the 149 | outermost thread is synchronous (#214) 150 | 151 | 152 | 3.5.0 (2022-01-22) 153 | ------------------ 154 | 155 | * Python 3.6 is no longer supported, and asyncio calls have been changed to 156 | use only the modern versions of the APIs as a result 157 | 158 | * Several causes of RuntimeErrors in cases where an event loop was assigned 159 | to a thread but not running 160 | 161 | * Speed improvements in the Local class 162 | 163 | 164 | 3.4.1 (2021-07-01) 165 | ------------------ 166 | 167 | * Fixed an issue with the deadlock detection where it had false positives 168 | during exception handling. 169 | 170 | 171 | 3.4.0 (2021-06-27) 172 | ------------------ 173 | 174 | * Calling sync_to_async directly from inside itself (which causes a deadlock 175 | when in the default, thread-sensitive mode) now has deadlock detection. 176 | 177 | * asyncio usage has been updated to use the new versions of get_event_loop, 178 | ensure_future, wait and gather, avoiding deprecation warnings in Python 3.10. 179 | Python 3.6 installs continue to use the old versions; this is only for 3.7+ 180 | 181 | * sync_to_async and async_to_sync now have improved type hints that pass 182 | through the underlying function type correctly. 183 | 184 | * All Websocket* types are now spelled WebSocket, to match our specs and the 185 | official spelling. The old names will work until release 3.5.0, but will 186 | raise deprecation warnings. 187 | 188 | * The typing for WebSocketScope and HTTPScope's `extensions` key has been 189 | fixed. 190 | 191 | 192 | 3.3.4 (2021-04-06) 193 | ------------------ 194 | 195 | * The async_to_sync type error is now a warning due the high false negative 196 | rate when trying to detect coroutine-returning callables in Python. 197 | 198 | 199 | 3.3.3 (2021-04-06) 200 | ------------------ 201 | 202 | * The sync conversion functions now correctly detect functools.partial and other 203 | wrappers around async functions on earlier Python releases. 204 | 205 | 206 | 3.3.2 (2021-04-05) 207 | ------------------ 208 | 209 | * SyncToAsync now takes an optional "executor" argument if you want to supply 210 | your own executor rather than using the built-in one. 211 | 212 | * async_to_sync and sync_to_async now check their arguments are functions of 213 | the correct type. 214 | 215 | * Raising CancelledError inside a SyncToAsync function no longer stops a future 216 | call from functioning. 217 | 218 | * ThreadSensitive now provides context hooks/override options so it can be 219 | made to be sensitive in a unit smaller than threads (e.g. per request) 220 | 221 | * Drop Python 3.5 support. 222 | 223 | * Add type annotations. 224 | 225 | 3.3.1 (2020-11-09) 226 | ------------------ 227 | 228 | * Updated StatelessServer to use ASGI v3 single-callable applications. 229 | 230 | 231 | 3.3.0 (2020-10-09) 232 | ------------------ 233 | 234 | * sync_to_async now defaults to thread-sensitive mode being on 235 | * async_to_sync now works inside of forked processes 236 | * WsgiToAsgi now correctly clamps its response body when Content-Length is set 237 | 238 | 239 | 3.2.10 (2020-08-18) 240 | ------------------- 241 | 242 | * Fixed bugs due to bad WeakRef handling introduced in 3.2.8 243 | 244 | 245 | 3.2.9 (2020-06-16) 246 | ------------------ 247 | 248 | * Fixed regression with exception handling in 3.2.8 related to the contextvars fix. 249 | 250 | 251 | 3.2.8 (2020-06-15) 252 | ------------------ 253 | 254 | * Fixed small memory leak in local.Local 255 | * contextvars are now persisted through AsyncToSync 256 | 257 | 258 | 3.2.7 (2020-03-24) 259 | ------------------ 260 | 261 | * Bug fixed in local.Local where deleted Locals would occasionally inherit 262 | their storage into new Locals due to memory reuse. 263 | 264 | 265 | 3.2.6 (2020-03-23) 266 | ------------------ 267 | 268 | * local.Local now works in all threading situations, no longer requires 269 | periodic garbage collection, and works with libraries that monkeypatch 270 | threading (like gevent) 271 | 272 | 273 | 3.2.5 (2020-03-11) 274 | ------------------ 275 | 276 | * __self__ is now preserved on methods by async_to_sync 277 | 278 | 279 | 3.2.4 (2020-03-10) 280 | ------------------ 281 | 282 | * Pending tasks/async generators are now cancelled when async_to_sync exits 283 | * Contextvars now propagate changes both ways through sync_to_async 284 | * sync_to_async now preserves attributes on functions it wraps 285 | 286 | 287 | 3.2.3 (2019-10-23) 288 | ------------------ 289 | 290 | * Added support and testing for Python 3.8. 291 | 292 | 293 | 3.2.2 (2019-08-29) 294 | ------------------ 295 | 296 | * WsgiToAsgi maps multi-part request bodies into a single WSGI input file 297 | * WsgiToAsgi passes the `root_path` scope as SCRIPT_NAME 298 | * WsgiToAsgi now checks the scope type to handle `lifespan` better 299 | * WsgiToAsgi now passes the server port as a string, like WSGI 300 | * SyncToAsync values are now identified as coroutine functions by asyncio 301 | * SyncToAsync now handles __self__ correctly for methods 302 | 303 | 304 | 3.2.1 (2019-08-05) 305 | ------------------ 306 | 307 | * sys.exc_info() is now propagated across thread boundaries 308 | 309 | 310 | 3.2.0 (2019-07-29) 311 | ------------------ 312 | 313 | * New "thread_sensitive" argument to SyncToAsync allows for pinning of code into 314 | the same thread as other thread_sensitive code. 315 | * Test collection on Python 3.7 fixed 316 | 317 | 3.1.4 (2019-07-07) 318 | ------------------ 319 | 320 | * Fixed an incompatibility with Python 3.5 introduced in the last release. 321 | 322 | 323 | 3.1.3 (2019-07-05) 324 | ------------------ 325 | 326 | * async_timeout has been removed as a dependency, so there are now no required 327 | dependencies. 328 | * The WSGI adapter now sets ``REMOTE_ADDR`` from the ASGI ``client``. 329 | 330 | 331 | 3.1.2 (2019-04-17) 332 | ------------------ 333 | 334 | * New thread_critical argument to Local to tell it to not inherit contexts 335 | across threads/tasks. 336 | * Local now inherits across any number of sync_to_async to async_to_sync calls 337 | nested inside each other 338 | 339 | 340 | 3.1.1 (2019-04-13) 341 | ------------------ 342 | 343 | * Local now cleans up storage of old threads and tasks to prevent a memory leak. 344 | 345 | 346 | 3.1.0 (2019-04-13) 347 | ------------------ 348 | 349 | * Added ``asgiref.local`` module to provide threading.local drop-in replacement. 350 | 351 | 352 | 3.0.0 (2019-03-20) 353 | ------------------ 354 | 355 | * Updated to match new ASGI 3.0 spec 356 | * Compatibility library added that allows adapting ASGI 2 apps into ASGI 3 apps 357 | losslessly 358 | 359 | 360 | 2.3.2 (2018-05-23) 361 | ------------------ 362 | 363 | * Packaging fix to allow old async_timeout dependencies (2.0 as well as 3.0) 364 | 365 | 366 | 2.3.1 (2018-05-23) 367 | ------------------ 368 | 369 | * WSGI-to-ASGI adapter now works with empty bodies in responses 370 | * Update async-timeout dependency 371 | 372 | 373 | 2.3.0 (2018-04-11) 374 | ------------------ 375 | 376 | * ApplicationCommunicator now has a receive_nothing() test available 377 | 378 | 379 | 2.2.0 (2018-03-06) 380 | ------------------ 381 | 382 | * Cancelled tasks now correctly cascade-cancel their children 383 | 384 | * Communicator.wait() no longer re-raises CancelledError from inner coroutines 385 | 386 | 387 | 2.1.6 (2018-02-19) 388 | ------------------ 389 | 390 | * async_to_sync now works inside of threads (but is still not allowed in threads 391 | that have an active event loop) 392 | 393 | 394 | 2.1.5 (2018-02-14) 395 | ------------------ 396 | 397 | * Fixed issues with async_to_sync not setting the event loop correctly 398 | 399 | * Stop async_to_sync being called from threads with an active event loop 400 | 401 | 402 | 2.1.4 (2018-02-07) 403 | ------------------ 404 | 405 | * Values are now correctly returned from sync_to_async and async_to_sync 406 | 407 | * ASGI_THREADS environment variable now works correctly 408 | 409 | 410 | 2.1.3 (2018-02-04) 411 | ------------------ 412 | 413 | * Add an ApplicationCommunicator.wait() method to allow you to wait for an 414 | application instance to exit before seeing what it did. 415 | 416 | 417 | 2.1.2 (2018-02-03) 418 | ------------------ 419 | 420 | * Allow AsyncToSync to work if called from a non-async-wrapped sync context. 421 | 422 | 423 | 2.1.1 (2018-02-02) 424 | ------------------ 425 | 426 | * Allow AsyncToSync constructor to be called inside SyncToAsync. 427 | 428 | 429 | 2.1.0 (2018-01-19) 430 | ------------------ 431 | 432 | * Add `asgiref.testing` module with ApplicationCommunicator testing helper 433 | 434 | 435 | 2.0.1 (2017-11-28) 436 | ------------------ 437 | 438 | * Bugfix release to have HTTP response content message as the correct 439 | "http.response.content" not the older "http.response.chunk". 440 | 441 | 442 | 2.0.0 (2017-11-28) 443 | ------------------ 444 | 445 | * Complete rewrite for new async-based ASGI mechanisms and removal of 446 | channel layers. 447 | 448 | 449 | 1.1.2 (2017-05-16) 450 | ----------------- 451 | 452 | * Conformance test suite now allows for retries and tests group_send's behaviour with capacity 453 | * valid_channel_names now has a receive parameter 454 | 455 | 456 | 1.1.1 (2017-04-02) 457 | ------------------ 458 | 459 | * Error with sending to multi-process channels with the same message fixed 460 | 461 | 462 | 1.1.0 (2017-04-01) 463 | ------------------ 464 | 465 | * Process-specific channel behaviour has been changed, and the base layer 466 | and conformance suites updated to match. 467 | 468 | 469 | 1.0.1 (2017-03-19) 470 | ------------------ 471 | 472 | * Improved channel and group name validation 473 | * Test rearrangements and improvements 474 | 475 | 476 | 1.0.0 (2016-04-11) 477 | ------------------ 478 | 479 | * `receive_many` is now `receive` 480 | * In-memory layer deepcopies messages so they cannot be mutated post-send 481 | * Better errors for bad channel/group names 482 | -------------------------------------------------------------------------------- /specs/asgi.rst: -------------------------------------------------------------------------------- 1 | ========================================================== 2 | ASGI (Asynchronous Server Gateway Interface) Specification 3 | ========================================================== 4 | 5 | **Version**: 3.0 (2019-03-20) 6 | 7 | Abstract 8 | ======== 9 | 10 | This document proposes a standard interface between network protocol 11 | servers (particularly web servers) and Python applications, intended 12 | to allow handling of multiple common protocol styles (including HTTP, HTTP/2, 13 | and WebSocket). 14 | 15 | This base specification is intended to fix in place the set of APIs by which 16 | these servers interact and run application code; 17 | each supported protocol (such as HTTP) has a sub-specification that outlines 18 | how to encode and decode that protocol into messages. 19 | 20 | 21 | Rationale 22 | ========= 23 | 24 | The WSGI specification has worked well since it was introduced, and 25 | allowed for great flexibility in Python framework and web server choice. 26 | However, its design is irrevocably tied to the HTTP-style 27 | request/response cycle, and more and more protocols that do not follow this 28 | pattern are becoming a standard part of web programming (most notably, 29 | WebSocket). 30 | 31 | ASGI attempts to preserve a simple application interface, while providing an 32 | abstraction that allows for data to be sent and received at any time, and from 33 | different application threads or processes. 34 | 35 | It also takes the principle of turning protocols into Python-compatible, 36 | asynchronous-friendly sets of messages and generalises it into two parts; 37 | a standardised interface for communication around which to build servers (this 38 | document), and a set of standard message formats for each protocol. 39 | 40 | Its primary goal is to provide a way to write HTTP/2 and WebSocket code 41 | alongside normal HTTP handling code, however; part of this design means 42 | ensuring there is an easy path to use both existing WSGI servers and 43 | applications, as a large majority of Python web usage relies on WSGI and 44 | providing an easy path forward is critical to adoption. Details on that 45 | interoperability are covered in the ASGI-HTTP spec. 46 | 47 | 48 | Overview 49 | ======== 50 | 51 | ASGI consists of two different components: 52 | 53 | - A *protocol server*, which terminates sockets and translates them into 54 | connections and per-connection event messages. 55 | 56 | - An *application*, which lives inside a *protocol server*, is called once 57 | per connection, and handles event messages as they happen, emitting its own 58 | event messages back when necessary. 59 | 60 | Like WSGI, the server hosts the application inside it, and dispatches incoming 61 | requests to it in a standardized format. Unlike WSGI, however, applications 62 | are asynchronous callables rather than simple callables, and they communicate with 63 | the server by receiving and sending asynchronous event messages rather than receiving 64 | a single input stream and returning a single iterable. ASGI applications must run as 65 | ``async`` / ``await`` compatible coroutines (i.e. ``asyncio``-compatible) (on the main thread; 66 | they are free to use threading or other processes if they need synchronous 67 | code). 68 | 69 | Unlike WSGI, there are two separate parts to an ASGI connection: 70 | 71 | - A *connection scope*, which represents a protocol connection to a user and 72 | survives until the connection closes. 73 | 74 | - *Events*, which are messages sent to the application as things happen on the 75 | connection, and messages sent back by the application to be received by the server, 76 | including data to be transmitted to the client. 77 | 78 | Applications are called and awaited with a connection ``scope`` and two awaitable 79 | callables to ``receive`` event messages and ``send`` event messages back. All this 80 | happening in an asynchronous event loop. 81 | 82 | Each call of the application callable maps to a single incoming "socket" or 83 | connection, and is expected to last the lifetime of that connection plus a little 84 | longer if there is cleanup to do. Some protocols may not use traditional sockets; ASGI 85 | specifications for those protocols are expected to define what the scope lifetime is 86 | and when it gets shut down. 87 | 88 | 89 | Specification Details 90 | ===================== 91 | 92 | Connection Scope 93 | ---------------- 94 | 95 | Every connection by a user to an ASGI application results in a call of the 96 | application callable to handle that connection entirely. How long this lives, 97 | and the information that describes each specific connection, is called the 98 | *connection scope*. 99 | 100 | Closely related, the first argument passed to an application callable is a 101 | ``scope`` dictionary with all the information describing that specific connection. 102 | 103 | For example, under HTTP the connection scope lasts just one request, but the ``scope`` 104 | passed contains most of the request data (apart from the HTTP request body, as this 105 | is streamed in via events). 106 | 107 | Under WebSocket, though, the connection scope lasts for as long as the socket 108 | is connected. And the ``scope`` passed contains information like the WebSocket's path, but 109 | details like incoming messages come through as events instead. 110 | 111 | Some protocols may give you a ``scope`` with very limited information up 112 | front because they encapsulate something like a handshake. Each protocol 113 | definition must contain information about how long its connection scope lasts, 114 | and what information you will get in the ``scope`` parameter. 115 | 116 | Depending on the protocol spec, applications may have to wait for an initial 117 | opening message before communicating with the client. 118 | 119 | 120 | Events 121 | ------ 122 | 123 | ASGI decomposes protocols into a series of *events* that an application must 124 | *receive* and react to, and *events* the application might *send* in response. 125 | For HTTP, this is as simple as *receiving* two events in order - ``http.request`` 126 | and ``http.disconnect``, and *sending* the corresponding event messages back. For 127 | something like a WebSocket, it could be more like *receiving* ``websocket.connect``, 128 | *sending* a ``websocket.send``, *receiving* a ``websocket.receive``, and finally 129 | *receiving* a ``websocket.disconnect``. 130 | 131 | Each event is a ``dict`` with a top-level ``type`` key that contains a 132 | Unicode string of the message type. Users are free to invent their own message 133 | types and send them between application instances for high-level events - for 134 | example, a chat application might send chat messages with a user type of 135 | ``mychat.message``. It is expected that applications should be able to handle 136 | a mixed set of events, some sourced from the incoming client connection and 137 | some from other parts of the application. 138 | 139 | Because these messages could be sent over a network, they need to be 140 | serializable, and so they are only allowed to contain the following types: 141 | 142 | * Byte strings 143 | * Unicode strings 144 | * Integers (within the signed 64-bit range) 145 | * Floating point numbers (within the IEEE 754 double precision range; no 146 | ``Nan`` or infinities) 147 | * Lists (tuples should be encoded as lists) 148 | * Dicts (keys must be Unicode strings) 149 | * Booleans 150 | * ``None`` 151 | 152 | 153 | Applications 154 | ------------ 155 | 156 | .. note:: 157 | 158 | The application format changed in 3.0 to use a single callable, rather than 159 | the prior two-callable format. Two-callable is documented below in 160 | "Legacy Applications"; servers can easily implement support for it using 161 | the ``asgiref.compatibility`` library, and should try to support it. 162 | 163 | ASGI applications should be a single async callable:: 164 | 165 | coroutine application(scope, receive, send) 166 | 167 | * ``scope``: The connection scope information, a dictionary that contains at least a 168 | ``type`` key specifying the protocol that is incoming 169 | * ``receive``: an awaitable callable that will yield a new event dictionary 170 | when one is available 171 | * ``send``: an awaitable callable taking a single event dictionary as a 172 | positional argument that will return once the send has been 173 | completed or the connection has been closed 174 | 175 | The application is called once per "connection". The definition of a connection 176 | and its lifespan are dictated by the protocol specification in question. For 177 | example, with HTTP it is one request, whereas for a WebSocket it is a single 178 | WebSocket connection. 179 | 180 | Both the ``scope`` and the format of the event messages you send and receive 181 | are defined by one of the application protocols. ``scope`` must be a 182 | ``dict``. The key ``scope["type"]`` will always be present, and can 183 | be used to work out which protocol is incoming. The key 184 | ``scope["asgi"]`` will also be present as a dictionary containing a 185 | ``scope["asgi"]["version"]`` key that corresponds to the ASGI version 186 | the server implements. If missing, the version should default to ``"2.0"``. 187 | 188 | There may also be a spec-specific version present as 189 | ``scope["asgi"]["spec_version"]``. This allows the individual protocol 190 | specifications to make enhancements without bumping the overall ASGI version. 191 | 192 | The protocol-specific sub-specifications cover these scope and event message formats. 193 | They are equivalent to the specification for keys in the ``environ`` dict for 194 | WSGI. 195 | 196 | 197 | Legacy Applications 198 | ------------------- 199 | 200 | Legacy (v2.0) ASGI applications are defined as a callable:: 201 | 202 | application(scope) 203 | 204 | Which returns another, awaitable callable:: 205 | 206 | coroutine application_instance(receive, send) 207 | 208 | The meanings of ``scope``, ``receive`` and ``send`` are the same as in the 209 | newer single-callable application, but note that the first callable is 210 | *synchronous*. 211 | 212 | The first callable is called when the connection is started, and then the 213 | second callable is called and awaited immediately afterwards. 214 | 215 | This style was retired in version 3.0 as the two-callable layout was deemed 216 | unnecessary. It's now legacy, but there are applications out there written in 217 | this style, and so it's important to support them. 218 | 219 | There is a compatibility suite available in the ``asgiref.compatibility`` 220 | module which allows you to both detect legacy applications and convert them 221 | to the new single-protocol style seamlessly. Servers are encouraged to support 222 | both types as of ASGI 3.0 and gradually drop support by default over time. 223 | 224 | 225 | Protocol Specifications 226 | ----------------------- 227 | 228 | These describe the standardized scope and message formats for various 229 | protocols. 230 | 231 | The one common key across all scopes and messages is ``type``, a way to 232 | indicate what type of scope or event message is being received. 233 | 234 | In scopes, the ``type`` key must be a Unicode string, like ``"http"`` or 235 | ``"websocket"``, as defined in the relevant protocol specification. 236 | 237 | In messages, the ``type`` should be namespaced as ``protocol.message_type``, 238 | where the ``protocol`` matches the scope type, and ``message_type`` is 239 | defined by the protocol spec. Examples of a message ``type`` value include 240 | ``http.request`` and ``websocket.send``. 241 | 242 | .. note:: 243 | 244 | Applications should actively reject any protocol that they do not understand 245 | with an `Exception` (of any type). Failure to do this may result in the 246 | server thinking you support a protocol you don't, which can be confusing when 247 | using with the Lifespan protocol, as the server will wait to start until you 248 | tell it. 249 | 250 | 251 | Current protocol specifications: 252 | 253 | * :doc:`HTTP and WebSocket ` 254 | * :doc:`Lifespan ` 255 | 256 | 257 | Middleware 258 | ---------- 259 | 260 | It is possible to have ASGI "middleware" - code that plays the role of both 261 | server and application, taking in a ``scope`` and the ``send``/``receive`` awaitable callables, 262 | potentially modifying them, and then calling an inner application. 263 | 264 | When middleware is modifying the ``scope``, it should make a copy of the ``scope`` 265 | object before mutating it and passing it to the inner application, as changes 266 | may leak upstream otherwise. In particular, you should not assume that the copy 267 | of the ``scope`` you pass down to the application is the one that it ends up using, 268 | as there may be other middleware in the way; thus, do not keep a reference to 269 | it and try to mutate it outside of the initial ASGI app call. Your one and only 270 | chance to add to it is before you hand control to the child application. 271 | 272 | 273 | Error Handling 274 | -------------- 275 | 276 | If a server receives an invalid event dictionary - for example, having an 277 | unknown ``type``, missing keys an event type should have, or with wrong Python 278 | types for objects (e.g. Unicode strings for HTTP headers) - it should raise an 279 | exception out of the ``send`` awaitable back to the application. 280 | 281 | If an application receives an invalid event dictionary from ``receive``, it 282 | should raise an exception. 283 | 284 | In both cases, the presence of additional keys in the event dictionary should 285 | not raise an exception. This allows non-breaking upgrades to protocol 286 | specifications over time. 287 | 288 | Servers are free to surface errors that bubble up out of application instances 289 | they are running however they wish - log to console, send to syslog, or other 290 | options - but they must terminate the application instance and its associated 291 | connection if this happens. 292 | 293 | Note that messages received by a server after the connection has been 294 | closed are generally not considered errors unless specified by a protocol. 295 | If no error condition is specified, the ``send`` awaitable callable should act 296 | as a no-op. 297 | 298 | Even if an error is raised on ``send()``, it should be an error 299 | class that the server catches and ignores if it is raised out of the 300 | application, ensuring that the server does not itself error in the process. 301 | 302 | Extensions 303 | ---------- 304 | 305 | There are times when protocol servers may want to provide server-specific 306 | extensions outside of a core ASGI protocol specification, or when a change 307 | to a specification is being trialled before being rolled in. 308 | 309 | For this use case, we define a common pattern for ``extensions`` - named 310 | additions to a protocol specification that are optional but that, if provided 311 | by the server and understood by the application, can be used to get more 312 | functionality. 313 | 314 | This is achieved via an ``extensions`` entry in the ``scope`` dictionary, which 315 | is itself a ``dict``. Extensions have a Unicode string name that 316 | is agreed upon between servers and applications. 317 | 318 | If the server supports an extension, it should place an entry into the 319 | ``extensions`` dictionary under the extension's name, and the value of that 320 | entry should itself be a ``dict``. Servers can provide any extra scope 321 | information that is part of the extension inside this value or, if the 322 | extension is only to indicate that the server accepts additional events via 323 | the ``send`` callable, it may just be an empty ``dict``. 324 | 325 | As an example, imagine a HTTP protocol server wishes to provide an extension 326 | that allows a new event to be sent back to the server that tries to flush the 327 | network send buffer all the way through the OS level. It provides an empty 328 | entry in the ``extensions`` dictionary to signal that it can handle the event:: 329 | 330 | scope = { 331 | "type": "http", 332 | "method": "GET", 333 | ... 334 | "extensions": { 335 | "fullflush": {}, 336 | }, 337 | } 338 | 339 | If an application sees this it then knows it can send the custom event 340 | (say, of type ``http.fullflush``) via the ``send`` callable. 341 | 342 | 343 | Strings and Unicode 344 | ------------------- 345 | 346 | In this document, and all sub-specifications, *byte string* refers to 347 | the ``bytes`` type in Python 3. *Unicode string* refers to the ``str`` type 348 | in Python 3. 349 | 350 | This document will never specify just *string* - all strings are one of the 351 | two exact types. 352 | 353 | All ``dict`` keys mentioned (including those for *scopes* and *events*) are 354 | Unicode strings. 355 | 356 | 357 | Version History 358 | =============== 359 | 360 | * 3.0 (2019-03-04): Changed to single-callable application style 361 | * 2.0 (2017-11-28): Initial non-channel-layer based ASGI spec 362 | 363 | 364 | Copyright 365 | ========= 366 | 367 | This document has been placed in the public domain. 368 | -------------------------------------------------------------------------------- /asgiref/sync.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import asyncio.coroutines 3 | import contextvars 4 | import functools 5 | import inspect 6 | import os 7 | import sys 8 | import threading 9 | import warnings 10 | import weakref 11 | from concurrent.futures import Future, ThreadPoolExecutor 12 | from typing import ( 13 | TYPE_CHECKING, 14 | Any, 15 | Awaitable, 16 | Callable, 17 | Coroutine, 18 | Dict, 19 | Generic, 20 | List, 21 | Optional, 22 | TypeVar, 23 | Union, 24 | overload, 25 | ) 26 | 27 | from .current_thread_executor import CurrentThreadExecutor 28 | from .local import Local 29 | 30 | if sys.version_info >= (3, 10): 31 | from typing import ParamSpec 32 | else: 33 | from typing_extensions import ParamSpec 34 | 35 | if TYPE_CHECKING: 36 | # This is not available to import at runtime 37 | from _typeshed import OptExcInfo 38 | 39 | _F = TypeVar("_F", bound=Callable[..., Any]) 40 | _P = ParamSpec("_P") 41 | _R = TypeVar("_R") 42 | 43 | 44 | def _restore_context(context: contextvars.Context) -> None: 45 | # Check for changes in contextvars, and set them to the current 46 | # context for downstream consumers 47 | for cvar in context: 48 | cvalue = context.get(cvar) 49 | try: 50 | if cvar.get() != cvalue: 51 | cvar.set(cvalue) 52 | except LookupError: 53 | cvar.set(cvalue) 54 | 55 | 56 | # Python 3.12 deprecates asyncio.iscoroutinefunction() as an alias for 57 | # inspect.iscoroutinefunction(), whilst also removing the _is_coroutine marker. 58 | # The latter is replaced with the inspect.markcoroutinefunction decorator. 59 | # Until 3.12 is the minimum supported Python version, provide a shim. 60 | 61 | if hasattr(inspect, "markcoroutinefunction"): 62 | iscoroutinefunction = inspect.iscoroutinefunction 63 | markcoroutinefunction: Callable[[_F], _F] = inspect.markcoroutinefunction 64 | else: 65 | iscoroutinefunction = asyncio.iscoroutinefunction # type: ignore[assignment] 66 | 67 | def markcoroutinefunction(func: _F) -> _F: 68 | func._is_coroutine = asyncio.coroutines._is_coroutine # type: ignore 69 | return func 70 | 71 | 72 | class AsyncSingleThreadContext: 73 | """Context manager to run async code inside the same thread. 74 | 75 | Normally, AsyncToSync functions run either inside a separate ThreadPoolExecutor or 76 | the main event loop if it exists. This context manager ensures that all AsyncToSync 77 | functions execute within the same thread. 78 | 79 | This context manager is re-entrant, so only the outer-most call to 80 | AsyncSingleThreadContext will set the context. 81 | 82 | Usage: 83 | 84 | >>> import asyncio 85 | >>> with AsyncSingleThreadContext(): 86 | ... async_to_sync(asyncio.sleep(1))() 87 | """ 88 | 89 | def __init__(self): 90 | self.token = None 91 | 92 | def __enter__(self): 93 | try: 94 | AsyncToSync.async_single_thread_context.get() 95 | except LookupError: 96 | self.token = AsyncToSync.async_single_thread_context.set(self) 97 | 98 | return self 99 | 100 | def __exit__(self, exc, value, tb): 101 | if not self.token: 102 | return 103 | 104 | executor = AsyncToSync.context_to_thread_executor.pop(self, None) 105 | if executor: 106 | executor.shutdown() 107 | 108 | AsyncToSync.async_single_thread_context.reset(self.token) 109 | 110 | 111 | class ThreadSensitiveContext: 112 | """Async context manager to manage context for thread sensitive mode 113 | 114 | This context manager controls which thread pool executor is used when in 115 | thread sensitive mode. By default, a single thread pool executor is shared 116 | within a process. 117 | 118 | The ThreadSensitiveContext() context manager may be used to specify a 119 | thread pool per context. 120 | 121 | This context manager is re-entrant, so only the outer-most call to 122 | ThreadSensitiveContext will set the context. 123 | 124 | Usage: 125 | 126 | >>> import time 127 | >>> async with ThreadSensitiveContext(): 128 | ... await sync_to_async(time.sleep, 1)() 129 | """ 130 | 131 | def __init__(self): 132 | self.token = None 133 | 134 | async def __aenter__(self): 135 | try: 136 | SyncToAsync.thread_sensitive_context.get() 137 | except LookupError: 138 | self.token = SyncToAsync.thread_sensitive_context.set(self) 139 | 140 | return self 141 | 142 | async def __aexit__(self, exc, value, tb): 143 | if not self.token: 144 | return 145 | 146 | executor = SyncToAsync.context_to_thread_executor.pop(self, None) 147 | if executor: 148 | executor.shutdown() 149 | SyncToAsync.thread_sensitive_context.reset(self.token) 150 | 151 | 152 | class AsyncToSync(Generic[_P, _R]): 153 | """ 154 | Utility class which turns an awaitable that only works on the thread with 155 | the event loop into a synchronous callable that works in a subthread. 156 | 157 | If the call stack contains an async loop, the code runs there. 158 | Otherwise, the code runs in a new loop in a new thread. 159 | 160 | Either way, this thread then pauses and waits to run any thread_sensitive 161 | code called from further down the call stack using SyncToAsync, before 162 | finally exiting once the async task returns. 163 | """ 164 | 165 | # Keeps a reference to the CurrentThreadExecutor in local context, so that 166 | # any sync_to_async inside the wrapped code can find it. 167 | executors: "Local" = Local() 168 | 169 | # When we can't find a CurrentThreadExecutor from the context, such as 170 | # inside create_task, we'll look it up here from the running event loop. 171 | loop_thread_executors: "Dict[asyncio.AbstractEventLoop, CurrentThreadExecutor]" = {} 172 | 173 | async_single_thread_context: "contextvars.ContextVar[AsyncSingleThreadContext]" = ( 174 | contextvars.ContextVar("async_single_thread_context") 175 | ) 176 | 177 | context_to_thread_executor: "weakref.WeakKeyDictionary[AsyncSingleThreadContext, ThreadPoolExecutor]" = ( 178 | weakref.WeakKeyDictionary() 179 | ) 180 | 181 | def __init__( 182 | self, 183 | awaitable: Union[ 184 | Callable[_P, Coroutine[Any, Any, _R]], 185 | Callable[_P, Awaitable[_R]], 186 | ], 187 | force_new_loop: bool = False, 188 | ): 189 | if not callable(awaitable) or ( 190 | not iscoroutinefunction(awaitable) 191 | and not iscoroutinefunction(getattr(awaitable, "__call__", awaitable)) 192 | ): 193 | # Python does not have very reliable detection of async functions 194 | # (lots of false negatives) so this is just a warning. 195 | warnings.warn( 196 | "async_to_sync was passed a non-async-marked callable", stacklevel=2 197 | ) 198 | self.awaitable = awaitable 199 | try: 200 | self.__self__ = self.awaitable.__self__ # type: ignore[union-attr] 201 | except AttributeError: 202 | pass 203 | self.force_new_loop = force_new_loop 204 | self.main_event_loop = None 205 | try: 206 | self.main_event_loop = asyncio.get_running_loop() 207 | except RuntimeError: 208 | # There's no event loop in this thread. 209 | pass 210 | 211 | def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: 212 | __traceback_hide__ = True # noqa: F841 213 | 214 | if not self.force_new_loop and not self.main_event_loop: 215 | # There's no event loop in this thread. Look for the threadlocal if 216 | # we're inside SyncToAsync 217 | main_event_loop_pid = getattr( 218 | SyncToAsync.threadlocal, "main_event_loop_pid", None 219 | ) 220 | # We make sure the parent loop is from the same process - if 221 | # they've forked, this is not going to be valid any more (#194) 222 | if main_event_loop_pid and main_event_loop_pid == os.getpid(): 223 | self.main_event_loop = getattr( 224 | SyncToAsync.threadlocal, "main_event_loop", None 225 | ) 226 | 227 | # You can't call AsyncToSync from a thread with a running event loop 228 | try: 229 | asyncio.get_running_loop() 230 | except RuntimeError: 231 | pass 232 | else: 233 | raise RuntimeError( 234 | "You cannot use AsyncToSync in the same thread as an async event loop - " 235 | "just await the async function directly." 236 | ) 237 | 238 | # Make a future for the return information 239 | call_result: "Future[_R]" = Future() 240 | 241 | # Make a CurrentThreadExecutor we'll use to idle in this thread - we 242 | # need one for every sync frame, even if there's one above us in the 243 | # same thread. 244 | old_executor = getattr(self.executors, "current", None) 245 | current_executor = CurrentThreadExecutor(old_executor) 246 | self.executors.current = current_executor 247 | 248 | # Wrapping context in list so it can be reassigned from within 249 | # `main_wrap`. 250 | context = [contextvars.copy_context()] 251 | 252 | # Get task context so that parent task knows which task to propagate 253 | # an asyncio.CancelledError to. 254 | task_context = getattr(SyncToAsync.threadlocal, "task_context", None) 255 | 256 | # Use call_soon_threadsafe to schedule a synchronous callback on the 257 | # main event loop's thread if it's there, otherwise make a new loop 258 | # in this thread. 259 | try: 260 | awaitable = self.main_wrap( 261 | call_result, 262 | sys.exc_info(), 263 | task_context, 264 | context, 265 | # prepare an awaitable which can be passed as is to self.main_wrap, 266 | # so that `args` and `kwargs` don't need to be 267 | # destructured when passed to self.main_wrap 268 | # (which is required by `ParamSpec`) 269 | # as that may cause overlapping arguments 270 | self.awaitable(*args, **kwargs), 271 | ) 272 | 273 | async def new_loop_wrap() -> None: 274 | loop = asyncio.get_running_loop() 275 | self.loop_thread_executors[loop] = current_executor 276 | try: 277 | await awaitable 278 | finally: 279 | del self.loop_thread_executors[loop] 280 | 281 | if self.main_event_loop is not None: 282 | try: 283 | self.main_event_loop.call_soon_threadsafe( 284 | self.main_event_loop.create_task, awaitable 285 | ) 286 | except RuntimeError: 287 | running_in_main_event_loop = False 288 | else: 289 | running_in_main_event_loop = True 290 | # Run the CurrentThreadExecutor until the future is done. 291 | current_executor.run_until_future(call_result) 292 | else: 293 | running_in_main_event_loop = False 294 | 295 | if not running_in_main_event_loop: 296 | loop_executor = None 297 | 298 | if self.async_single_thread_context.get(None): 299 | single_thread_context = self.async_single_thread_context.get() 300 | 301 | if single_thread_context in self.context_to_thread_executor: 302 | loop_executor = self.context_to_thread_executor[ 303 | single_thread_context 304 | ] 305 | else: 306 | loop_executor = ThreadPoolExecutor(max_workers=1) 307 | self.context_to_thread_executor[ 308 | single_thread_context 309 | ] = loop_executor 310 | else: 311 | # Make our own event loop - in a new thread - and run inside that. 312 | loop_executor = ThreadPoolExecutor(max_workers=1) 313 | 314 | loop_future = loop_executor.submit(asyncio.run, new_loop_wrap()) 315 | # Run the CurrentThreadExecutor until the future is done. 316 | current_executor.run_until_future(loop_future) 317 | # Wait for future and/or allow for exception propagation 318 | loop_future.result() 319 | finally: 320 | _restore_context(context[0]) 321 | # Restore old current thread executor state 322 | self.executors.current = old_executor 323 | 324 | # Wait for results from the future. 325 | return call_result.result() 326 | 327 | def __get__(self, parent: Any, objtype: Any) -> Callable[_P, _R]: 328 | """ 329 | Include self for methods 330 | """ 331 | func = functools.partial(self.__call__, parent) 332 | return functools.update_wrapper(func, self.awaitable) 333 | 334 | async def main_wrap( 335 | self, 336 | call_result: "Future[_R]", 337 | exc_info: "OptExcInfo", 338 | task_context: "Optional[List[asyncio.Task[Any]]]", 339 | context: List[contextvars.Context], 340 | awaitable: Union[Coroutine[Any, Any, _R], Awaitable[_R]], 341 | ) -> None: 342 | """ 343 | Wraps the awaitable with something that puts the result into the 344 | result/exception future. 345 | """ 346 | 347 | __traceback_hide__ = True # noqa: F841 348 | 349 | if context is not None: 350 | _restore_context(context[0]) 351 | 352 | current_task = asyncio.current_task() 353 | if current_task is not None and task_context is not None: 354 | task_context.append(current_task) 355 | 356 | try: 357 | # If we have an exception, run the function inside the except block 358 | # after raising it so exc_info is correctly populated. 359 | if exc_info[1]: 360 | try: 361 | raise exc_info[1] 362 | except BaseException: 363 | result = await awaitable 364 | else: 365 | result = await awaitable 366 | except BaseException as e: 367 | call_result.set_exception(e) 368 | else: 369 | call_result.set_result(result) 370 | finally: 371 | if current_task is not None and task_context is not None: 372 | task_context.remove(current_task) 373 | context[0] = contextvars.copy_context() 374 | 375 | 376 | class SyncToAsync(Generic[_P, _R]): 377 | """ 378 | Utility class which turns a synchronous callable into an awaitable that 379 | runs in a threadpool. It also sets a threadlocal inside the thread so 380 | calls to AsyncToSync can escape it. 381 | 382 | If thread_sensitive is passed, the code will run in the same thread as any 383 | outer code. This is needed for underlying Python code that is not 384 | threadsafe (for example, code which handles SQLite database connections). 385 | 386 | If the outermost program is async (i.e. SyncToAsync is outermost), then 387 | this will be a dedicated single sub-thread that all sync code runs in, 388 | one after the other. If the outermost program is sync (i.e. AsyncToSync is 389 | outermost), this will just be the main thread. This is achieved by idling 390 | with a CurrentThreadExecutor while AsyncToSync is blocking its sync parent, 391 | rather than just blocking. 392 | 393 | If executor is passed in, that will be used instead of the loop's default executor. 394 | In order to pass in an executor, thread_sensitive must be set to False, otherwise 395 | a TypeError will be raised. 396 | """ 397 | 398 | # Storage for main event loop references 399 | threadlocal = threading.local() 400 | 401 | # Single-thread executor for thread-sensitive code 402 | single_thread_executor = ThreadPoolExecutor(max_workers=1) 403 | 404 | # Maintain a contextvar for the current execution context. Optionally used 405 | # for thread sensitive mode. 406 | thread_sensitive_context: "contextvars.ContextVar[ThreadSensitiveContext]" = ( 407 | contextvars.ContextVar("thread_sensitive_context") 408 | ) 409 | 410 | # Contextvar that is used to detect if the single thread executor 411 | # would be awaited on while already being used in the same context 412 | deadlock_context: "contextvars.ContextVar[bool]" = contextvars.ContextVar( 413 | "deadlock_context" 414 | ) 415 | 416 | # Maintaining a weak reference to the context ensures that thread pools are 417 | # erased once the context goes out of scope. This terminates the thread pool. 418 | context_to_thread_executor: "weakref.WeakKeyDictionary[ThreadSensitiveContext, ThreadPoolExecutor]" = ( 419 | weakref.WeakKeyDictionary() 420 | ) 421 | 422 | def __init__( 423 | self, 424 | func: Callable[_P, _R], 425 | thread_sensitive: bool = True, 426 | executor: Optional["ThreadPoolExecutor"] = None, 427 | context: Optional[contextvars.Context] = None, 428 | ) -> None: 429 | if ( 430 | not callable(func) 431 | or iscoroutinefunction(func) 432 | or iscoroutinefunction(getattr(func, "__call__", func)) 433 | ): 434 | raise TypeError("sync_to_async can only be applied to sync functions.") 435 | self.func = func 436 | self.context = context 437 | functools.update_wrapper(self, func) 438 | self._thread_sensitive = thread_sensitive 439 | markcoroutinefunction(self) 440 | if thread_sensitive and executor is not None: 441 | raise TypeError("executor must not be set when thread_sensitive is True") 442 | self._executor = executor 443 | try: 444 | self.__self__ = func.__self__ # type: ignore 445 | except AttributeError: 446 | pass 447 | 448 | async def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: 449 | __traceback_hide__ = True # noqa: F841 450 | loop = asyncio.get_running_loop() 451 | 452 | # Work out what thread to run the code in 453 | if self._thread_sensitive: 454 | current_thread_executor = getattr(AsyncToSync.executors, "current", None) 455 | if current_thread_executor: 456 | # If we have a parent sync thread above somewhere, use that 457 | executor = current_thread_executor 458 | elif self.thread_sensitive_context.get(None): 459 | # If we have a way of retrieving the current context, attempt 460 | # to use a per-context thread pool executor 461 | thread_sensitive_context = self.thread_sensitive_context.get() 462 | 463 | if thread_sensitive_context in self.context_to_thread_executor: 464 | # Re-use thread executor in current context 465 | executor = self.context_to_thread_executor[thread_sensitive_context] 466 | else: 467 | # Create new thread executor in current context 468 | executor = ThreadPoolExecutor(max_workers=1) 469 | self.context_to_thread_executor[thread_sensitive_context] = executor 470 | elif loop in AsyncToSync.loop_thread_executors: 471 | # Re-use thread executor for running loop 472 | executor = AsyncToSync.loop_thread_executors[loop] 473 | elif self.deadlock_context.get(False): 474 | raise RuntimeError( 475 | "Single thread executor already being used, would deadlock" 476 | ) 477 | else: 478 | # Otherwise, we run it in a fixed single thread 479 | executor = self.single_thread_executor 480 | self.deadlock_context.set(True) 481 | else: 482 | # Use the passed in executor, or the loop's default if it is None 483 | executor = self._executor 484 | 485 | context = contextvars.copy_context() if self.context is None else self.context 486 | child = functools.partial(self.func, *args, **kwargs) 487 | func = context.run 488 | task_context: List[asyncio.Task[Any]] = [] 489 | 490 | # Run the code in the right thread 491 | exec_coro = loop.run_in_executor( 492 | executor, 493 | functools.partial( 494 | self.thread_handler, 495 | loop, 496 | sys.exc_info(), 497 | task_context, 498 | func, 499 | child, 500 | ), 501 | ) 502 | ret: _R 503 | try: 504 | ret = await asyncio.shield(exec_coro) 505 | except asyncio.CancelledError: 506 | cancel_parent = True 507 | try: 508 | task = task_context[0] 509 | task.cancel() 510 | try: 511 | await task 512 | cancel_parent = False 513 | except asyncio.CancelledError: 514 | pass 515 | except IndexError: 516 | pass 517 | if exec_coro.done(): 518 | raise 519 | if cancel_parent: 520 | exec_coro.cancel() 521 | ret = await exec_coro 522 | finally: 523 | if self.context is None: 524 | _restore_context(context) 525 | self.deadlock_context.set(False) 526 | 527 | return ret 528 | 529 | def __get__( 530 | self, parent: Any, objtype: Any 531 | ) -> Callable[_P, Coroutine[Any, Any, _R]]: 532 | """ 533 | Include self for methods 534 | """ 535 | func = functools.partial(self.__call__, parent) 536 | return functools.update_wrapper(func, self.func) 537 | 538 | def thread_handler(self, loop, exc_info, task_context, func, *args, **kwargs): 539 | """ 540 | Wraps the sync application with exception handling. 541 | """ 542 | 543 | __traceback_hide__ = True # noqa: F841 544 | 545 | # Set the threadlocal for AsyncToSync 546 | self.threadlocal.main_event_loop = loop 547 | self.threadlocal.main_event_loop_pid = os.getpid() 548 | self.threadlocal.task_context = task_context 549 | 550 | # Run the function 551 | # If we have an exception, run the function inside the except block 552 | # after raising it so exc_info is correctly populated. 553 | if exc_info[1]: 554 | try: 555 | raise exc_info[1] 556 | except BaseException: 557 | return func(*args, **kwargs) 558 | else: 559 | return func(*args, **kwargs) 560 | 561 | 562 | @overload 563 | def async_to_sync( 564 | *, 565 | force_new_loop: bool = False, 566 | ) -> Callable[ 567 | [Union[Callable[_P, Coroutine[Any, Any, _R]], Callable[_P, Awaitable[_R]]]], 568 | Callable[_P, _R], 569 | ]: 570 | ... 571 | 572 | 573 | @overload 574 | def async_to_sync( 575 | awaitable: Union[ 576 | Callable[_P, Coroutine[Any, Any, _R]], 577 | Callable[_P, Awaitable[_R]], 578 | ], 579 | *, 580 | force_new_loop: bool = False, 581 | ) -> Callable[_P, _R]: 582 | ... 583 | 584 | 585 | def async_to_sync( 586 | awaitable: Optional[ 587 | Union[ 588 | Callable[_P, Coroutine[Any, Any, _R]], 589 | Callable[_P, Awaitable[_R]], 590 | ] 591 | ] = None, 592 | *, 593 | force_new_loop: bool = False, 594 | ) -> Union[ 595 | Callable[ 596 | [Union[Callable[_P, Coroutine[Any, Any, _R]], Callable[_P, Awaitable[_R]]]], 597 | Callable[_P, _R], 598 | ], 599 | Callable[_P, _R], 600 | ]: 601 | if awaitable is None: 602 | return lambda f: AsyncToSync( 603 | f, 604 | force_new_loop=force_new_loop, 605 | ) 606 | return AsyncToSync( 607 | awaitable, 608 | force_new_loop=force_new_loop, 609 | ) 610 | 611 | 612 | @overload 613 | def sync_to_async( 614 | *, 615 | thread_sensitive: bool = True, 616 | executor: Optional["ThreadPoolExecutor"] = None, 617 | context: Optional[contextvars.Context] = None, 618 | ) -> Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]]: 619 | ... 620 | 621 | 622 | @overload 623 | def sync_to_async( 624 | func: Callable[_P, _R], 625 | *, 626 | thread_sensitive: bool = True, 627 | executor: Optional["ThreadPoolExecutor"] = None, 628 | context: Optional[contextvars.Context] = None, 629 | ) -> Callable[_P, Coroutine[Any, Any, _R]]: 630 | ... 631 | 632 | 633 | def sync_to_async( 634 | func: Optional[Callable[_P, _R]] = None, 635 | *, 636 | thread_sensitive: bool = True, 637 | executor: Optional["ThreadPoolExecutor"] = None, 638 | context: Optional[contextvars.Context] = None, 639 | ) -> Union[ 640 | Callable[[Callable[_P, _R]], Callable[_P, Coroutine[Any, Any, _R]]], 641 | Callable[_P, Coroutine[Any, Any, _R]], 642 | ]: 643 | if func is None: 644 | return lambda f: SyncToAsync( 645 | f, 646 | thread_sensitive=thread_sensitive, 647 | executor=executor, 648 | context=context, 649 | ) 650 | return SyncToAsync( 651 | func, 652 | thread_sensitive=thread_sensitive, 653 | executor=executor, 654 | context=context, 655 | ) 656 | --------------------------------------------------------------------------------