Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request config #2949

Merged
merged 16 commits into from
Apr 25, 2018
1 change: 1 addition & 0 deletions CHANGES/2949.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add ``request.config_dict`` for exposing nested applications data.
46 changes: 45 additions & 1 deletion aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import time
import weakref
from collections import namedtuple
from collections.abc import Mapping
from contextlib import suppress
from math import ceil
from pathlib import Path
Expand All @@ -30,7 +31,7 @@
from .log import client_logger


__all__ = ('BasicAuth',)
__all__ = ('BasicAuth', 'ChainMapProxy')

PY_36 = sys.version_info >= (3, 6)
PY_37 = sys.version_info >= (3, 7)
Expand Down Expand Up @@ -741,3 +742,46 @@ def set_result(fut, result):
def set_exception(fut, exc):
if not fut.done():
fut.set_exception(exc)


class ChainMapProxy(Mapping):
__slots__ = ('_maps',)

def __init__(self, maps):
self._maps = tuple(maps)

def __init_subclass__(cls):
raise TypeError("Inheritance class {} from ChainMapProxy "
"is forbidden".format(cls.__name__))

def __getitem__(self, key):
for mapping in self._maps:
try:
return mapping[key]
except KeyError:
pass
raise KeyError(key)

def get(self, key, default=None):
return self[key] if key in self else default

def __len__(self):
# reuses stored hash values if possible
return len(set().union(*self._maps))

def __iter__(self):
d = {}
for mapping in reversed(self._maps):
# reuses stored hash values if possible
d.update(mapping)
return iter(d)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like you can have there the same iter(set().union(*self._maps)) instead of another dict construction.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did copy collections.ChainMap without careful thinking :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it for sake of shared implementation with ChainMap

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iteration is not the most common call, get/__getitem__ doesn't create a dict.
Moreover your proposal constructs a set, not sure what is better.


def __contains__(self, key):
return any(key in m for m in self._maps)

def __bool__(self):
return any(self._maps)

def __repr__(self):
content = ", ".join(map(repr, self._maps))
return 'ChainMapProxy({})'.format(content)
10 changes: 9 additions & 1 deletion aiohttp/web_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from yarl import URL

from . import hdrs, multipart
from .helpers import DEBUG, HeadersMixin, reify, sentinel
from .helpers import DEBUG, ChainMapProxy, HeadersMixin, reify, sentinel
from .streams import EmptyStreamReader
from .web_exceptions import HTTPRequestEntityTooLarge

Expand Down Expand Up @@ -664,6 +664,14 @@ def app(self):
"""Application instance."""
return self._match_info.current_app

@property
def config_dict(self):
lst = self._match_info.apps
app = self.app
idx = lst.index(app)
sublist = list(reversed(lst[:idx + 1]))
return ChainMapProxy(sublist)

async def _prepare_hook(self, response):
match_info = self._match_info
if match_info is None:
Expand Down
55 changes: 55 additions & 0 deletions docs/structures.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.. _aiohttp-structures:


Common data structures
======================

.. module:: aiohttp

.. currentmodule:: aiohttp


Common data structures used by *aiohttp* internally.


FrozenList
----------

A list-like structure which implements
:class:`collections.abc.MutableSequence`.

The list is *mutable* unless :meth:`FrozenList.freeze` is called,
after that the list modification raises :exc:`RuntimeError`.


.. class:: FrozenList(items)

Construct a new *non-frozen* list from *items* iterable.

The list implements all :class:`collections.abc.MutableSequence`
methods plus two additional APIs.

.. attribute:: frozen

A read-only property, ``True`` is the list is *frozen*
(modifications are forbidden).

.. method:: freeze()

Freeze the list. There is no way to *thaw* it back.


ChainMapProxy
-------------

An *immutable* version of :class:`collections.ChainMap`. Internally
the proxy is a list of mappings (dictionaries), if the requested key
is not present in the first mapping the second is looked up and so on.

The class supports :class:`collections.abc.Mapping` interface.

.. class:: ChainMapProxy(maps)

Create a new chained mapping proxy from a list of mappings (*maps*).

.. versionadded:: 3.2
1 change: 1 addition & 0 deletions docs/utilities.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ Miscellaneous API Shared between Client And Server.
multipart_reference
streams
signals
structures
websocket_utilities
74 changes: 51 additions & 23 deletions docs/web_advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ internal data structures and could terminate them gracefully::
app.router.add_get('/', handler)

All not finished jobs will be terminated on
:attr:`aiohttp.web.Application.on_cleanup` signal.
:attr:`Application.on_cleanup` signal.

To prevent cancellation of the whole :term:`web-handler` use
``@atomic`` decorator::
Expand Down Expand Up @@ -306,7 +306,7 @@ handling every incoming request.
.. warning::

Parallel reads from websocket are forbidden, there is no
possibility to call :meth:`aiohttp.web.WebSocketResponse.receive`
possibility to call :meth:`WebSocketResponse.receive`
from two tasks.

See :ref:`FAQ section <aiohttp_faq_parallel_event_sources>` for
Expand All @@ -321,12 +321,18 @@ Data Sharing aka No Singletons Please
:mod:`aiohttp.web` discourages the use of *global variables*, aka *singletons*.
Every variable should have its own context that is *not global*.

So, :class:`aiohttp.web.Application` and :class:`aiohttp.web.Request`
So, :class:`Application` and :class:`Request`
support a :class:`collections.abc.MutableMapping` interface (i.e. they are
dict-like objects), allowing them to be used as data stores.


.. _aiohttp-web-data-sharing-app-config:

Application's config
^^^^^^^^^^^^^^^^^^^^

For storing *global-like* variables, feel free to save them in an
:class:`~.Application` instance::
:class:`Application` instance::

app['my_private_key'] = data

Expand All @@ -335,8 +341,24 @@ and get it back in the :term:`web-handler`::
async def handler(request):
data = request.app['my_private_key']

Variables that are only needed for the lifetime of a :class:`~.Request`, can be
stored in a :class:`~.Request`::
In case of :ref:`nested applications
<aiohttp-web-nested-applications>` the desired lookup strategy could
be the following:

1. Search the key in the current nested application.
2. If the key is not found continue searching in the parent application(s).

For this please use :attr:`Request.config_dict` read-only property::

async def handler(request):
data = request.config_dict['my_private_key']


Request's storage
^^^^^^^^^^^^^^^^^

Variables that are only needed for the lifetime of a :class:`Request`, can be
stored in a :class:`Request`::

async def handler(request):
request['my_private_key'] = "data"
Expand All @@ -346,7 +368,10 @@ This is mostly useful for :ref:`aiohttp-web-middlewares` and
:ref:`aiohttp-web-signals` handlers to store data for further processing by the
next handlers in the chain.

:class:`aiohttp.web.StreamResponse` and :class:`aiohttp.web.Response` objects
Response's storage
^^^^^^^^^^^^^^^^^^

:class:`StreamResponse` and :class:`Response` objects
also support :class:`collections.abc.MutableMapping` interface. This is useful
when you want to share data with signals and middlewares once all the work in
the handler is done::
Expand All @@ -357,6 +382,9 @@ the handler is done::
return response


Naming hint
^^^^^^^^^^^

To avoid clashing with other *aiohttp* users and third-party libraries, please
choose a unique key name for storing data.

Expand Down Expand Up @@ -392,8 +420,8 @@ response. For example, here's a simple *middleware* which appends
Every *middleware* should accept two parameters, a :class:`request
<Request>` instance and a *handler*, and return the response or raise
an exception. If the exception is not an instance of
:exc:`aiohttp.web.HTTPException` it is converted to ``500``
:exc:`aiohttp.web.HTTPInternalServerError` after processing the
:exc:`HTTPException` it is converted to ``500``
:exc:`HTTPInternalServerError` after processing the
middlewares chain.

.. warning::
Expand Down Expand Up @@ -507,19 +535,19 @@ has been prepared, they can't customize a :class:`Response` **while** it's
being prepared. For this :mod:`aiohttp.web` provides *signals*.

For example, a middleware can only change HTTP headers for *unprepared*
responses (see :meth:`~aiohttp.web.StreamResponse.prepare`), but sometimes we
responses (see :meth:`StreamResponse.prepare`), but sometimes we
need a hook for changing HTTP headers for streamed responses and WebSockets.
This can be accomplished by subscribing to the
:attr:`~aiohttp.web.Application.on_response_prepare` signal::
:attr:`Application.on_response_prepare` signal::

async def on_prepare(request, response):
response.headers['My-Header'] = 'value'

app.on_response_prepare.append(on_prepare)


Additionally, the :attr:`~aiohttp.web.Application.on_startup` and
:attr:`~aiohttp.web.Application.on_cleanup` signals can be subscribed to for
Additionally, the :attr:`Application.on_startup` and
:attr:`Application.on_cleanup` signals can be subscribed to for
application component setup and tear down accordingly.

The following example will properly initialize and dispose an aiopg connection
Expand Down Expand Up @@ -621,7 +649,7 @@ toolbar URLs are served by prefix like ``/admin``.

Thus we'll create a totally separate application named ``admin`` and
connect it to main app with prefix by
:meth:`~aiohttp.web.Application.add_subapp`::
:meth:`Application.add_subapp`::

admin = web.Application()
# setup admin routes, signals and middlewares
Expand All @@ -635,13 +663,13 @@ It means that if URL is ``'/admin/something'`` middlewares from
the call chain.

The same is going for
:attr:`~aiohttp.web.Application.on_response_prepare` signal -- the
:attr:`Application.on_response_prepare` signal -- the
signal is delivered to both top level ``app`` and ``admin`` if
processing URL is routed to ``admin`` sub-application.

Common signals like :attr:`~aiohttp.web.Application.on_startup`,
:attr:`~aiohttp.web.Application.on_shutdown` and
:attr:`~aiohttp.web.Application.on_cleanup` are delivered to all
Common signals like :attr:`Application.on_startup`,
:attr:`Application.on_shutdown` and
:attr:`Application.on_cleanup` are delivered to all
registered sub-applications. The passed parameter is sub-application
instance, not top-level application.

Expand Down Expand Up @@ -895,8 +923,8 @@ As discussed in :ref:`aiohttp-deployment` the preferable way is
deploying *aiohttp* web server behind a *Reverse Proxy Server* like
:term:`nginx` for production usage.

In this way properties like :attr:`~BaseRequest.scheme`
:attr:`~BaseRequest.host` and :attr:`~BaseRequest.remote` are
In this way properties like :attr:`BaseRequest.scheme`
:attr:`BaseRequest.host` and :attr:`BaseRequest.remote` are
incorrect.

Real values should be given from proxy server, usually either
Expand All @@ -910,9 +938,9 @@ headers too, pushing non-trusted data values.
That's why *aiohttp server* should setup *forwarded* headers in custom
middleware in tight conjunction with *reverse proxy configuration*.

For changing :attr:`~BaseRequest.scheme` :attr:`~BaseRequest.host` and
:attr:`~BaseRequest.remote` the middleware might use
:meth:`~BaseRequest.clone`.
For changing :attr:`BaseRequest.scheme` :attr:`BaseRequest.host` and
:attr:`BaseRequest.remote` the middleware might use
:meth:`BaseRequest.clone`.

.. seealso::

Expand Down
11 changes: 11 additions & 0 deletions docs/web_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,16 @@ and :ref:`aiohttp-web-signals` handlers.
An :class:`Application` instance used to call :ref:`request handler
<aiohttp-web-handler>`, Read-only property.

.. attribute:: config_dict

A :class:`aiohttp.ChainMapProxy` instance for mapping all properties
from the current application returned by :attr:`app` property
and all its parents.

.. seealso:: :ref:`aiohttp-web-data-sharing-app-config`

.. versionadded:: 3.2

.. note::

You should never create the :class:`Request` instance manually
Expand All @@ -498,6 +508,7 @@ and :ref:`aiohttp-web-signals` handlers.




.. _aiohttp-web-response:


Expand Down
Loading