Skip to content

Commit

Permalink
3.0: Single-callable style applications & lifecycle improvements (#80)
Browse files Browse the repository at this point in the history
This switches the ASGI specification to use a single, async callable, as well as improving the error behaviour of the lifecycle scope type.
  • Loading branch information
andrewgodwin authored Mar 20, 2019
1 parent e4015bf commit 8ae25bd
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 50 deletions.
47 changes: 47 additions & 0 deletions asgiref/compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import asyncio
import inspect


def is_double_callable(application):
"""
Tests to see if an application is a legacy-style (double-callable) application.
"""
# Look for a hint on the object first
if getattr(application, "_asgi_single_callable", False):
return False
if getattr(application, "_asgi_double_callable", False):
return True
# Uninstanted classes are double-callable
if inspect.isclass(application):
return True
# Instanted classes depend on their __call__
if hasattr(application, "__call__"):
# We only check to see if its __call__ is a coroutine function -
# if it's not, it still might be a coroutine function itself.
if asyncio.iscoroutinefunction(application.__call__):
return False
# Non-classes we just check directly
return not asyncio.iscoroutinefunction(application)


def double_to_single_callable(application):
"""
Transforms a double-callable ASGI application into a single-callable one.
"""

async def new_application(scope, receive, send):
instance = application(scope)
return await instance(receive, send)

return new_application


def guarantee_single_callable(application):
"""
Takes either a single- or double-callable application and always returns it
in single-callable style. Use this to add backwards compatibility for ASGI
2.0 applications to your server/test harness/etc.
"""
if is_double_callable(application):
application = double_to_single_callable(application)
return application
11 changes: 6 additions & 5 deletions asgiref/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,22 @@

import async_timeout

from .compatibility import guarantee_single_callable


class ApplicationCommunicator:
"""
Runs an ASGI application in a test mode, allowing sending of messages to
it and retrieval of messages it sends.
Runs an ASGI application in a test mode, allowing sending of
messages to it and retrieval of messages it sends.
"""

def __init__(self, application, scope):
self.application = application
self.application = guarantee_single_callable(application)
self.scope = scope
self.instance = self.application(scope)
self.input_queue = asyncio.Queue()
self.output_queue = asyncio.Queue()
self.future = asyncio.ensure_future(
self.instance(self.input_queue.get, self.output_queue.put)
self.application(scope, self.input_queue.get, self.output_queue.put)
)

async def wait(self, timeout=1):
Expand Down
10 changes: 5 additions & 5 deletions asgiref/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@ class WsgiToAsgi:
def __init__(self, wsgi_application):
self.wsgi_application = wsgi_application

def __call__(self, scope):
async def __call__(self, scope, receive, send):
"""
ASGI application instantiation point.
We return a new WsgiToAsgiInstance here with the WSGI app
and the scope, ready to respond when it is __call__ed.
"""
return WsgiToAsgiInstance(self.wsgi_application, scope)
await WsgiToAsgiInstance(self.wsgi_application)(scope, receive, send)


class WsgiToAsgiInstance:
"""
Per-socket instance of a wrapped WSGI application
"""

def __init__(self, wsgi_application, scope):
def __init__(self, wsgi_application):
self.wsgi_application = wsgi_application
self.scope = scope
self.response_started = False

async def __call__(self, receive, send):
async def __call__(self, scope, receive, send):
self.scope = scope
# Alright, wait for the http.request message
message = await receive()
if message["type"] != "http.request":
Expand Down
91 changes: 56 additions & 35 deletions specs/asgi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
ASGI (Asynchronous Server Gateway Interface) Specification
==========================================================

**Version**: 2.0 (2017-11-28)
**Version**: 3.0 (2019-03-04)

Abstract
========
Expand Down Expand Up @@ -143,46 +143,29 @@ serializable, and so they are only allowed to contain the following types:
Applications
------------

ASGI applications are defined as a callable::
.. note::

application(scope)

* ``scope``: The Connection Scope, a dictionary that contains at least a
``type`` key specifying the protocol that is incoming.
The application format changed in 3.0 to use a single callable, rather than
the prior two-callable format. Two-callable is documented below in
"Legacy Applications"; servers can easily implement support for it using
the ``asgiref.compatibility`` library, and should try to support it.

This first callable is called whenever a new connection comes in to the
protocol server, and creates a new *instance* of the application per
connection (the instance is the object that this first callable returns).
ASGI applications should be a single async callable::

This callable is synchronous, and must not contain blocking calls (it's
recommended that all it does is store the scope). If you need to do
blocking work, you must do it at the start of the next callable, before you
application awaits incoming events.

It must return another, awaitable callable::

coroutine application_instance(receive, send)
coroutine application(scope, receive, send)

* ``scope``: The Connection Scope, a dictionary that contains at least a
``type`` key specifying the protocol that is incoming.
* ``receive``, an awaitable callable that will yield a new event dict when
one is available
* ``send``, an awaitable callable taking a single event dict as a
positional argument that will return once the send has been
completed or the connection has been closed

This design is perhaps more easily recognised as one of its possible
implementations, as a class::

class Application:

def __init__(self, scope):
self.scope = scope

async def __call__(self, receive, send):
...

The application interface is specified as the more generic case of two callables
to allow more flexibility for things like factory functions or type-based
dispatchers.
The application is called once per "connection". What exactly a connection is
and its lifespan is dictated by the protocol specification in question. For
example, with HTTP it is one request, whereas for a WebSocket it is a single
WebSocket connection.

Both the ``scope`` and the format of the messages you send and receive
are defined by one of the application protocols. ``scope`` must be a
Expand All @@ -194,11 +177,40 @@ the server implements. If missing the version should default to
``"2.0"``.

There may also be a spec-specific version present as
``scope["asgi"]["spec_version"]``.
``scope["asgi"]["spec_version"]``. This allows the individual protocol
specifications to make enhancements without bumping the overall ASGI version.

The protocol-specific sub-specifications cover these scope and message formats.
They are equivalent to the specification for keys in the ``environ`` dict for
WSGI.

The protocol-specific sub-specifications cover these scope
and message formats. They are equivalent to the specification for keys in the
``environ`` dict for WSGI.

Legacy Applications
-------------------

Legacy (v2.0) ASGI applications are defined as a callable::

application(scope)

Which returns another, awaitable callable::

coroutine application_instance(receive, send)

The meanings of ``scope``, ``receive`` and ``send`` are the same as in the
newer single-callable application, but note that the first callable is
*synchronous*.

The first callable is called when the connection is started, and then the
second callable is called immediately afterwards.

This style was retired in version 3.0 as the two-callable layout was deemed
unnecessary. It's now legacy, but there are applications out there written in
this style, and so it's important to support them.

There is a compatability suite available in the ``asgiref.compatability``
module which allows you to both detect legacy applications, and convert them
to the new single-protocol style seamlessly. Servers are encouraged to support
both types as of ASGI 3.0 and gradually drop support by default over time.


Protocol Specifications
Expand All @@ -217,6 +229,14 @@ where the ``protocol`` matches the scope type, and ``message_type`` is
defined by the protocol spec. Examples of a message ``type`` value include
``http.request`` and ``websocket.send``.

.. note::

Applications should actively reject any protocol that they do not understand
with an Exception (of any type). Failure to do this may result in the server
thinking you support a protocol you don't, which can be confusing with
the Lifespan protocol, as the server will wait to start until you tell it.


Current protocol specifications:

* :doc:`HTTP and WebSocket <www>`
Expand Down Expand Up @@ -344,6 +364,7 @@ unicode strings.
Version History
===============

* 3.0 (2019-03-04): Changed to single-callable application style
* 2.0 (2017-11-28): Initial non-channel-layer based ASGI spec


Expand Down
42 changes: 37 additions & 5 deletions specs/lifespan.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Lifespan Protocol
=================

**Version**: 1.0 (2018-09-06)
**Version**: 2.0 (2019-03-04)

The Lifespan ASGI sub-specification outlines how to communicate
lifespan events such as startup and shutdown within ASGI. The lifespan
Expand Down Expand Up @@ -46,14 +46,20 @@ Scope
'''''

The lifespan scope exists for the duration of the event loop. The
scope itself contains basic metadata,
scope itself contains basic metadata:

* ``type``: ``lifespan``
* ``asgi["version"]``: The version of the ASGI spec, as a string.
* ``asgi["spec_version"]``: The version of this spec being used, as a string. Optional, defaults to ``"1.0"``.

If an exception is raised when calling the application callable with a
lifespan scope the server must continue but not send any lifespan
events. This allows for compatibility with applications that do not
support the lifespan protocol.
``lifespan.startup`` message or a scope with type ``lifespan``
the server must continue but not send any lifespan events.

This allows for compatibility with applications that do not support the lifespan
protocol. If you want to log an error that occurred during lifespan startup and
prevent the server from starting, then send back ``lifespan.startup.failed``
instead.


Startup
Expand All @@ -78,6 +84,18 @@ Keys:
* ``type``: ``lifespan.startup.complete``


Startup Failed
''''''''''''''

Sent by the application when it has failed to complete its startup. If a server
sees this it should log/print the message provided and then exit.

Keys:

* ``type``: ``lifespan.startup.failed``
* ``message``: Unicode string (optional, defaults to ``""``).


Shutdown
''''''''

Expand All @@ -100,9 +118,23 @@ Keys:
* ``type``: ``lifespan.shutdown.complete``


Shutdown Failed
'''''''''''''''

Sent by the application when it has failed to complete its cleanup. If a server
sees this it should log/print the message provided and then terminate.

Keys:

* ``type``: ``lifespan.shutdown.failed``
* ``message``: Unicode string (optional, defaults to ``""``).


Version History
===============

* 2.0 (2019-03-04): Added startup.failed and shutdown.failed,
clarified exception handling during startup phase.
* 1.0 (2018-09-06): Updated ASGI spec with a lifespan protocol.


Expand Down
4 changes: 4 additions & 0 deletions specs/www.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ The connection scope contains:

* ``type``: ``http``

* ``asgi["version"]``: The version of the ASGI spec, as a string.

* ``asgi['spec_version']``: Version of the ASGI HTTP spec this server understands
as a string; one of ``2.0`` or ``2.1``. Optional, if missing assume ``2.0``.

Expand Down Expand Up @@ -207,6 +209,8 @@ contains the initial connection metadata (mostly from the HTTP handshake):

* ``type``: ``websocket``

* ``asgi["version"]``: The version of the ASGI spec, as a string.

* ``asgi['spec_version']``: Version of the ASGI HTTP spec this server understands
as a string; one of ``2.0`` or ``2.1``. Optional, if missing assume ``2.0``.

Expand Down
Loading

0 comments on commit 8ae25bd

Please sign in to comment.