diff --git a/.travis.yml b/.travis.yml index 8deffb336c9..0b08ce660a6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ install: - pip install --upgrade pip wheel - pip install --upgrade setuptools - pip install -r requirements-ci.txt + - pip install aiodns - pip install coveralls script: diff --git a/CHANGES.txt b/CHANGES.txt index 3cf2393b461..e565f2398e0 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,41 @@ CHANGES 0.22.0 (XX-XX-XXXX) ------------------- +- Fix bug in serving static directory #803 + +- Fix command line arg parsing #797 + +- Fix a documentation chapter about cookie usage #790 + +- Handle empty body with gzipped encoding #758 + +- Support 451 Unavailable For Legal Reasons http status #697 + +- Fix Cookie share example and few small typos in docs #817 + +- UrlDispatcher.add_route with partial coroutine handler #814 + +- Optional support for aiodns #728 + +- Add ServiceRestart and TryAgainLater websocket close codes #828 + +- Fix prompt message for `web.run_app` #832 + +- Allow to pass None as a timeout value to disable timeout logic #834 + +0.21.5 (04-22-2016) +------------------- + +- Fix command line arg parsing #797 + +0.21.4 (03-12-2016) +------------------- + +- Fix ResourceAdapter: dont add method to allowed if resource is not + match #826 + +- Fix Resouce: append found method to returned allowed methods + 0.21.2 (02-16-2016) ------------------- diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 387837593e0..c29e903fde5 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -5,6 +5,7 @@ A. Jesse Jiryu Davis Alejandro Gómez Alex Khomchenko Alex Lisovoy +Alex Key Alexander Bayandin Alexander Karpinsky Alexander Malev @@ -54,6 +55,7 @@ Kirill Klenov Kirill Malovitsa Kyrylo Perevozchikov Lars P. Søndergaard +Louis-Philippe Huberdeau Ludovic Gasc Lukasz Marcin Dobrzanski Marco Paolini @@ -78,6 +80,7 @@ Sebastian Hüther Sergey Ninua Sergey Skripnick Simon Kennedy +Sin-Woo Bang Stanislas Plum Stanislav Prokop Stephen Granade @@ -94,5 +97,7 @@ Vladimir Rutsky Vladimir Shulyak Vladimir Zakharov W. Trevor King +Willem de Groot Yannick Koechlin -Коренберг Марк +Марк Коренберг +Семён Марьясин diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..2966f512751 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,23 @@ +## Long story short + + + +## Expected behaviour + + + +## Actual behaviour + + + +## Steps to reproduce + + + +## Your environment + + diff --git a/Makefile b/Makefile index 3beed227605..6adc85cc76a 100644 --- a/Makefile +++ b/Makefile @@ -8,8 +8,8 @@ flake: .install-deps # python setup.py check -rms flake8 aiohttp if python -c "import sys; sys.exit(sys.version_info < (3,5))"; then \ - flake8 examples tests; \ - fi + flake8 examples tests; \ + fi .develop: .install-deps $(shell find aiohttp -type f) @@ -26,13 +26,13 @@ cov cover coverage: tox cov-dev: .develop - py.test --cov=aiohttp --cov-report=term --cov-report=html tests + py.test --cov=aiohttp --cov-report=term --cov-report=html tests @echo "open file://`pwd`/coverage/index.html" cov-dev-full: .develop - AIOHTTP_NO_EXTENSIONS=1 py.test --cov=aiohttp --cov-append tests - PYTHONASYNCIODEBUG=1 py.test --cov=aiohttp --cov-append tests - py.test --cov=aiohttp --cov-report=term --cov-report=html tests + AIOHTTP_NO_EXTENSIONS=1 py.test --cov=aiohttp --cov-append tests + PYTHONASYNCIODEBUG=1 py.test --cov=aiohttp --cov-append tests + py.test --cov=aiohttp --cov-report=term --cov-report=html tests @echo "open file://`pwd`/coverage/index.html" clean: diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..67414ac235f --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ + + +## What these changes does? + + + +## How to test your changes? + + + +## Related issue number + + + +## Checklist + +- [ ] Code is written and well +- [ ] Tests for the changes are provided +- [ ] Documentation reflects the changes diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index af5a2bd20c3..9769c6332a4 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -2,7 +2,9 @@ __version__ = '0.22.0a0' +import multidict # noqa +from multidict import * # noqa from . import hdrs # noqa from .protocol import * # noqa from .connector import * # noqa @@ -12,20 +14,19 @@ from .helpers import * # noqa from .parsers import * # noqa from .streams import * # noqa -from .multidict import * # noqa from .multipart import * # noqa from .websocket_client import * # noqa -__all__ = (client.__all__ + - client_reqrep.__all__ + - errors.__all__ + - helpers.__all__ + - parsers.__all__ + - protocol.__all__ + - connector.__all__ + - streams.__all__ + - multidict.__all__ + - multipart.__all__ + - websocket_client.__all__ + +__all__ = (client.__all__ + # noqa + client_reqrep.__all__ + # noqa + errors.__all__ + # noqa + helpers.__all__ + # noqa + parsers.__all__ + # noqa + protocol.__all__ + # noqa + connector.__all__ + # noqa + streams.__all__ + # noqa + multidict.__all__ + # noqa + multipart.__all__ + # noqa + websocket_client.__all__ + # noqa ('hdrs', '__version__')) diff --git a/aiohttp/_multidict.pyx b/aiohttp/_multidict.pyx deleted file mode 100644 index a61f0ba735d..00000000000 --- a/aiohttp/_multidict.pyx +++ /dev/null @@ -1,653 +0,0 @@ -import sys -from collections import abc -from collections.abc import Iterable, Set -from operator import itemgetter - - -_marker = object() - - -class upstr(str): - - """Case insensitive str.""" - - def __new__(cls, val='', - encoding=sys.getdefaultencoding(), errors='strict'): - if isinstance(val, (bytes, bytearray, memoryview)): - val = str(val, encoding, errors) - elif isinstance(val, str): - pass - else: - val = str(val) - val = val.upper() - return str.__new__(cls, val) - - def upper(self): - return self - - -cdef _eq(self, other): - cdef _Base typed_self - cdef _Base typed_other - cdef int is_left_base, is_right_base - - is_left_base = isinstance(self, _Base) - is_right_base = isinstance(other, _Base) - - if is_left_base and is_right_base: - return (<_Base>self)._items == (<_Base>other)._items - elif is_left_base and isinstance(other, abc.Mapping): - return (<_Base>self)._eq_to_mapping(other) - elif is_right_base and isinstance(self, abc.Mapping): - return (<_Base>other)._eq_to_mapping(self) - else: - return NotImplemented - - -cdef class _Pair: - cdef object _key - cdef object _value - - def __cinit__(self, key, value): - self._key = key - self._value = value - - def __richcmp__(self, other, op): - cdef _Pair left, right - if not isinstance(self, _Pair) or not isinstance(other, _Pair): - return NotImplemented - left = <_Pair>self - right = <_Pair>other - if op == 2: # == - return left._key == right._key and left._value == right._value - elif op == 3: # != - return left._key != right._key and left._value != right._value - -cdef class _Base: - - cdef list _items - cdef object _upstr - cdef object marker - - def __cinit__(self): - self._upstr = upstr - self.marker = _marker - - cdef str _upper(self, s): - if type(s) is self._upstr: - return s - return s - - def getall(self, key, default=_marker): - """Return a list of all values matching the key.""" - return self._getall(self._upper(key), default) - - cdef _getall(self, str key, default): - cdef list res - cdef _Pair item - key = self._upper(key) - res = [] - for i in self._items: - item = <_Pair>i - if item._key == key: - res.append(item._value) - if res: - return res - if not res and default is not self.marker: - return default - raise KeyError('Key not found: %r' % key) - - def getone(self, key, default=_marker): - """Get first value matching the key.""" - return self._getone(self._upper(key), default) - - cdef _getone(self, str key, default): - cdef _Pair item - key = self._upper(key) - for i in self._items: - item = <_Pair>i - if item._key == key: - return item._value - if default is not self.marker: - return default - raise KeyError('Key not found: %r' % key) - - # Mapping interface # - - def __getitem__(self, key): - return self._getone(self._upper(key), self.marker) - - def get(self, key, default=None): - """Get first value matching the key. - - The method is alias for .getone(). - """ - return self._getone(self._upper(key), default) - - def __contains__(self, key): - return self._contains(self._upper(key)) - - cdef _contains(self, str key): - cdef _Pair item - key = self._upper(key) - for i in self._items: - item = <_Pair>i - if item._key == key: - return True - return False - - def __iter__(self): - return iter(self.keys()) - - def __len__(self): - return len(self._items) - - cpdef keys(self): - """Return a new view of the dictionary's keys.""" - return _KeysView.__new__(_KeysView, self._items) - - def items(self): - """Return a new view of the dictionary's items *(key, value) pairs).""" - return _ItemsView.__new__(_ItemsView, self._items) - - def values(self): - """Return a new view of the dictionary's values.""" - return _ValuesView.__new__(_ValuesView, self._items) - - def __repr__(self): - cdef _Pair item - lst = [] - for i in self._items: - item = <_Pair>i - lst.append("'{}': {!r}".format(item._key, item._value)) - body = ', '.join(lst) - return '<{}({})>'.format(self.__class__.__name__, body) - - cdef _eq_to_mapping(self, other): - cdef _Pair item - left_keys = set(self.keys()) - right_keys = set(other.keys()) - if left_keys != right_keys: - return False - if len(self._items) != len(right_keys): - return False - for i in self._items: - item = <_Pair>i - nv = other.get(item._key, self.marker) - if item._value != nv: - return False - return True - - def __richcmp__(self, other, op): - if op == 2: # == - return _eq(self, other) - elif op == 3: # != - ret = _eq(self, other) - if ret is NotImplemented: - return ret - else: - return not ret - else: - return NotImplemented - - -cdef class MultiDictProxy(_Base): - - def __init__(self, arg): - cdef MultiDict mdict - if not isinstance(arg, MultiDict): - raise TypeError( - 'MultiDictProxy requires MultiDict instance, not {}'.format( - type(arg))) - - mdict = arg - self._items = mdict._items - - def copy(self): - """Return a copy of itself.""" - return MultiDict(self._items) - -abc.Mapping.register(MultiDictProxy) - - -cdef class CIMultiDictProxy(MultiDictProxy): - - def __init__(self, arg): - cdef CIMultiDict mdict - if not isinstance(arg, CIMultiDict): - raise TypeError( - 'CIMultiDictProxy requires CIMultiDict instance, not {}'.format( - type(arg))) - - mdict = arg - self._items = mdict._items - - cdef str _upper(self, s): - if type(s) is self._upstr: - return s - return s.upper() - - def copy(self): - """Return a copy of itself.""" - return CIMultiDict(self._items) - - -abc.Mapping.register(CIMultiDictProxy) - - -cdef class MultiDict(_Base): - """An ordered dictionary that can have multiple values for each key.""" - - def __init__(self, *args, **kwargs): - self._items = [] - - self._extend(args, kwargs, self.__class__.__name__, 1) - - cdef _extend(self, tuple args, dict kwargs, name, int do_add): - cdef _Pair item - cdef str key - - if len(args) > 1: - raise TypeError("{} takes at most 1 positional argument" - " ({} given)".format(name, len(args))) - - if args: - arg = args[0] - if isinstance(arg, _Base): - for i in (<_Base>arg)._items: - item = <_Pair>i - key = self._upper(item._key) - value = item._value - if do_add: - self._add(key, value) - else: - self._replace(key, value) - elif hasattr(arg, 'items'): - for i in arg.items(): - if isinstance(i, _Pair): - item = <_Pair>i - key = item._key - value = item._value - else: - key = self._upper(i[0]) - value = i[1] - if do_add: - self._add(key, value) - else: - self._replace(key, value) - else: - for i in arg: - if isinstance(i, _Pair): - item = <_Pair>i - key = item._key - value = item._value - else: - if not len(i) == 2: - raise TypeError( - "{} takes either dict or list of (key, value) " - "tuples".format(name)) - key = self._upper(i[0]) - value = i[1] - if do_add: - self._add(key, value) - else: - self._replace(key, value) - - - for key, value in kwargs.items(): - key = self._upper(key) - if do_add: - self._add(key, value) - else: - self._replace(key, value) - - cdef _add(self, str key, value): - self._items.append(_Pair.__new__(_Pair, key, value)) - - cdef _replace(self, str key, value): - self._remove(key, 0) - self._items.append(_Pair.__new__(_Pair, key, value)) - - def add(self, key, value): - """Add the key and value, not overwriting any previous value.""" - self._add(self._upper(key), value) - - def copy(self): - """Return a copy of itself.""" - cls = self.__class__ - return cls(self._items) - - def extend(self, *args, **kwargs): - """Extend current MultiDict with more values. - - This method must be used instead of update. - """ - self._extend(args, kwargs, "extend", 1) - - def clear(self): - """Remove all items from MultiDict""" - self._items.clear() - - # MutableMapping interface # - - def __setitem__(self, key, value): - self._replace(self._upper(key), value) - - def __delitem__(self, key): - self._remove(self._upper(key), True) - - cdef _remove(self, str key, int raise_key_error): - cdef _Pair item - cdef int found - found = False - for i in range(len(self._items) - 1, -1, -1): - item = <_Pair>self._items[i] - if item._key == key: - del self._items[i] - found = True - if not found and raise_key_error: - raise KeyError(key) - - def setdefault(self, key, default=None): - """Return value for key, set value to default if key is not present.""" - cdef str skey - cdef _Pair item - skey = self._upper(key) - for i in self._items: - item = <_Pair>i - if item._key == skey: - return item._value - self._add(skey, default) - return default - - def pop(self, key, default=_marker): - """Remove specified key and return the corresponding value. - - If key is not found, d is returned if given, otherwise - KeyError is raised. - - """ - cdef int found - cdef str skey - cdef object value - cdef _Pair item - skey = self._upper(key) - value = None - found = False - for i in range(len(self._items) - 1, -1, -1): - item = <_Pair>self._items[i] - if item._key == key: - value = item._value - del self._items[i] - found = True - if not found: - if default is self.marker: - raise KeyError(key) - else: - return default - else: - return value - - def popitem(self): - """Remove and return an arbitrary (key, value) pair.""" - cdef _Pair item - if self._items: - item = <_Pair>self._items.pop(0) - return (item._key, item._value) - else: - raise KeyError("empty multidict") - - def update(self, *args, **kwargs): - """Update the dictionary from *other*, overwriting existing keys.""" - self._extend(args, kwargs, "update", 0) - - -abc.MutableMapping.register(MultiDict) - - -cdef class CIMultiDict(MultiDict): - """An ordered dictionary that can have multiple values for each key.""" - - cdef str _upper(self, s): - if type(s) is self._upstr: - return s - return s.upper() - - - -abc.MutableMapping.register(CIMultiDict) - - -cdef class _ViewBase: - - cdef list _items - - def __cinit__(self, list items): - self._items = items - - def __len__(self): - return len(self._items) - - -cdef class _ViewBaseSet(_ViewBase): - - def __richcmp__(self, other, op): - if op == 0: # < - if not isinstance(other, Set): - return NotImplemented - return len(self) < len(other) and self <= other - elif op == 1: # <= - if not isinstance(other, Set): - return NotImplemented - if len(self) > len(other): - return False - for elem in self: - if elem not in other: - return False - return True - elif op == 2: # == - if not isinstance(other, Set): - return NotImplemented - return len(self) == len(other) and self <= other - elif op == 3: # != - return not self == other - elif op == 4: # > - if not isinstance(other, Set): - return NotImplemented - return len(self) > len(other) and self >= other - elif op == 5: # >= - if not isinstance(other, Set): - return NotImplemented - if len(self) < len(other): - return False - for elem in other: - if elem not in self: - return False - return True - - def __and__(self, other): - if not isinstance(other, Iterable): - return NotImplemented - if not isinstance(other, Set): - other = set(other) - return set(self) & other - - def __or__(self, other): - if not isinstance(other, Iterable): - return NotImplemented - if not isinstance(other, Set): - other = set(other) - return set(self) | other - - def __sub__(self, other): - if not isinstance(other, Iterable): - return NotImplemented - if not isinstance(other, Set): - other = set(other) - return set(self) - other - - def __xor__(self, other): - if not isinstance(other, Set): - if not isinstance(other, Iterable): - return NotImplemented - other = set(other) - return set(self) ^ other - - -cdef class _ItemsIter: - cdef list _items - cdef int _current - cdef int _len - - def __cinit__(self, items): - self._items = items - self._current = 0 - self._len = len(self._items) - - def __iter__(self): - return self - - def __next__(self): - if self._current == self._len: - raise StopIteration - item = <_Pair>self._items[self._current] - self._current += 1 - return (item._key, item._value) - - -cdef class _ItemsView(_ViewBaseSet): - - def isdisjoint(self, other): - 'Return True if two sets have a null intersection.' - cdef _Pair item - for i in self._items: - item = <_Pair>i - t = (item._key, item._value) - if t in other: - return False - return True - - def __contains__(self, i): - cdef _Pair item - assert isinstance(i, tuple) or isinstance(i, list) - assert len(i) == 2 - item = _Pair.__new__(_Pair, i[0], i[1]) - return item in self._items - - def __iter__(self): - return _ItemsIter.__new__(_ItemsIter, self._items) - - def __repr__(self): - cdef _Pair item - lst = [] - for i in self._items: - item = <_Pair>i - lst.append("{!r}: {!r}".format(item._key, item._value)) - body = ', '.join(lst) - return '{}({})'.format(self.__class__.__name__, body) - - -abc.ItemsView.register(_ItemsView) - - -cdef class _ValuesIter: - cdef list _items - cdef int _current - cdef int _len - - def __cinit__(self, items): - self._items = items - self._current = 0 - self._len = len(self._items) - - def __iter__(self): - return self - - def __next__(self): - if self._current == self._len: - raise StopIteration - item = <_Pair>self._items[self._current] - self._current += 1 - return item._value - - -cdef class _ValuesView(_ViewBase): - - def __contains__(self, value): - cdef _Pair item - for i in self._items: - item = <_Pair>i - if item._value == value: - return True - return False - - def __iter__(self): - return _ValuesIter.__new__(_ValuesIter, self._items) - - def __repr__(self): - cdef _Pair item - lst = [] - for i in self._items: - item = <_Pair>i - lst.append("{!r}".format(item._value)) - body = ', '.join(lst) - return '{}({})'.format(self.__class__.__name__, body) - - -abc.ValuesView.register(_ValuesView) - - -cdef class _KeysIter: - cdef list _items - cdef int _current - cdef int _len - - def __cinit__(self, items): - self._items = items - self._current = 0 - self._len = len(self._items) - - def __iter__(self): - return self - - def __next__(self): - if self._current == self._len: - raise StopIteration - item = <_Pair>self._items[self._current] - self._current += 1 - return item._key - - -cdef class _KeysView(_ViewBaseSet): - - def isdisjoint(self, other): - 'Return True if two sets have a null intersection.' - cdef _Pair item - for i in self._items: - item = <_Pair>i - if item._key in other: - return False - return True - - def __contains__(self, value): - cdef _Pair item - for i in self._items: - item = <_Pair>i - if item._key == value: - return True - return False - - def __iter__(self): - return _KeysIter.__new__(_KeysIter, self._items) - - def __repr__(self): - cdef _Pair item - lst = [] - for i in self._items: - item = <_Pair>i - lst.append("{!r}".format(item._key)) - body = ', '.join(lst) - return '{}({})'.format(self.__class__.__name__, body) - - -abc.KeysView.register(_KeysView) diff --git a/aiohttp/abc.py b/aiohttp/abc.py index 39218e40891..fa41c043e34 100644 --- a/aiohttp/abc.py +++ b/aiohttp/abc.py @@ -31,7 +31,7 @@ def expect_handler(self, request): def http_exception(self): """HTTPException instance raised on router's resolving, or None""" - @abstractmethod + @abstractmethod # pragma: no branch def get_info(self): """Return a dict with additional info useful for introspection""" @@ -45,13 +45,26 @@ def __init__(self, request): def request(self): return self._request - @asyncio.coroutine + @asyncio.coroutine # pragma: no branch @abstractmethod def __iter__(self): while False: # pragma: no cover yield None - if PY_35: + if PY_35: # pragma: no branch @abstractmethod def __await__(self): - return + return # pragma: no cover + + +class AbstractResolver(ABC): + + @asyncio.coroutine # pragma: no branch + @abstractmethod + def resolve(self, hostname): + """Return IP address for given hostname""" + + @asyncio.coroutine # pragma: no branch + @abstractmethod + def close(self): + """Release resolver""" diff --git a/aiohttp/client.py b/aiohttp/client.py index a4a4a54ca30..f8502242423 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -9,11 +9,12 @@ import warnings import urllib.parse +from multidict import MultiDictProxy, MultiDict, CIMultiDict, upstr + import aiohttp from .client_reqrep import ClientRequest, ClientResponse from .errors import WSServerHandshakeError from .helpers import CookieJar -from .multidict import MultiDictProxy, MultiDict, CIMultiDict, upstr from .websocket import WS_KEY, WebSocketParser, WebSocketWriter from .websocket_client import ClientWebSocketResponse from . import hdrs diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 8b8d6a096ca..3dacbecf3dc 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -15,12 +15,13 @@ except ImportError: import chardet +from multidict import (CIMultiDictProxy, MultiDictProxy, MultiDict, + CIMultiDict) + import aiohttp from . import hdrs, helpers, streams from .log import client_logger from .streams import EOF_MARKER, FlowControlStreamReader -from .multidict import (CIMultiDictProxy, MultiDictProxy, MultiDict, - CIMultiDict) from .multipart import MultipartWriter from .protocol import HttpMessage diff --git a/aiohttp/connector.py b/aiohttp/connector.py index d13e8f28ae3..c261037f7db 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -3,7 +3,6 @@ import functools import http.cookies import ssl -import socket import sys import traceback import warnings @@ -21,6 +20,7 @@ from .errors import ClientOSError, ClientTimeoutError from .errors import FingerprintMismatch from .helpers import BasicAuth +from .resolver import DefaultResolver __all__ = ('BaseConnector', 'TCPConnector', 'ProxyConnector', 'UnixConnector') @@ -403,7 +403,11 @@ class TCPConnector(BaseConnector): digest of the expected certificate in DER format to verify that the certificate the server presents matches. See also https://en.wikipedia.org/wiki/Transport_Layer_Security#Certificate_pinning - :param bool resolve: Set to True to do DNS lookup for host name. + :param bool resolve: (Deprecated) Set to True to do DNS lookup for + host name. + :param AbstractResolver resolver: Enable DNS lookups and use this + resolver + :param bool use_dns_cache: Use memory cache for DNS lookups. :param family: socket address family :param local_addr: local :class:`tuple` of (host, port) to bind socket to :param args: see :class:`BaseConnector` @@ -412,7 +416,7 @@ class TCPConnector(BaseConnector): def __init__(self, *, verify_ssl=True, fingerprint=None, resolve=_marker, use_dns_cache=_marker, - family=0, ssl_context=None, local_addr=None, + family=0, ssl_context=None, local_addr=None, resolver=None, **kwargs): super().__init__(**kwargs) @@ -447,6 +451,13 @@ def __init__(self, *, verify_ssl=True, fingerprint=None, else: _use_dns_cache = False + self._resolver = resolver or DefaultResolver(loop=self._loop) + + if _use_dns_cache or resolver: + self._use_resolver = True + else: + self._use_resolver = False + self._use_dns_cache = _use_dns_cache self._cached_hosts = {} self._ssl_context = ssl_context @@ -534,26 +545,24 @@ def clear_resolved_hosts(self, host=None, port=None): @asyncio.coroutine def _resolve_host(self, host, port): + if not self._use_resolver: + return [{'hostname': host, 'host': host, 'port': port, + 'family': self._family, 'proto': 0, 'flags': 0}] + + assert self._resolver + if self._use_dns_cache: key = (host, port) if key not in self._cached_hosts: - infos = yield from self._loop.getaddrinfo( - host, port, type=socket.SOCK_STREAM, family=self._family) - - hosts = [] - for family, _, proto, _, address in infos: - hosts.append( - {'hostname': host, - 'host': address[0], 'port': address[1], - 'family': family, 'proto': proto, - 'flags': socket.AI_NUMERICHOST}) - self._cached_hosts[key] = hosts - - return list(self._cached_hosts[key]) + self._cached_hosts[key] = yield from \ + self._resolver.resolve(host, port, family=self._family) + + return self._cached_hosts[key] else: - return [{'hostname': host, 'host': host, 'port': port, - 'family': self._family, 'proto': 0, 'flags': 0}] + res = yield from self._resolver.resolve( + host, port, family=self._family) + return res @asyncio.coroutine def _create_connection(self, req): diff --git a/aiohttp/hdrs.py b/aiohttp/hdrs.py index 06c7936bbcc..cfccd3df715 100644 --- a/aiohttp/hdrs.py +++ b/aiohttp/hdrs.py @@ -1,5 +1,5 @@ """HTTP Headers constants.""" -from .multidict import upstr +from multidict import upstr METH_ANY = upstr('*') METH_CONNECT = upstr('CONNECT') diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index a0d2d074846..e44d3b489ee 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -13,7 +13,9 @@ from collections import namedtuple from pathlib import Path -from . import hdrs, multidict +import multidict + +from . import hdrs from .errors import InvalidURL try: @@ -470,7 +472,7 @@ class Timeout: ... await r.text() - :param timeout: timeout value in seconds + :param timeout: timeout value in seconds or None to disable timeout logic :param loop: asyncio compatible event loop """ def __init__(self, timeout, *, loop=None): @@ -487,8 +489,9 @@ def __enter__(self): if self._task is None: raise RuntimeError('Timeout context manager should be used ' 'inside a task') - self._cancel_handler = self._loop.call_later( - self._timeout, self._cancel_task) + if self._timeout is not None: + self._cancel_handler = self._loop.call_later( + self._timeout, self._cancel_task) return self def __exit__(self, exc_type, exc_val, exc_tb): @@ -496,8 +499,9 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._cancel_handler = None self._task = None raise asyncio.TimeoutError - self._cancel_handler.cancel() - self._cancel_handler = None + if self._timeout is not None: + self._cancel_handler.cancel() + self._cancel_handler = None self._task = None def _cancel_task(self): diff --git a/aiohttp/multidict.py b/aiohttp/multidict.py deleted file mode 100644 index 7037aa7923a..00000000000 --- a/aiohttp/multidict.py +++ /dev/null @@ -1,385 +0,0 @@ -"""Multidict implementation. - -HTTP Headers and URL query string require specific data structure: -multidict. It behaves mostly like a dict but it can have -several values for the same key. -""" - - -from collections import abc -import os -import sys - -__all__ = ('MultiDictProxy', 'CIMultiDictProxy', - 'MultiDict', 'CIMultiDict', 'upstr') - -_marker = object() - - -class _upstr(str): - - """Case insensitive str.""" - - def __new__(cls, val='', - encoding=sys.getdefaultencoding(), errors='strict'): - if isinstance(val, (bytes, bytearray, memoryview)): - val = str(val, encoding, errors) - elif isinstance(val, str): - pass - else: - val = str(val) - val = val.upper() - return str.__new__(cls, val) - - def upper(self): - return self - - -class _Base: - - def getall(self, key, default=_marker): - """Return a list of all values matching the key.""" - res = [v for k, v in self._items if k == key] - if res: - return res - if not res and default is not _marker: - return default - raise KeyError('Key not found: %r' % key) - - def getone(self, key, default=_marker): - """Get first value matching the key.""" - for k, v in self._items: - if k == key: - return v - if default is not _marker: - return default - raise KeyError('Key not found: %r' % key) - - # Mapping interface # - - def __getitem__(self, key): - return self.getone(key, _marker) - - def get(self, key, default=None): - """Get first value matching the key. - - The method is alias for .getone(). - """ - return self.getone(key, default) - - def __iter__(self): - return iter(self.keys()) - - def __len__(self): - return len(self._items) - - def keys(self): - """Return a new view of the dictionary's keys.""" - return _KeysView(self._items) - - def items(self): - """Return a new view of the dictionary's items *(key, value) pairs).""" - return _ItemsView(self._items) - - def values(self): - """Return a new view of the dictionary's values.""" - return _ValuesView(self._items) - - def __eq__(self, other): - if not isinstance(other, abc.Mapping): - return NotImplemented - if isinstance(other, _Base): - return self._items == other._items - for k, v in self.items(): - nv = other.get(k, _marker) - if v != nv: - return False - return True - - def __contains__(self, key): - for k, v in self._items: - if k == key: - return True - return False - - def __repr__(self): - body = ', '.join("'{}': {!r}".format(k, v) for k, v in self.items()) - return '<{}({})>'.format(self.__class__.__name__, body) - - -class _CIBase(_Base): - - def getall(self, key, default=_marker): - """Return a list of all values matching the key.""" - return super().getall(key.upper(), default) - - def getone(self, key, default=_marker): - """Get first value matching the key.""" - return super().getone(key.upper(), default) - - def get(self, key, default=None): - """Get first value matching the key. - - The method is alias for .getone(). - """ - return super().get(key.upper(), default) - - def __getitem__(self, key): - return super().__getitem__(key.upper()) - - def __contains__(self, key): - return super().__contains__(key.upper()) - - -class _MultiDictProxy(_Base, abc.Mapping): - - def __init__(self, arg): - if not isinstance(arg, _MultiDict): - raise TypeError( - 'MultiDictProxy requires MultiDict instance, not {}'.format( - type(arg))) - - self._items = arg._items - - def copy(self): - """Return a copy of itself.""" - return _MultiDict(self.items()) - - -class _CIMultiDictProxy(_CIBase, _MultiDictProxy): - - def __init__(self, arg): - if not isinstance(arg, _CIMultiDict): - raise TypeError( - 'CIMultiDictProxy requires CIMultiDict instance, not {}' - .format(type(arg))) - - self._items = arg._items - - def copy(self): - """Return a copy of itself.""" - return _CIMultiDict(self.items()) - - -class _MultiDict(_Base, abc.MutableMapping): - - def __init__(self, *args, **kwargs): - self._items = [] - - self._extend(args, kwargs, self.__class__.__name__, self.add) - - def add(self, key, value): - """Add the key and value, not overwriting any previous value.""" - self._items.append((key, value)) - - def copy(self): - """Return a copy of itself.""" - cls = self.__class__ - return cls(self.items()) - - def extend(self, *args, **kwargs): - """Extend current MultiDict with more values. - - This method must be used instead of update. - """ - self._extend(args, kwargs, 'extend', self.add) - - def _extend(self, args, kwargs, name, method): - if len(args) > 1: - raise TypeError("{} takes at most 1 positional argument" - " ({} given)".format(name, len(args))) - if args: - arg = args[0] - if isinstance(args[0], _MultiDictProxy): - items = arg._items - elif isinstance(args[0], _MultiDict): - items = arg._items - elif hasattr(arg, 'items'): - items = arg.items() - else: - for item in arg: - if not len(item) == 2: - raise TypeError( - "{} takes either dict or list of (key, value) " - "tuples".format(name)) - items = arg - - for key, value in items: - method(key, value) - - for key, value in kwargs.items(): - method(key, value) - - def clear(self): - """Remove all items from MultiDict.""" - self._items.clear() - - # Mapping interface # - - def __setitem__(self, key, value): - self._replace(key, value) - - def __delitem__(self, key): - items = self._items - found = False - for i in range(len(items) - 1, -1, -1): - if items[i][0] == key: - del items[i] - found = True - if not found: - raise KeyError(key) - - def setdefault(self, key, default=None): - """Return value for key, set value to default if key is not present.""" - for k, v in self._items: - if k == key: - return v - self._items.append((key, default)) - return default - - def pop(self, key, default=_marker): - """Remove specified key and return the corresponding value. - - If key is not found, d is returned if given, otherwise - KeyError is raised. - - """ - value = None - found = False - for i in range(len(self._items) - 1, -1, -1): - if self._items[i][0] == key: - value = self._items[i][1] - del self._items[i] - found = True - if not found: - if default is _marker: - raise KeyError(key) - else: - return default - else: - return value - - def popitem(self): - """Remove and return an arbitrary (key, value) pair.""" - if self._items: - return self._items.pop(0) - else: - raise KeyError("empty multidict") - - def update(self, *args, **kwargs): - """Update the dictionary from *other*, overwriting existing keys.""" - self._extend(args, kwargs, 'update', self._replace) - - def _replace(self, key, value): - if key in self: - del self[key] - self.add(key, value) - - -class _CIMultiDict(_CIBase, _MultiDict): - - def add(self, key, value): - """Add the key and value, not overwriting any previous value.""" - super().add(key.upper(), value) - - def __setitem__(self, key, value): - super().__setitem__(key.upper(), value) - - def __delitem__(self, key): - super().__delitem__(key.upper()) - - def _replace(self, key, value): - super()._replace(key.upper(), value) - - def setdefault(self, key, default=None): - """Return value for key, set value to default if key is not present.""" - key = key.upper() - return super().setdefault(key, default) - - -class _ViewBase: - - def __init__(self, items): - self._items = items - - def __len__(self): - return len(self._items) - - -class _ItemsView(_ViewBase, abc.ItemsView): - - def __contains__(self, item): - assert isinstance(item, tuple) or isinstance(item, list) - assert len(item) == 2 - return item in self._items - - def __iter__(self): - yield from self._items - - def __repr__(self): - lst = [] - for item in self._items: - lst.append("{!r}: {!r}".format(item[0], item[1])) - body = ', '.join(lst) - return '{}({})'.format(self.__class__.__name__, body) - - -class _ValuesView(_ViewBase, abc.ValuesView): - - def __contains__(self, value): - for item in self._items: - if item[1] == value: - return True - return False - - def __iter__(self): - for item in self._items: - yield item[1] - - def __repr__(self): - lst = [] - for item in self._items: - lst.append("{!r}".format(item[1])) - body = ', '.join(lst) - return '{}({})'.format(self.__class__.__name__, body) - - -class _KeysView(_ViewBase, abc.KeysView): - - def __contains__(self, key): - for item in self._items: - if item[0] == key: - return True - return False - - def __iter__(self): - for item in self._items: - yield item[0] - - def __repr__(self): - lst = [] - for item in self._items: - lst.append("{!r}".format(item[0])) - body = ', '.join(lst) - return '{}({})'.format(self.__class__.__name__, body) - - -if bool(os.environ.get('AIOHTTP_NO_EXTENSIONS')): - MultiDictProxy = _MultiDictProxy - CIMultiDictProxy = _CIMultiDictProxy - MultiDict = _MultiDict - CIMultiDict = _CIMultiDict - upstr = _upstr -else: - try: - from ._multidict import (MultiDictProxy, - CIMultiDictProxy, - MultiDict, - CIMultiDict, - upstr) - except ImportError: # pragma: no cover - MultiDictProxy = _MultiDictProxy - CIMultiDictProxy = _CIMultiDictProxy - MultiDict = _MultiDict - CIMultiDict = _CIMultiDict - upstr = _upstr diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 47caad26d49..adb3d5ab4f8 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -13,8 +13,9 @@ from collections import deque, Mapping, Sequence from pathlib import Path +from multidict import CIMultiDict + from .helpers import parse_mimetype -from .multidict import CIMultiDict from .protocol import HttpParser from .hdrs import ( CONTENT_DISPOSITION, diff --git a/aiohttp/protocol.py b/aiohttp/protocol.py index 2928f90aa7c..6214d0e7e7a 100644 --- a/aiohttp/protocol.py +++ b/aiohttp/protocol.py @@ -9,10 +9,10 @@ import zlib from abc import abstractmethod, ABC from wsgiref.handlers import format_date_time +from multidict import CIMultiDict, upstr import aiohttp from . import errors, hdrs -from .multidict import CIMultiDict, upstr from .log import internal_logger from .helpers import reify @@ -286,7 +286,8 @@ def __call__(self, out, buf): length = 8 # payload decompression wrapper - if self.compression and self.message.compression: + if (self.response_with_body and + self.compression and self.message.compression): out = DeflateBuffer(out, self.message.compression) # payload parser diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py new file mode 100644 index 00000000000..b40dfa65fc4 --- /dev/null +++ b/aiohttp/resolver.py @@ -0,0 +1,74 @@ +import socket +import asyncio +from .abc import AbstractResolver + +try: + import aiodns +except ImportError: + aiodns = None + + +class DefaultResolver(AbstractResolver): + """Use Executor for synchronous getaddrinfo() calls, which defaults to + concurrent.futures.ThreadPoolExecutor. + """ + + def __init__(self, loop=None): + if loop is None: + loop = asyncio.get_event_loop() + self._loop = loop + + @asyncio.coroutine + def resolve(self, host, port=0, family=socket.AF_INET): + infos = yield from self._loop.getaddrinfo( + host, port, type=socket.SOCK_STREAM, family=family) + + hosts = [] + for family, _, proto, _, address in infos: + hosts.append( + {'hostname': host, + 'host': address[0], 'port': address[1], + 'family': family, 'proto': proto, + 'flags': socket.AI_NUMERICHOST}) + + return hosts + + @asyncio.coroutine + def close(self): + pass + + +class AsyncResolver(AbstractResolver): + """Use the `aiodns` package to make asynchronous DNS lookups""" + + def __init__(self, loop=None, *args, **kwargs): + if loop is None: + loop = asyncio.get_event_loop() + + if aiodns is None: + raise RuntimeError("Resolver requires aiodns library") + + self._loop = loop + self._resolver = aiodns.DNSResolver(*args, loop=loop, **kwargs) + + @asyncio.coroutine + def resolve(self, host, port=0, family=socket.AF_INET): + if family == socket.AF_INET6: + qtype = 'AAAA' + else: + qtype = 'A' + + hosts = [] + resp = yield from self._resolver.query(host, qtype) + + for rr in resp: + hosts.append( + {'hostname': host, + 'host': rr.host, 'port': port, + 'family': family, 'proto': 0, + 'flags': socket.AI_NUMERICHOST}) + return hosts + + @asyncio.coroutine + def close(self): + return self._resolver.cancel() diff --git a/aiohttp/web.py b/aiohttp/web.py index 27149cb2052..010f46b4899 100644 --- a/aiohttp/web.py +++ b/aiohttp/web.py @@ -65,7 +65,7 @@ def handle_request(self, message, payload): now = self._loop.time() app = self._app - request = Request( + request = web_reqrep.Request( app, message, payload, self.transport, self.reader, self.writer, secure_proxy_ssl_header=self._secure_proxy_ssl_header) @@ -89,11 +89,11 @@ def handle_request(self, message, payload): handler = yield from factory(app, handler) resp = yield from handler(request) - assert isinstance(resp, StreamResponse), \ + assert isinstance(resp, web_reqrep.StreamResponse), \ ("Handler {!r} should return response instance, " "got {!r} [middlewares {!r}]").format( match_info.handler, type(resp), self._middlewares) - except HTTPException as exc: + except web_exceptions.HTTPException as exc: resp = exc resp_msg = yield from resp.prepare(request) @@ -193,7 +193,7 @@ def __init__(self, *, logger=web_logger, loop=None, if loop is None: loop = asyncio.get_event_loop() if router is None: - router = UrlDispatcher() + router = web_urldispatcher.UrlDispatcher() assert isinstance(router, AbstractRouter), router self._debug = debug @@ -309,10 +309,9 @@ def run_app(app, *, host='0.0.0.0', port=None, ssl=ssl_context)) scheme = 'https' if ssl_context else 'http' - prompt = '127.0.0.1' if host == '0.0.0.0' else host - print("======== Running on {scheme}://{prompt}:{port}/ ========\n" + print("======== Running on {scheme}://{host}:{port}/ ========\n" "(Press CTRL+C to quit)".format( - scheme=scheme, prompt=prompt, port=port)) + scheme=scheme, host=host, port=port)) try: loop.run_forever() @@ -349,7 +348,7 @@ def main(argv): type=int, default="8080" ) - args, extra_args = arg_parser.parse_known_args(argv) + args, extra_argv = arg_parser.parse_known_args(argv) # Import logic mod_str, _, func_str = args.entry_func.partition(":") @@ -368,9 +367,9 @@ def main(argv): except AttributeError: arg_parser.error("module %r has no attribute %r" % (mod_str, func_str)) - app = func(extra_args) + app = func(extra_argv) run_app(app, host=args.hostname, port=args.port) arg_parser.exit(message="Stopped\n") if __name__ == "__main__": - main(sys.argv) + main(sys.argv[1:]) diff --git a/aiohttp/web_exceptions.py b/aiohttp/web_exceptions.py index adc8cbb3f3c..254f07319a2 100644 --- a/aiohttp/web_exceptions.py +++ b/aiohttp/web_exceptions.py @@ -45,6 +45,7 @@ 'HTTPPreconditionRequired', 'HTTPTooManyRequests', 'HTTPRequestHeaderFieldsTooLarge', + 'HTTPUnavailableForLegalReasons', 'HTTPServerError', 'HTTPInternalServerError', 'HTTPNotImplemented', @@ -286,6 +287,18 @@ class HTTPRequestHeaderFieldsTooLarge(HTTPClientError): status_code = 431 +class HTTPUnavailableForLegalReasons(HTTPClientError): + status_code = 451 + + def __init__(self, link=None, *, headers=None, reason=None, + body=None, text=None, content_type=None): + super().__init__(headers=headers, reason=reason, + body=body, text=text, content_type=content_type) + if link: + self.headers['Link'] = '<%s>; rel="blocked-by"' % link + self.link = link + + ############################################################ # 5xx Server Error ############################################################ diff --git a/aiohttp/web_reqrep.py b/aiohttp/web_reqrep.py index d661029588d..68ab7fdf5f2 100644 --- a/aiohttp/web_reqrep.py +++ b/aiohttp/web_reqrep.py @@ -16,12 +16,13 @@ from types import MappingProxyType from urllib.parse import urlsplit, parse_qsl, unquote +from multidict import (CIMultiDictProxy, + CIMultiDict, + MultiDictProxy, + MultiDict) + from . import hdrs from .helpers import reify -from .multidict import (CIMultiDictProxy, - CIMultiDict, - MultiDictProxy, - MultiDict) from .protocol import Response as ResponseImpl, HttpVersion10, HttpVersion11 from .streams import EOF_MARKER @@ -482,6 +483,10 @@ def enable_compression(self, force=None): # Backwards compatibility for when force was a bool <0.17. if type(force) == bool: force = ContentCoding.deflate if force else ContentCoding.identity + elif force is not None: + assert isinstance(force, ContentCoding), ("force should one of " + "None, bool or " + "ContentEncoding") self._compression = True self._compression_force = force diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index e7361463012..16e6e02372c 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -15,13 +15,14 @@ from urllib.parse import urlencode, unquote from types import MappingProxyType +from multidict import upstr + from . import hdrs from .abc import AbstractRouter, AbstractMatchInfo, AbstractView from .protocol import HttpVersion11 from .web_exceptions import (HTTPMethodNotAllowed, HTTPNotFound, HTTPNotModified, HTTPExpectationFailed) from .web_reqrep import StreamResponse -from .multidict import upstr __all__ = ('UrlDispatcher', 'UrlMappingMatchInfo', @@ -93,7 +94,14 @@ def __init__(self, method, handler, *, issubclass(handler, AbstractView)): pass else: - handler = asyncio.coroutine(handler) + @asyncio.coroutine + def handler_wrapper(*args, **kwargs): + result = old_handler(*args, **kwargs) + if asyncio.iscoroutine(result): + result = yield from result + return result + old_handler = handler + handler = handler_wrapper self._method = method self._handler = handler @@ -204,10 +212,11 @@ def url(self, **kwargs): @asyncio.coroutine def resolve(self, method, path): route_method = self._route.method - allowed_methods = {route_method} - if route_method == method or route_method == hdrs.METH_ANY: - match_dict = self._route.match(path) - if match_dict is not None: + allowed_methods = set() + match_dict = self._route.match(path) + if match_dict is not None: + allowed_methods.add(route_method) + if route_method == hdrs.METH_ANY or route_method == method: return (UrlMappingMatchInfo(match_dict, self._route), allowed_methods) return None, allowed_methods @@ -257,11 +266,10 @@ def resolve(self, method, path): for route in self._routes: route_method = route.method + allowed_methods.add(route_method) if route_method == method or route_method == hdrs.METH_ANY: return UrlMappingMatchInfo(match_dict, route), allowed_methods - - allowed_methods.add(route_method) else: return None, allowed_methods @@ -569,6 +577,10 @@ def handle(self, request): request.logger.exception(error) raise HTTPNotFound() from error + # Make sure that filepath is a file + if not filepath.is_file(): + raise HTTPNotFound() + st = filepath.stat() modsince = request.if_modified_since diff --git a/aiohttp/websocket.py b/aiohttp/websocket.py index 14e4542fd27..2ee0eff2144 100644 --- a/aiohttp/websocket.py +++ b/aiohttp/websocket.py @@ -33,6 +33,8 @@ CLOSE_MESSAGE_TOO_BIG = 1009 CLOSE_MANDATORY_EXTENSION = 1010 CLOSE_INTERNAL_ERROR = 1011 +CLOSE_SERVICE_RESTART = 1012 +CLOSE_TRY_AGAIN_LATER = 1013 ALLOWED_CLOSE_CODES = ( CLOSE_OK, @@ -44,6 +46,8 @@ CLOSE_MESSAGE_TOO_BIG, CLOSE_MANDATORY_EXTENSION, CLOSE_INTERNAL_ERROR, + CLOSE_SERVICE_RESTART, + CLOSE_TRY_AGAIN_LATER, ) WS_KEY = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' diff --git a/demos/chat/aiohttpdemo_chat/__init__.py b/demos/chat/aiohttpdemo_chat/__init__.py new file mode 100644 index 00000000000..b8023d8bc0c --- /dev/null +++ b/demos/chat/aiohttpdemo_chat/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.1' diff --git a/demos/chat/aiohttpdemo_chat/main.py b/demos/chat/aiohttpdemo_chat/main.py new file mode 100644 index 00000000000..6ee53282e62 --- /dev/null +++ b/demos/chat/aiohttpdemo_chat/main.py @@ -0,0 +1,40 @@ +import asyncio +import jinja2 +import logging + +from aiohttp import web +import aiohttp_jinja2 + +from aiohttpdemo_chat.views import setup as setup_routes + + +async def init(loop): + app = web.Application(loop=loop) + app['sockets'] = {} + app.on_shutdown.append(shutdown) + + aiohttp_jinja2.setup( + app, loader=jinja2.PackageLoader('aiohttpdemo_chat', 'templates')) + + setup_routes(app) + + return app + + +async def shutdown(app): + for ws in app['sockets'].values(): + await ws.close() + app['sockets'].clear() + + +def main(): + # init logging + logging.basicConfig(level=logging.DEBUG) + + loop = asyncio.get_event_loop() + app = loop.run_until_complete(init(loop)) + web.run_app(app) + + +if __name__ == '__main__': + main() diff --git a/demos/chat/aiohttpdemo_chat/templates/index.html b/demos/chat/aiohttpdemo_chat/templates/index.html new file mode 100644 index 00000000000..6b51fb63734 --- /dev/null +++ b/demos/chat/aiohttpdemo_chat/templates/index.html @@ -0,0 +1,112 @@ + + + + + + + + +

Chat!

+
+  | Status: + UNKNOWN + disconnected +
+
+
+
+ + +
+ + diff --git a/demos/chat/aiohttpdemo_chat/views.py b/demos/chat/aiohttpdemo_chat/views.py new file mode 100644 index 00000000000..f15e55027f8 --- /dev/null +++ b/demos/chat/aiohttpdemo_chat/views.py @@ -0,0 +1,52 @@ +import json +import logging +import random +import string + +from aiohttp import web +import aiohttp_jinja2 + + +log = logging.getLogger(__name__) + + +async def index(request): + resp = web.WebSocketResponse() + ok, protocol = resp.can_start(request) + if not ok: + return aiohttp_jinja2.render_template('index.html', request, {}) + + await resp.prepare(request) + name = (random.choice(string.ascii_uppercase) + + ''.join(random.sample(string.ascii_lowercase*10, 10))) + log.info('%s joined.', name) + resp.send_str(json.dumps({'action': 'connect', + 'name': name})) + for ws in request.app['sockets'].values(): + ws.send_str(json.dumps({'action': 'join', + 'name': name})) + request.app['sockets'][name] = resp + + while True: + msg = await resp.receive() + + if msg.tp == web.MsgType.text: + for ws in request.app['sockets'].values(): + if ws is not resp: + ws.send_str(json.dumps({'action': 'sent', + 'name': name, + 'text': msg.data})) + else: + break + + del request.app['sockets'][name] + log.info('%s disconnected.', name) + for ws in request.app['sockets'].values(): + ws.send_str(json.dumps({'action': 'disconnect', + 'name': name})) + return resp + + + +def setup(app): + app.router.add_route('GET', '/', index) diff --git a/demos/chat/setup.py b/demos/chat/setup.py new file mode 100644 index 00000000000..9a927835dab --- /dev/null +++ b/demos/chat/setup.py @@ -0,0 +1,32 @@ +import os +import re + +from setuptools import find_packages, setup + + +def read_version(): + regexp = re.compile(r"^__version__\W*=\W*'([\d.abrc]+)'") + init_py = os.path.join(os.path.dirname(__file__), + 'aiohttpdemo_chat', '__init__.py') + with open(init_py) as f: + for line in f: + match = regexp.match(line) + if match is not None: + return match.group(1) + else: + msg = 'Cannot find version in aiohttpdemo_chat/__init__.py' + raise RuntimeError(msg) + + +install_requires = ['aiohttp', + 'aiohttp_jinja2'] + + +setup(name='aiohttpdemo_chat', + version=read_version(), + description='Chat example from aiohttp', + platforms=['POSIX'], + packages=find_packages(), + include_package_data=True, + install_requires=install_requires, + zip_safe=False) diff --git a/demos/polls/aiohttpdemo_polls/main.py b/demos/polls/aiohttpdemo_polls/main.py index 5be71cc06d5..52f4991a3b8 100644 --- a/demos/polls/aiohttpdemo_polls/main.py +++ b/demos/polls/aiohttpdemo_polls/main.py @@ -6,20 +6,20 @@ import jinja2 from aiohttp import web +from aiohttpdemo_polls.middlewares import setup_middlewares from aiohttpdemo_polls.routes import setup_routes from aiohttpdemo_polls.utils import init_postgres, load_config from aiohttpdemo_polls.views import SiteHandler PROJ_ROOT = pathlib.Path(__file__).parent.parent -TEMPLATES_ROOT = pathlib.Path(__file__).parent / 'templates' async def init(loop): # setup application and extensions app = web.Application(loop=loop) aiohttp_jinja2.setup( - app, loader=jinja2.FileSystemLoader(str(TEMPLATES_ROOT))) + app, loader=jinja2.PackageLoader('aiohttpdemo_polls', 'templates')) # load config from yaml file conf = load_config(str(PROJ_ROOT / 'config' / 'polls.yaml')) @@ -35,6 +35,7 @@ async def close_pg(app): # setup views and routes handler = SiteHandler(pg) setup_routes(app, handler, PROJ_ROOT) + setup_middlewares(app) host, port = conf['host'], conf['port'] return app, host, port diff --git a/demos/polls/aiohttpdemo_polls/middlewares.py b/demos/polls/aiohttpdemo_polls/middlewares.py new file mode 100644 index 00000000000..a7c75406d4f --- /dev/null +++ b/demos/polls/aiohttpdemo_polls/middlewares.py @@ -0,0 +1,42 @@ +import aiohttp_jinja2 +from aiohttp import web + + +async def handle_404(request, response): + response = aiohttp_jinja2.render_template('404.html', + request, + {}) + return response + + +async def handle_500(request, response): + response = aiohttp_jinja2.render_template('500.html', + request, + {}) + return response + + +def error_pages(overrides): + async def middleware(app, handler): + async def middleware_handler(request): + try: + response = await handler(request) + override = overrides.get(response.status) + if override is None: + return response + else: + return await override(request, response) + except web.HTTPException as ex: + override = overrides.get(ex.status) + if override is None: + raise + else: + return await override(request, ex) + return middleware_handler + return middleware + + +def setup_middlewares(app): + error_middleware = error_pages({404: handle_404, + 500: handle_500}) + app.middlewares.append(error_middleware) diff --git a/demos/polls/aiohttpdemo_polls/templates/404.html b/demos/polls/aiohttpdemo_polls/templates/404.html new file mode 100644 index 00000000000..1d47f08a585 --- /dev/null +++ b/demos/polls/aiohttpdemo_polls/templates/404.html @@ -0,0 +1,3 @@ +{% extends "base.html" %} + +{% set title = "Page Not Found" %} diff --git a/demos/polls/aiohttpdemo_polls/templates/500.html b/demos/polls/aiohttpdemo_polls/templates/500.html new file mode 100644 index 00000000000..a9201ce52e3 --- /dev/null +++ b/demos/polls/aiohttpdemo_polls/templates/500.html @@ -0,0 +1,3 @@ +{% extends "base.html" %} + +{% set title = "Internal Server Error" %} diff --git a/demos/polls/aiohttpdemo_polls/templates/base.html b/demos/polls/aiohttpdemo_polls/templates/base.html new file mode 100644 index 00000000000..0a3f5cf6adb --- /dev/null +++ b/demos/polls/aiohttpdemo_polls/templates/base.html @@ -0,0 +1,17 @@ + + + + {% block head %} + + {{title}} + {% endblock %} + + +

{{title}}

+
+ {% block content %} + {% endblock %} +
+ + diff --git a/demos/polls/aiohttpdemo_polls/templates/detail.html b/demos/polls/aiohttpdemo_polls/templates/detail.html index 3b250c63e91..72e7d60e462 100644 --- a/demos/polls/aiohttpdemo_polls/templates/detail.html +++ b/demos/polls/aiohttpdemo_polls/templates/detail.html @@ -1,5 +1,8 @@ -

{{ question.question_text }}

+{% extends "base.html" %} +{% set title = question.question_text %} + +{% block content %} {% if error_message %}

{{ error_message }}

{% endif %}
@@ -9,3 +12,4 @@

{{ question.question_text }}

{% endfor %}
+{% endblock %} diff --git a/demos/polls/aiohttpdemo_polls/templates/index.html b/demos/polls/aiohttpdemo_polls/templates/index.html index 1c04ad25237..3e21f80ba22 100644 --- a/demos/polls/aiohttpdemo_polls/templates/index.html +++ b/demos/polls/aiohttpdemo_polls/templates/index.html @@ -1,6 +1,8 @@ - +{% extends "base.html" %} +{% set title = "Main" %} + +{% block content %} {% if questions %}
    {% for question in questions %} @@ -10,3 +12,4 @@ {% else %}

    No polls are available.

    {% endif %} +{% endblock %} diff --git a/demos/polls/aiohttpdemo_polls/templates/results.html b/demos/polls/aiohttpdemo_polls/templates/results.html index dca55753c42..40304d25574 100644 --- a/demos/polls/aiohttpdemo_polls/templates/results.html +++ b/demos/polls/aiohttpdemo_polls/templates/results.html @@ -1,5 +1,8 @@ -

    {{ question.question }}

    +{% extends "base.html" %} +{% set title = question.question %} + +{% block content %}
      {% for choice in choices %}
    • {{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes }}
    • @@ -7,3 +10,4 @@

      {{ question.question }}

    Vote again? +{% endblock %} diff --git a/demos/polls/setup.py b/demos/polls/setup.py index 11a38bd0161..e5a57736ce5 100644 --- a/demos/polls/setup.py +++ b/demos/polls/setup.py @@ -14,7 +14,7 @@ def read_version(): if match is not None: return match.group(1) else: - msg = 'Cannot find version in aiohttp_polls/__init__.py' + msg = 'Cannot find version in aiohttpdemo_polls/__init__.py' raise RuntimeError(msg) diff --git a/docs/aiohttp_doctools.py b/docs/aiohttp_doctools.py deleted file mode 100644 index fb21066a722..00000000000 --- a/docs/aiohttp_doctools.py +++ /dev/null @@ -1,27 +0,0 @@ -from sphinx.domains.python import PyModulelevel, PyClassmember -from sphinx import addnodes - - -class PyCoroutineMixin(object): - def handle_signature(self, sig, signode): - ret = super(PyCoroutineMixin, self).handle_signature(sig, signode) - signode.insert(0, addnodes.desc_annotation('coroutine ', 'coroutine ')) - return ret - - -class PyCoroutineFunction(PyCoroutineMixin, PyModulelevel): - def run(self): - self.name = 'py:function' - return PyModulelevel.run(self) - - -class PyCoroutineMethod(PyCoroutineMixin, PyClassmember): - def run(self): - self.name = 'py:method' - return PyClassmember.run(self) - - -def setup(app): - app.add_directive_to_domain('py', 'coroutinefunction', PyCoroutineFunction) - app.add_directive_to_domain('py', 'coroutinemethod', PyCoroutineMethod) - return {'version': '1.0', 'parallel_read_safe': True} diff --git a/docs/client.rst b/docs/client.rst index 808f95a36d0..eb57625baf6 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -18,7 +18,7 @@ Begin by importing the aiohttp module:: Now, let's try to get a web-page. For example let's get GitHub's public time-line :: - with aiohttp.ClientSession() as session: + async with aiohttp.ClientSession() as session: async with session.get('https://api.github.com/events') as resp: print(resp.status) print(await resp.text()) @@ -200,14 +200,18 @@ Custom Cookies -------------- To send your own cookies to the server, you can use the *cookies* -parameter:: +parameter of :class:`ClientSession` constructor:: url = 'http://httpbin.org/cookies' - cookies = dict(cookies_are='working') + async with ClientSession({'cookies_are': 'working'}) as session: + async with session.get(url) as resp: + assert await resp.json() == {"cookies": + {"cookies_are": "working"}} - async with session.get(url, cookies=cookies) as resp: - assert await resp.json() == {"cookies": - {"cookies_are": "working"}} +.. note:: + ``httpbin.org/cookies`` endpoint returns request cookies + in JSON-encoded body. + To access session cookies see :attr:`ClientSession.cookies`. More complicated POST requests @@ -362,20 +366,22 @@ Keep-Alive, connection pooling and cookie sharing :class:`~aiohttp.ClientSession` may be used for sharing cookies between multiple requests:: - session = aiohttp.ClientSession() - await session.post( - 'http://httpbin.org/cookies/set/my_cookie/my_value') - async with session.get('http://httpbin.org/cookies') as r: - json = await r.json() - assert json['cookies']['my_cookie'] == 'my_value' + async with aiohttp.ClientSession() as session: + await session.get( + 'http://httpbin.org/cookies/set?my_cookie=my_value') + assert session.cookies['my_cookie'].value == 'my_value' + async with session.get('http://httpbin.org/cookies') as r: + json_body = await r.json() + assert json_body['cookies']['my_cookie'] == 'my_value' You also can set default headers for all session requests:: - session = aiohttp.ClientSession( - headers={"Authorization": "Basic bG9naW46cGFzcw=="}) - async with s.get("http://httpbin.org/headers") as r: - json = yield from r.json() - assert json['headers']['Authorization'] == 'Basic bG9naW46cGFzcw==' + async with aiohttp.ClientSession( + headers={"Authorization": "Basic bG9naW46cGFzcw=="}) as session: + async with session.get("http://httpbin.org/headers") as r: + json_body = await r.json() + assert json_body['headers']['Authorization'] == \ + 'Basic bG9naW46cGFzcw==' :class:`~aiohttp.ClientSession` supports keep-alive requests and connection pooling out-of-the-box. @@ -408,6 +414,19 @@ parameter to *connector*:: The example limits amount of parallel connections to `30`. +Resolving using custom nameservers +---------------------------------- + +In order to specify the nameservers to when resolving the hostnames, +aiodns is required. + + from aiohttp.resolver import AsyncResolver + + + resolver = AsyncResolver(nameservers=["8.8.8.8", "8.8.4.4"]) + conn = aiohttp.TCPConnector(resolver=resolver) + + SSL control for TCP sockets --------------------------- diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 36a38a7520f..8da3a94d996 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -119,13 +119,15 @@ The client session supports the context manager protocol for self closing. forbidden, but you may modify the object in-place if needed. - .. coroutinemethod:: request(method, url, *, params=None, data=None,\ - headers=None, skip_auto_headers=None, \ - auth=None, allow_redirects=True,\ - max_redirects=10, encoding='utf-8',\ - version=HttpVersion(major=1, minor=1),\ - compress=None, chunked=None, expect100=False,\ - read_until_eof=True) + .. comethod:: request(method, url, *, params=None, data=None,\ + headers=None, skip_auto_headers=None, \ + auth=None, allow_redirects=True,\ + max_redirects=10, encoding='utf-8',\ + version=HttpVersion(major=1, minor=1),\ + compress=None, chunked=None, expect100=False,\ + read_until_eof=True) + :async-with: + :coroutine: Performs an asynchronous HTTP request. Returns a response object. @@ -191,7 +193,9 @@ The client session supports the context manager protocol for self closing. :return ClientResponse: a :class:`client response ` object. - .. coroutinemethod:: get(url, *, allow_redirects=True, **kwargs) + .. comethod:: get(url, *, allow_redirects=True, **kwargs) + :async-with: + :coroutine: Perform a ``GET`` request. @@ -207,7 +211,9 @@ The client session supports the context manager protocol for self closing. :return ClientResponse: a :class:`client response ` object. - .. coroutinemethod:: post(url, *, data=None, **kwargs) + .. comethod:: post(url, *, data=None, **kwargs) + :async-with: + :coroutine: Perform a ``POST`` request. @@ -224,7 +230,9 @@ The client session supports the context manager protocol for self closing. :return ClientResponse: a :class:`client response ` object. - .. coroutinemethod:: put(url, *, data=None, **kwargs) + .. comethod:: put(url, *, data=None, **kwargs) + :async-with: + :coroutine: Perform a ``PUT`` request. @@ -241,7 +249,9 @@ The client session supports the context manager protocol for self closing. :return ClientResponse: a :class:`client response ` object. - .. coroutinemethod:: delete(url, **kwargs) + .. comethod:: delete(url, **kwargs) + :async-with: + :coroutine: Perform a ``DELETE`` request. @@ -254,7 +264,9 @@ The client session supports the context manager protocol for self closing. :return ClientResponse: a :class:`client response ` object. - .. coroutinemethod:: head(url, *, allow_redirects=False, **kwargs) + .. comethod:: head(url, *, allow_redirects=False, **kwargs) + :async-with: + :coroutine: Perform a ``HEAD`` request. @@ -270,7 +282,9 @@ The client session supports the context manager protocol for self closing. :return ClientResponse: a :class:`client response ` object. - .. coroutinemethod:: options(url, *, allow_redirects=True, **kwargs) + .. comethod:: options(url, *, allow_redirects=True, **kwargs) + :async-with: + :coroutine: Perform an ``OPTIONS`` request. @@ -287,7 +301,9 @@ The client session supports the context manager protocol for self closing. :return ClientResponse: a :class:`client response ` object. - .. coroutinemethod:: patch(url, *, data=None, **kwargs) + .. comethod:: patch(url, *, data=None, **kwargs) + :async-with: + :coroutine: Perform a ``PATCH`` request. @@ -304,11 +320,13 @@ The client session supports the context manager protocol for self closing. :return ClientResponse: a :class:`client response ` object. - .. coroutinemethod:: ws_connect(url, *, protocols=(), timeout=10.0,\ - auth=None,\ - autoclose=True,\ - autoping=True,\ - origin=None) + .. comethod:: ws_connect(url, *, protocols=(), timeout=10.0,\ + auth=None,\ + autoclose=True,\ + autoping=True,\ + origin=None) + :async-with: + :coroutine: Create a websocket connection. Returns a :class:`ClientWebSocketResponse` object. @@ -343,7 +361,7 @@ The client session supports the context manager protocol for self closing. Add *origin* parameter. - .. coroutinemethod:: close() + .. comethod:: close() Close underlying connector. @@ -715,7 +733,7 @@ BaseConnector .. versionadded:: 0.16 - .. coroutinemethod:: close() + .. comethod:: close() Close all opened connections. @@ -725,7 +743,7 @@ BaseConnector returns a future for keeping backward compatibility during transition period). - .. coroutinemethod:: connect(request) + .. comethod:: connect(request) Get a free connection from pool or create new one if connection is absent in the pool. @@ -739,7 +757,7 @@ BaseConnector :return: :class:`Connection` object. - .. coroutinemethod:: _create_connection(req) + .. comethod:: _create_connection(req) Abstract method for actual connection establishing, should be overridden in subclasses. @@ -788,6 +806,15 @@ TCPConnector .. versionadded:: 0.17 + :param aiohttp.abc.AbstractResolver resolver: Custom resolver instance to use. + ``aiohttp.resolver.DefaultResolver`` by default. + + Custom resolvers allow to resolve hostnames differently than the way the + host is configured. Alternate resolvers include aiodns, which does not rely + on a thread executor. + + .. versionadded:: 0.22 + :param bool resolve: alias for *use_dns_cache* parameter. .. deprecated:: 0.17 @@ -1102,7 +1129,7 @@ Response object For :term:`keep-alive` support see :meth:`release`. - .. coroutinemethod:: read() + .. comethod:: read() Read the whole response's body as :class:`bytes`. @@ -1113,13 +1140,13 @@ Response object .. seealso:: :meth:`close`, :meth:`release`. - .. coroutinemethod:: release() + .. comethod:: release() Finish response processing, release underlying connection and return it into free connection pool for re-usage by next upcoming request. - .. coroutinemethod:: text(encoding=None) + .. comethod:: text(encoding=None) Read response's body and return decoded :class:`str` using specified *encoding* parameter. @@ -1137,7 +1164,7 @@ Response object :return str: decoded *BODY* - .. coroutinemethod:: json(encoding=None, loads=json.loads) + .. comethod:: json(encoding=None, loads=json.loads) Read response's body as *JSON*, return :class:`dict` using specified *encoding* and *loader*. @@ -1213,7 +1240,7 @@ manually. :raise TypeError: if data is not :class:`bytes`, :class:`bytearray` or :class:`memoryview`. - .. coroutinemethod:: close(*, code=1000, message=b'') + .. comethod:: close(*, code=1000, message=b'') A :ref:`coroutine` that initiates closing handshake by sending :const:`~aiohttp.websocket.MSG_CLOSE` message. It waits for @@ -1226,7 +1253,7 @@ manually. :class:`str` (converted to *UTF-8* encoded bytes) or :class:`bytes`. - .. coroutinemethod:: receive() + .. comethod:: receive() A :ref:`coroutine` that waits upcoming *data* message from peer and returns it. diff --git a/docs/conf.py b/docs/conf.py index 0da9558c1ac..757a7e609a3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,13 +54,22 @@ 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', 'alabaster', - 'aiohttp_doctools', - 'sphinxcontrib.spelling', + 'sphinxcontrib.asyncio', 'sphinxcontrib.newsfeed', ] + +try: + import sphinxcontrib.spelling # noqa + extensions.append('sphinxcontrib.spelling') +except ImportError: + pass + + intersphinx_mapping = { 'python': ('http://docs.python.org/3', None), + 'multidict': + ('http://multidict.readthedocs.org/en/stable/', None), 'aiohttpjinja2': ('http://aiohttp-jinja2.readthedocs.org/en/stable/', None), 'aiohttpsession': diff --git a/docs/faq.rst b/docs/faq.rst index 0ed6803e230..04b14eacf5e 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -44,7 +44,7 @@ following example:: async def init_app(loop): app = Application(loop=loop) - db = await crate_connection(user='user', password='123') + db = await create_connection(user='user', password='123') app['db'] = db app.router.add_route('GET', '/', go) return app @@ -76,7 +76,7 @@ as :class:`aiohttp.web.Application`. Just put data inside *request*:: async def handler(request): - requset['unique_key'] = data + request['unique_key'] = data See https://github.com/aio-libs/aiohttp_session code for inspiration, ``aiohttp_session.get_session(request)`` method uses ``SESSION_KEY`` diff --git a/docs/index.rst b/docs/index.rst index 805cc73e472..fb47cd0bca9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -146,7 +146,6 @@ Contents web_reference web_abc server - multidict multipart api logging diff --git a/docs/multidict.rst b/docs/multidict.rst deleted file mode 100644 index 393a95ae7d9..00000000000 --- a/docs/multidict.rst +++ /dev/null @@ -1,375 +0,0 @@ -.. _aiohttp-multidict: - -============ - Multidicts -============ - -.. module:: aiohttp - - -*HTTP Headers* and *URL query string* require specific data structure: -*multidict*. It behaves mostly like a :class:`dict` but it can have -several *values* for the same *key*. - -:mod:`aiohttp` has four multidict classes: -:class:`MultiDict`, :class:`MultiDictProxy`, :class:`CIMultiDict` -and :class:`CIMultiDictProxy`. - -Immutable proxies (:class:`MultiDictProxy` and -:class:`CIMultiDictProxy`) provide a dynamic view on the -proxied multidict, the view reflects the multidict changes. They -implement the :class:`~collections.abc.Mapping` interface. - -Regular mutable (:class:`MultiDict` and :class:`CIMultiDict`) classes -implement :class:`~collections.abc.MutableMapping` and allows to change -their own content. - - -*Case insensitive* (:class:`CIMultiDict` and -:class:`CIMultiDictProxy`) ones assumes the *keys* are case -insensitive, e.g.:: - - >>> dct = CIMultiDict(a='val') - >>> 'A' in dct - True - >>> dct['A'] - 'val' - -*Keys* should be a :class:`str`. - - -MultiDict -========= - -.. class:: MultiDict(**kwargs) - MultiDict(mapping, **kwargs) - MultiDict(iterable, **kwargs) - - Creates a mutable multidict instance. - - Accepted parameters are the same as for :class:`dict`. - - If the same key appears several times it will be added, e.g.:: - - >>> d = MultiDict([('a', 1), ('b', 2), ('a', 3)]) - >>> d - - - .. method:: len(d) - - Return the number of items in multidict *d*. - - .. method:: d[key] - - Return the **first** item of *d* with key *key*. - - Raises a :exc:`KeyError` if key is not in the multidict. - - .. method:: d[key] = value - - Set ``d[key]`` to *value*. - - Replace all items where key is equal to *key* with single item - ``(key, value)``. - - .. method:: del d[key] - - Remove all items where key is equal to *key* from *d*. - Raises a :exc:`KeyError` if *key* is not in the map. - - .. method:: key in d - - Return ``True`` if d has a key *key*, else ``False``. - - .. method:: key not in d - - Equivalent to ``not (key in d)`` - - .. method:: iter(d) - - Return an iterator over the keys of the dictionary. - This is a shortcut for ``iter(d.keys())``. - - .. method:: add(key, value) - - Append ``(key, value)`` pair to the dictionary. - - .. method:: clear() - - Remove all items from the dictionary. - - .. method:: copy() - - Return a shallow copy of the dictionary. - - .. method:: extend([other]) - - Extend the dictionary with the key/value pairs from *other*, - overwriting existing keys. - Return ``None``. - - :meth:`extend` accepts either another dictionary object or an - iterable of key/value pairs (as tuples or other iterables of - length two). If keyword arguments are specified, the dictionary - is then extended with those key/value pairs: - ``d.extend(red=1, blue=2)``. - - .. method:: getone(key[, default]) - - Return the **first** value for *key* if *key* is in the - dictionary, else *default*. - - Raises :exc:`KeyError` if *default* is not given and *key* is not found. - - ``d[key]`` is equivalent to ``d.getone(key)``. - - .. method:: getall(key[, default]) - - Return a list of all values for *key* if *key* is in the - dictionary, else *default*. - - Raises :exc:`KeyError` if *default* is not given and *key* is not found. - - .. method:: get(key[, default]) - - Return the **first** value for *key* if *key* is in the - dictionary, else *default*. - - If *default* is not given, it defaults to ``None``, so that this - method never raises a :exc:`KeyError`. - - ``d.get(key)`` is equivalent to ``d.getone(key, None)``. - - .. method:: keys() - - Return a new view of the dictionary's keys. - - View contains all keys, possibly with duplicates. - - .. method:: items() - - Return a new view of the dictionary's items (``(key, value)`` pairs). - - View contains all items, multiple items can have the same key. - - .. method:: values() - - Return a new view of the dictionary's values. - - View contains all values. - - .. method:: pop(key[, default]) - - If *key* is in the dictionary, remove it and return its the - **first** value, else return *default*. - - If *default* is not given and *key* is not in the dictionary, a - :exc:`KeyError` is raised. - - - .. method:: popitem() - - Remove and return an arbitrary ``(key, value)`` pair from the dictionary. - - :meth:`popitem` is useful to destructively iterate over a - dictionary, as often used in set algorithms. - - If the dictionary is empty, calling :meth:`popitem` raises a - :exc:`KeyError`. - - .. method:: setdefault(key[, default]) - - If *key* is in the dictionary, return its the **first** value. - If not, insert *key* with a value of *default* and return *default*. - *default* defaults to ``None``. - - .. method:: update([other]) - - Update the dictionary with the key/value pairs from *other*, - overwriting existing keys. - - Return ``None``. - - :meth:`update` accepts either another dictionary object or an - iterable of key/value pairs (as tuples or other iterables - of length two). If keyword arguments are specified, the - dictionary is then updated with those key/value pairs: - ``d.update(red=1, blue=2)``. - - .. seealso:: - - :class:`MultiDictProxy` can be used to create a read-only view - of a :class:`MultiDict`. - - -CIMultiDict -=========== - - -.. class:: CIMultiDict(**kwargs) - CIMultiDict(mapping, **kwargs) - CIMultiDict(iterable, **kwargs) - - Create a case insensitive multidict instance. - - The behavior is the same as of :class:`MultiDict` but key - comparisons are case insensitive, e.g.:: - - >>> dct = CIMultiDict(a='val') - >>> 'A' in dct - True - >>> dct['A'] - 'val' - >>> dct['a'] - 'val' - >>> dct['b'] = 'new val' - >>> dct['B'] - 'new val' - - The class is inherited from :class:`MultiDict`. - - .. seealso:: - - :class:`CIMultiDictProxy` can be used to create a read-only view - of a :class:`CIMultiDict`. - - -MultiDictProxy -============== - -.. class:: MultiDictProxy(multidict) - - Create an immutable multidict proxy. - - It provides a dynamic view on - the multidict’s entries, which means that when the multidict changes, - the view reflects these changes. - - Raises :exc:`TypeError` is *multidict* is not :class:`MultiDict` instance. - - .. method:: len(d) - - Return number of items in multidict *d*. - - .. method:: d[key] - - Return the **first** item of *d* with key *key*. - - Raises a :exc:`KeyError` if key is not in the multidict. - - .. method:: key in d - - Return ``True`` if d has a key *key*, else ``False``. - - .. method:: key not in d - - Equivalent to ``not (key in d)`` - - .. method:: iter(d) - - Return an iterator over the keys of the dictionary. - This is a shortcut for ``iter(d.keys())``. - - .. method:: copy() - - Return a shallow copy of the underlying multidict. - - .. method:: getone(key[, default]) - - Return the **first** value for *key* if *key* is in the - dictionary, else *default*. - - Raises :exc:`KeyError` if *default* is not given and *key* is not found. - - ``d[key]`` is equivalent to ``d.getone(key)``. - - .. method:: getall(key[, default]) - - Return a list of all values for *key* if *key* is in the - dictionary, else *default*. - - Raises :exc:`KeyError` if *default* is not given and *key* is not found. - - .. method:: get(key[, default]) - - Return the **first** value for *key* if *key* is in the - dictionary, else *default*. - - If *default* is not given, it defaults to ``None``, so that this - method never raises a :exc:`KeyError`. - - ``d.get(key)`` is equivalent to ``d.getone(key, None)``. - - .. method:: keys() - - Return a new view of the dictionary's keys. - - View contains all keys, possibly with duplicates. - - .. method:: items() - - Return a new view of the dictionary's items (``(key, value)`` pairs). - - View contains all items, multiple items can have the same key. - - .. method:: values() - - Return a new view of the dictionary's values. - - View contains all values. - -CIMultiDictProxy -================ - -.. class:: CIMultiDictProxy(multidict) - - Case insensitive version of :class:`MultiDictProxy`. - - Raises :exc:`TypeError` is *multidict* is not :class:`CIMultiDict` instance. - - The class is inherited from :class:`MultiDict`. - - -upstr -===== - -:class:`CIMultiDict` accepts :class:`str` as *key* argument for dict -lookups but converts it to upper case internally. - -For more effective processing it should know if the *key* is already upper cased. - -To skip the :meth:`~str.upper()` call you may want to create upper cased strings by -hand, e.g:: - - >>> key = upstr('Key') - >>> key - 'KEY' - >>> mdict = CIMultiDict(key='value') - >>> key in mdict - True - >>> mdict[key] - 'value' - -For performance you should create :class:`upstr` strings once and -store them globally, like :mod:`aiohttp.hdrs` does. - -.. class:: upstr(object='') - upstr(bytes_or_buffer[, encoding[, errors]]) - - Create a new **upper cased** string object from the given - *object*. If *encoding* or *errors* are specified, then the - object must expose a data buffer that will be decoded using the - given encoding and error handler. - - Otherwise, returns the result of ``object.__str__()`` (if defined) - or ``repr(object)``. - - *encoding* defaults to ``sys.getdefaultencoding()``. - - *errors* defaults to ``'strict'``. - - The class is inherited from :class:`str` and has all regular - string methods. - - -.. disqus:: diff --git a/docs/multipart.rst b/docs/multipart.rst index b0aba32b7a6..8fbc55a44bc 100644 --- a/docs/multipart.rst +++ b/docs/multipart.rst @@ -51,7 +51,7 @@ returned - that's the signal to break the loop:: Both :class:`BodyPartReader` and :class:`MultipartReader` provides access to body part headers: this allows you to filter parts by their attributes:: - if part.headers[aiohttp.hdrs.CONTENT-TYPE] == 'application/json': + if part.headers[aiohttp.hdrs.CONTENT_TYPE] == 'application/json': metadata = await part.json() continue diff --git a/docs/web.rst b/docs/web.rst index cbf877f7c6c..8de26a1bca1 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -52,13 +52,13 @@ Command Line Interface (CLI) :mod:`aiohttp.web` implements a basic CLI for quickly serving an :class:`Application` in *development* over TCP/IP:: - $ python -m aiohttp.web -H localhost -P 8080 package.module.init_func + $ python -m aiohttp.web -H localhost -P 8080 package.module:init_func ``package.module.init_func`` should be an importable :term:`callable` that accepts a list of any non-parsed command-line arguments and returns an :class:`Application` instance after setting it up:: - def init_function(args): + def init_function(argv): app = web.Application() app.router.add_route("GET", "/", index_handler) return app @@ -152,7 +152,7 @@ that *part*. This is done by looking up the ``identifier`` in the return web.Response( text="Hello, {}".format(request.match_info['name'])) - resource = app.router.add_route('/{name}') + resource = app.router.add_resource('/{name}') resource.add_route('GET', variable_handler) By default, each *part* matches the regular expression ``[^{}/]+``. @@ -408,7 +408,7 @@ third-party library, :mod:`aiohttp_session`, that adds *session* support:: "100-continue". It is possible to specify custom *Expect* header handler on per route basis. This handler gets called if *Expect* header exist in request after receiving all headers and before -processing application middlewares :ref:`aiohttp-web-middlewares` and +processing application's :ref:`aiohttp-web-middlewares` and route handler. Handler can return *None*, in that case the request processing continues as usual. If handler returns an instance of class :class:`StreamResponse`, *request handler* uses it as response. Also @@ -529,8 +529,10 @@ with the peer:: return ws Reading from the *WebSocket* (``await ws.receive()``) **must only** be -done inside the request handler coroutine; however, writing -(``ws.send_str(...)``) to the *WebSocket* may be delegated to other coroutines. +done inside the request handler *task*; however, writing +(``ws.send_str(...)``) to the *WebSocket* may be delegated to other tasks. +*aiohttp.web* creates an implicit :class:`asyncio.Task` for handling every +incoming request. .. note:: @@ -617,6 +619,7 @@ HTTP Exception hierarchy chart:: * 428 - HTTPPreconditionRequired * 429 - HTTPTooManyRequests * 431 - HTTPRequestHeaderFieldsTooLarge + * 451 - HTTPUnavailableForLegalReasons HTTPServerError * 500 - HTTPInternalServerError * 501 - HTTPNotImplemented diff --git a/docs/web_abc.rst b/docs/web_abc.rst index ccbb1d1c85c..9346031b8e1 100644 --- a/docs/web_abc.rst +++ b/docs/web_abc.rst @@ -56,7 +56,7 @@ Not Allowed*. :meth:`AbstractMatchInfo.handler` raises .. class:: AbstractMatchInfo - Abstract *math info*, returned by :meth:`AbstractRouter` call. + Abstract *match info*, returned by :meth:`AbstractRouter` call. .. attribute:: http_exception diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 1a1bb24abcc..7219e5673e5 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -1132,11 +1132,11 @@ RequestHandlerFactory RequestHandlerFactory is responsible for creating HTTP protocol objects that can handle HTTP connections. - .. attribute:: connections + .. attribute:: RequestHandlerFactory.connections List of all currently opened connections. - .. method:: finish_connections(timeout) + .. coroutinemethod:: RequestHandlerFactory.finish_connections(timeout) A :ref:`coroutine` that should be called to close all opened connections. @@ -1438,6 +1438,7 @@ Resource classes hierarchy:: ``(method, path)`` combination. :param str method: requested HTTP method. + :param str path: *path* part of request. :return: (*match_info*, *allowed_methods*) pair. @@ -1477,8 +1478,6 @@ Resource classes hierarchy:: The method should be unique for resource. - :param str path: route path. Should be started with slash (``'/'``). - :param callable handler: route handler. :param coroutine expect_handler: optional *expect* header handler. diff --git a/examples/web_app.py b/examples/cli_app.py similarity index 84% rename from examples/web_app.py rename to examples/cli_app.py index 8eeebaf5cd5..a853e9c6840 100644 --- a/examples/web_app.py +++ b/examples/cli_app.py @@ -3,13 +3,13 @@ Serve this app using:: - $ python -m aiohttp.web -H localhost -P 8080 --repeat 10 web_app.init \ + $ python -m aiohttp.web -H localhost -P 8080 --repeat 10 cli_app:init \ > "Hello World" Here ``--repeat`` & ``"Hello World"`` are application specific command-line arguments. `aiohttp.web` only parses & consumes the command-line arguments it needs (i.e. ``-H``, ``-P`` & ``entry-func``) and passes on any additional -arguments to the `web_app.init` function for processing. +arguments to the `cli_app:init` function for processing. """ from aiohttp.web import Application, Response @@ -22,7 +22,7 @@ def display_message(req): return Response(text=text) -def init(args): +def init(argv): arg_parser = ArgumentParser( prog="aiohttp.web ...", description="Application CLI", add_help=False ) @@ -45,10 +45,10 @@ def init(args): help="show this message and exit", action="help" ) - parsed_args = arg_parser.parse_args(args) + args = arg_parser.parse_args(argv) app = Application() - app["args"] = parsed_args + app["args"] = args app.router.add_route('GET', '/', display_message) return app diff --git a/requirements-ci.txt b/requirements-ci.txt index 1aa7bde71cc..8fbfb0e3dd0 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -12,4 +12,5 @@ pytest pytest-cov gunicorn pygments>=2.1 +#aiodns # Enable from .travis.yml as required c-ares will not build on windows -e . diff --git a/requirements-dev.txt b/requirements-dev.txt index 6bf6272ef0d..7463b2023a2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,3 +4,4 @@ pytest-sugar ipython pyenchant sphinxcontrib-spelling +sphinxcontrib-asyncio diff --git a/setup.py b/setup.py index 4beabf838f5..27ebc0ae387 100644 --- a/setup.py +++ b/setup.py @@ -17,8 +17,7 @@ ext = '.pyx' if USE_CYTHON else '.c' -extensions = [Extension('aiohttp._multidict', ['aiohttp/_multidict' + ext]), - Extension('aiohttp._websocket', ['aiohttp/_websocket' + ext])] +extensions = [Extension('aiohttp._websocket', ['aiohttp/_websocket' + ext])] if USE_CYTHON: @@ -55,7 +54,7 @@ def build_extension(self, ext): raise RuntimeError('Unable to determine version.') -install_requires = ['chardet'] +install_requires = ['chardet', 'multidict'] if sys.version_info < (3, 4, 1): raise RuntimeError("aiohttp requires Python 3.4.1+") diff --git a/tests/conftest.py b/tests/conftest.py index 7e2e01bad01..e7709514f80 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,7 +163,12 @@ def loop(request): yield loop - if not loop._closed: + is_closed = getattr(loop, 'is_closed') + if is_closed is not None: + closed = is_closed() + else: + closed = loop._closed + if not closed: loop.call_soon(loop.stop) loop.run_forever() loop.close() @@ -222,6 +227,12 @@ def post(self, path, **kwargs): url = self._url + path return self._session.post(url, **kwargs) + def delete(self, path, **kwargs): + while path.startswith('/'): + path = path[1:] + url = self._url + path + return self._session.delete(url) + def ws_connect(self, path, **kwargs): while path.startswith('/'): path = path[1:] diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index dbb27d8e8ab..8cf5a3ebbcf 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -432,3 +432,22 @@ def handler(request): resp = yield from client.get('/', version=aiohttp.HttpVersion11) assert resp.status == 200 resp.close() + + +@pytest.mark.run_loop +def test_204_with_gzipped_content_encoding(create_app_and_client): + @asyncio.coroutine + def handler(request): + resp = web.StreamResponse(status=204) + resp.content_length = 0 + resp.content_type = 'application/json' + # resp.enable_compression(web.ContentCoding.gzip) + resp.headers['Content-Encoding'] = 'gzip' + yield from resp.prepare(request) + return resp + + app, client = yield from create_app_and_client() + app.router.add_route('DELETE', '/', handler) + resp = yield from client.delete('/') + assert resp.status == 204 + yield from resp.release() diff --git a/tests/test_client_functional_oldstyle.py b/tests/test_client_functional_oldstyle.py index 68835d88cad..01b76cb4350 100644 --- a/tests/test_client_functional_oldstyle.py +++ b/tests/test_client_functional_oldstyle.py @@ -10,10 +10,11 @@ import unittest from unittest import mock +from multidict import MultiDict + import aiohttp from aiohttp import client, helpers from aiohttp import test_utils -from aiohttp.multidict import MultiDict from aiohttp.multipart import MultipartWriter diff --git a/tests/test_client_request.py b/tests/test_client_request.py index abebef95361..a4f41a66f55 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -11,11 +11,12 @@ import zlib from http.cookies import SimpleCookie +from multidict import CIMultiDict, CIMultiDictProxy, upstr + import pytest import aiohttp from aiohttp import BaseConnector from aiohttp.client_reqrep import ClientRequest, ClientResponse -from aiohttp.multidict import CIMultiDict, CIMultiDictProxy, upstr import os.path diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 8510cfc5bb7..668ca0a72d1 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -6,12 +6,13 @@ import http.cookies from unittest import mock +from multidict import CIMultiDict, MultiDict + import aiohttp import pytest from aiohttp import web from aiohttp.client import ClientSession from aiohttp.connector import BaseConnector, TCPConnector -from aiohttp.multidict import CIMultiDict, MultiDict @pytest.fixture diff --git a/tests/test_multidict.py b/tests/test_multidict.py deleted file mode 100644 index c31e1f470ab..00000000000 --- a/tests/test_multidict.py +++ /dev/null @@ -1,857 +0,0 @@ -import sys -import unittest - -from aiohttp.multidict import (MultiDictProxy, - MultiDict, - CIMultiDictProxy, - CIMultiDict, - upstr, - _MultiDictProxy, - _MultiDict, - _CIMultiDictProxy, - _CIMultiDict, - _upstr) - - -import aiohttp - - -class _Root: - - cls = None - - proxy_cls = None - - upstr_cls = None - - def test_exposed_names(self): - name = self.cls.__name__ - while name.startswith('_'): - name = name[1:] - self.assertIn(name, aiohttp.__all__) - - -class _BaseTest(_Root): - - def test_instantiate__empty(self): - d = self.make_dict() - self.assertEqual(d, {}) - self.assertEqual(len(d), 0) - self.assertEqual(list(d.keys()), []) - self.assertEqual(list(d.values()), []) - self.assertEqual(list(d.values()), []) - self.assertEqual(list(d.items()), []) - self.assertEqual(list(d.items()), []) - - self.assertNotEqual(self.make_dict(), list()) - with self.assertRaisesRegex(TypeError, "\(2 given\)"): - self.make_dict(('key1', 'value1'), ('key2', 'value2')) - - def test_instantiate__from_arg0(self): - d = self.make_dict([('key', 'value1')]) - - self.assertEqual(d, {'key': 'value1'}) - self.assertEqual(len(d), 1) - self.assertEqual(list(d.keys()), ['key']) - self.assertEqual(list(d.values()), ['value1']) - self.assertEqual(list(d.items()), [('key', 'value1')]) - - def test_instantiate__from_arg0_dict(self): - d = self.make_dict({'key': 'value1'}) - - self.assertEqual(d, {'key': 'value1'}) - self.assertEqual(len(d), 1) - self.assertEqual(list(d.keys()), ['key']) - self.assertEqual(list(d.values()), ['value1']) - self.assertEqual(list(d.items()), [('key', 'value1')]) - - def test_instantiate__with_kwargs(self): - d = self.make_dict([('key', 'value1')], key2='value2') - - self.assertEqual(d, {'key': 'value1', 'key2': 'value2'}) - self.assertEqual(len(d), 2) - self.assertEqual(sorted(d.keys()), ['key', 'key2']) - self.assertEqual(sorted(d.values()), ['value1', 'value2']) - self.assertEqual(sorted(d.items()), [('key', 'value1'), - ('key2', 'value2')]) - - def test_getone(self): - d = self.make_dict([('key', 'value1')], key='value2') - self.assertEqual(d.getone('key'), 'value1') - self.assertEqual(d.get('key'), 'value1') - self.assertEqual(d['key'], 'value1') - - with self.assertRaises(KeyError): - d['key2'] - with self.assertRaises(KeyError): - d.getone('key2') - - self.assertEqual('default', d.getone('key2', 'default')) - - def test__iter__(self): - d = self.make_dict([('key', 'one'), ('key2', 'two'), ('key', 3)]) - self.assertEqual(['key', 'key2', 'key'], list(d)) - - def test_keys__contains(self): - d = self.make_dict([('key', 'one'), ('key2', 'two'), ('key', 3)]) - self.assertEqual(list(d.keys()), ['key', 'key2', 'key']) - - self.assertIn('key', d.keys()) - self.assertIn('key2', d.keys()) - - self.assertNotIn('foo', d.keys()) - - def test_values__contains(self): - d = self.make_dict([('key', 'one'), ('key', 'two'), ('key', 3)]) - self.assertEqual(list(d.values()), ['one', 'two', 3]) - - self.assertIn('one', d.values()) - self.assertIn('two', d.values()) - self.assertIn(3, d.values()) - - self.assertNotIn('foo', d.values()) - - def test_items__contains(self): - d = self.make_dict([('key', 'one'), ('key', 'two'), ('key', 3)]) - self.assertEqual(list(d.items()), - [('key', 'one'), ('key', 'two'), ('key', 3)]) - self.assertEqual(list(d.items()), - [('key', 'one'), ('key', 'two'), ('key', 3)]) - - self.assertIn(('key', 'one'), d.items()) - self.assertIn(('key', 'two'), d.items()) - self.assertIn(('key', 3), d.items()) - - self.assertNotIn(('foo', 'bar'), d.items()) - - def test_cannot_create_from_unaccepted(self): - with self.assertRaises(TypeError): - self.make_dict([(1, 2, 3)]) - - def test_keys_is_set_less(self): - d = self.make_dict([('key', 'value1')]) - - self.assertLess(d.keys(), {'key', 'key2'}) - - def test_keys_is_set_less_equal(self): - d = self.make_dict([('key', 'value1')]) - - self.assertLessEqual(d.keys(), {'key'}) - - def test_keys_is_set_equal(self): - d = self.make_dict([('key', 'value1')]) - - self.assertEqual(d.keys(), {'key'}) - - def test_keys_is_set_greater(self): - d = self.make_dict([('key', 'value1')]) - - self.assertGreater({'key', 'key2'}, d.keys()) - - def test_keys_is_set_greater_equal(self): - d = self.make_dict([('key', 'value1')]) - - self.assertGreaterEqual({'key'}, d.keys()) - - def test_keys_is_set_not_equal(self): - d = self.make_dict([('key', 'value1')]) - - self.assertNotEqual(d.keys(), {'key2'}) - - def test_eq(self): - d = self.make_dict([('key', 'value1')]) - self.assertEqual({'key': 'value1'}, d) - - def test_ne(self): - d = self.make_dict([('key', 'value1')]) - self.assertNotEqual(d, {'key': 'another_value'}) - - def test_and(self): - d = self.make_dict([('key', 'value1')]) - self.assertEqual({'key'}, d.keys() & {'key', 'key2'}) - - def test_or(self): - d = self.make_dict([('key', 'value1')]) - self.assertEqual({'key', 'key2'}, d.keys() | {'key2'}) - - def test_sub(self): - d = self.make_dict([('key', 'value1'), ('key2', 'value2')]) - self.assertEqual({'key'}, d.keys() - {'key2'}) - - def test_xor(self): - d = self.make_dict([('key', 'value1'), ('key2', 'value2')]) - self.assertEqual({'key', 'key3'}, d.keys() ^ {'key2', 'key3'}) - - def test_isdisjoint(self): - d = self.make_dict([('key', 'value1')]) - self.assertTrue(d.keys().isdisjoint({'key2'})) - - def test_isdisjoint2(self): - d = self.make_dict([('key', 'value1')]) - self.assertFalse(d.keys().isdisjoint({'key'})) - - def test_repr_issue_410(self): - d = self.make_dict() - try: - raise Exception - self.fail("Sould never happen") # pragma: no cover - except Exception as e: - repr(d) - self.assertIs(sys.exc_info()[1], e) - - def test_or_issue_410(self): - d = self.make_dict([('key', 'value')]) - try: - raise Exception - self.fail("Sould never happen") # pragma: no cover - except Exception as e: - d.keys() | {'other'} - self.assertIs(sys.exc_info()[1], e) - - def test_and_issue_410(self): - d = self.make_dict([('key', 'value')]) - try: - raise Exception - self.fail("Sould never happen") # pragma: no cover - except Exception as e: - d.keys() & {'other'} - self.assertIs(sys.exc_info()[1], e) - - def test_sub_issue_410(self): - d = self.make_dict([('key', 'value')]) - try: - raise Exception - self.fail("Sould never happen") # pragma: no cover - except Exception as e: - d.keys() - {'other'} - self.assertIs(sys.exc_info()[1], e) - - def test_xor_issue_410(self): - d = self.make_dict([('key', 'value')]) - try: - raise Exception - self.fail("Sould never happen") # pragma: no cover - except Exception as e: - d.keys() ^ {'other'} - self.assertIs(sys.exc_info()[1], e) - - -class _MultiDictTests(_BaseTest): - - def test__repr__(self): - d = self.make_dict() - cls = self.proxy_cls if self.proxy_cls is not None else self.cls - - self.assertEqual(str(d), "<%s()>" % cls.__name__) - d = self.make_dict([('key', 'one'), ('key', 'two')]) - self.assertEqual( - str(d), - "<%s('key': 'one', 'key': 'two')>" % cls.__name__) - - def test_getall(self): - d = self.make_dict([('key', 'value1')], key='value2') - - self.assertNotEqual(d, {'key': 'value1'}) - self.assertEqual(len(d), 2) - - self.assertEqual(d.getall('key'), ['value1', 'value2']) - - with self.assertRaisesRegex(KeyError, "some_key"): - d.getall('some_key') - - default = object() - self.assertIs(d.getall('some_key', default), default) - - def test_preserve_stable_ordering(self): - d = self.make_dict([('a', 1), ('b', '2'), ('a', 3)]) - s = '&'.join('{}={}'.format(k, v) for k, v in d.items()) - - self.assertEqual('a=1&b=2&a=3', s) - - def test_get(self): - d = self.make_dict([('a', 1), ('a', 2)]) - self.assertEqual(1, d['a']) - - def test_items__repr__(self): - d = self.make_dict([('key', 'value1')], key='value2') - self.assertEqual(repr(d.items()), - "_ItemsView('key': 'value1', 'key': 'value2')") - - def test_keys__repr__(self): - d = self.make_dict([('key', 'value1')], key='value2') - self.assertEqual(repr(d.keys()), - "_KeysView('key', 'key')") - - def test_values__repr__(self): - d = self.make_dict([('key', 'value1')], key='value2') - self.assertEqual(repr(d.values()), - "_ValuesView('value1', 'value2')") - - -class _CIMultiDictTests(_Root): - - def test_basics(self): - d = self.make_dict([('KEY', 'value1')], KEY='value2') - self.assertEqual(d.getone('key'), 'value1') - self.assertEqual(d.get('key'), 'value1') - self.assertEqual(d.get('key2', 'val'), 'val') - self.assertEqual(d['key'], 'value1') - self.assertIn('key', d) - - with self.assertRaises(KeyError): - d['key2'] - with self.assertRaises(KeyError): - d.getone('key2') - - def test_getall(self): - d = self.make_dict([('KEY', 'value1')], KEY='value2') - - self.assertNotEqual(d, {'KEY': 'value1'}) - self.assertEqual(len(d), 2) - - self.assertEqual(d.getall('key'), ['value1', 'value2']) - - with self.assertRaisesRegex(KeyError, "SOME_KEY"): - d.getall('some_key') - - def test_get(self): - d = self.make_dict([('A', 1), ('a', 2)]) - self.assertEqual(1, d['a']) - - def test_items__repr__(self): - d = self.make_dict([('KEY', 'value1')], key='value2') - self.assertEqual(repr(d.items()), - "_ItemsView('KEY': 'value1', 'KEY': 'value2')") - - def test_keys__repr__(self): - d = self.make_dict([('KEY', 'value1')], key='value2') - self.assertEqual(repr(d.keys()), - "_KeysView('KEY', 'KEY')") - - def test_values__repr__(self): - d = self.make_dict([('KEY', 'value1')], key='value2') - self.assertEqual(repr(d.values()), - "_ValuesView('value1', 'value2')") - - -class _NonProxyCIMultiDict(_CIMultiDictTests): - - def test_extend_with_upstr(self): - us = self.upstr_cls('a') - d = self.make_dict() - - d.extend([(us, 'val')]) - self.assertEqual([('A', 'val')], list(d.items())) - - -class _TestProxy(_MultiDictTests): - - def make_dict(self, *args, **kwargs): - dct = self.cls(*args, **kwargs) - return self.proxy_cls(dct) - - def test_copy(self): - d1 = self.cls(key='value', a='b') - p1 = self.proxy_cls(d1) - - d2 = p1.copy() - self.assertEqual(d1, d2) - self.assertIsNot(d1, d2) - - -class _TestCIProxy(_CIMultiDictTests): - - def make_dict(self, *args, **kwargs): - dct = self.cls(*args, **kwargs) - return self.proxy_cls(dct) - - def test_copy(self): - d1 = self.cls(key='value', a='b') - p1 = self.proxy_cls(d1) - - d2 = p1.copy() - self.assertEqual(d1, d2) - self.assertIsNot(d1, d2) - - -class _BaseMutableMultiDictTests(_BaseTest): - - def test_copy(self): - d1 = self.make_dict(key='value', a='b') - - d2 = d1.copy() - self.assertEqual(d1, d2) - self.assertIsNot(d1, d2) - - def make_dict(self, *args, **kwargs): - return self.cls(*args, **kwargs) - - def test__repr__(self): - d = self.make_dict() - self.assertEqual(str(d), "<%s()>" % self.cls.__name__) - - d = self.make_dict([('key', 'one'), ('key', 'two')]) - - self.assertEqual( - str(d), - "<%s('key': 'one', 'key': 'two')>" % self.cls.__name__) - - def test_getall(self): - d = self.make_dict([('key', 'value1')], key='value2') - self.assertEqual(len(d), 2) - - self.assertEqual(d.getall('key'), ['value1', 'value2']) - - with self.assertRaisesRegex(KeyError, "some_key"): - d.getall('some_key') - - default = object() - self.assertIs(d.getall('some_key', default), default) - - def test_add(self): - d = self.make_dict() - - self.assertEqual(d, {}) - d['key'] = 'one' - self.assertEqual(d, {'key': 'one'}) - self.assertEqual(d.getall('key'), ['one']) - - d['key'] = 'two' - self.assertEqual(d, {'key': 'two'}) - self.assertEqual(d.getall('key'), ['two']) - - d.add('key', 'one') - self.assertEqual(2, len(d)) - self.assertEqual(d.getall('key'), ['two', 'one']) - - d.add('foo', 'bar') - self.assertEqual(3, len(d)) - self.assertEqual(d.getall('foo'), ['bar']) - - def test_extend(self): - d = self.make_dict() - self.assertEqual(d, {}) - - d.extend([('key', 'one'), ('key', 'two')], key=3, foo='bar') - self.assertNotEqual(d, {'key': 'one', 'foo': 'bar'}) - self.assertEqual(4, len(d)) - itms = d.items() - # we can't guarantee order of kwargs - self.assertTrue(('key', 'one') in itms) - self.assertTrue(('key', 'two') in itms) - self.assertTrue(('key', 3) in itms) - self.assertTrue(('foo', 'bar') in itms) - - other = self.make_dict(bar='baz') - self.assertEqual(other, {'bar': 'baz'}) - - d.extend(other) - self.assertIn(('bar', 'baz'), d.items()) - - d.extend({'foo': 'moo'}) - self.assertIn(('foo', 'moo'), d.items()) - - d.extend() - self.assertEqual(6, len(d)) - - with self.assertRaises(TypeError): - d.extend('foo', 'bar') - - def test_extend_from_proxy(self): - d = self.make_dict([('a', 'a'), ('b', 'b')]) - proxy = self.proxy_cls(d) - - d2 = self.make_dict() - d2.extend(proxy) - - self.assertEqual([('a', 'a'), ('b', 'b')], list(d2.items())) - - def test_clear(self): - d = self.make_dict([('key', 'one')], key='two', foo='bar') - - d.clear() - self.assertEqual(d, {}) - self.assertEqual(list(d.items()), []) - - def test_del(self): - d = self.make_dict([('key', 'one'), ('key', 'two')], foo='bar') - - del d['key'] - self.assertEqual(d, {'foo': 'bar'}) - self.assertEqual(list(d.items()), [('foo', 'bar')]) - - with self.assertRaises(KeyError): - del d['key'] - - def test_set_default(self): - d = self.make_dict([('key', 'one'), ('key', 'two')], foo='bar') - self.assertEqual('one', d.setdefault('key', 'three')) - self.assertEqual('three', d.setdefault('otherkey', 'three')) - self.assertIn('otherkey', d) - self.assertEqual('three', d['otherkey']) - - def test_popitem(self): - d = self.make_dict() - d.add('key', 'val1') - d.add('key', 'val2') - - self.assertEqual(('key', 'val1'), d.popitem()) - self.assertEqual([('key', 'val2')], list(d.items())) - - def test_popitem_empty_multidict(self): - d = self.make_dict() - - with self.assertRaises(KeyError): - d.popitem() - - def test_pop(self): - d = self.make_dict() - d.add('key', 'val1') - d.add('key', 'val2') - - self.assertEqual('val1', d.pop('key')) - self.assertFalse(d) - - def test_pop_default(self): - d = self.make_dict(other='val') - - self.assertEqual('default', d.pop('key', 'default')) - self.assertIn('other', d) - - def test_pop_raises(self): - d = self.make_dict(other='val') - - with self.assertRaises(KeyError): - d.pop('key') - - self.assertIn('other', d) - - def test_update(self): - d = self.make_dict() - d.add('key', 'val1') - d.add('key', 'val2') - d.add('key2', 'val3') - - d.update(key='val') - - self.assertEqual([('key2', 'val3'), ('key', 'val')], list(d.items())) - - -class _CIMutableMultiDictTests(_Root): - - def make_dict(self, *args, **kwargs): - return self.cls(*args, **kwargs) - - def test_getall(self): - d = self.make_dict([('KEY', 'value1')], KEY='value2') - - self.assertNotEqual(d, {'KEY': 'value1'}) - self.assertEqual(len(d), 2) - - self.assertEqual(d.getall('key'), ['value1', 'value2']) - - with self.assertRaisesRegex(KeyError, "SOME_KEY"): - d.getall('some_key') - - def test_ctor(self): - d = self.make_dict(k1='v1') - self.assertEqual('v1', d['K1']) - - def test_setitem(self): - d = self.make_dict() - d['k1'] = 'v1' - self.assertEqual('v1', d['K1']) - - def test_delitem(self): - d = self.make_dict() - d['k1'] = 'v1' - self.assertIn('K1', d) - del d['k1'] - self.assertNotIn('K1', d) - - def test_copy(self): - d1 = self.make_dict(key='KEY', a='b') - - d2 = d1.copy() - self.assertEqual(d1, d2) - self.assertIsNot(d1, d2) - - def test__repr__(self): - d = self.make_dict() - self.assertEqual(str(d), "<%s()>" % self.cls.__name__) - - d = self.make_dict([('KEY', 'one'), ('KEY', 'two')]) - - self.assertEqual( - str(d), - "<%s('KEY': 'one', 'KEY': 'two')>" % self.cls.__name__) - - def test_add(self): - d = self.make_dict() - - self.assertEqual(d, {}) - d['KEY'] = 'one' - self.assertEqual(d, {'KEY': 'one'}) - self.assertEqual(d.getall('key'), ['one']) - - d['KEY'] = 'two' - self.assertEqual(d, {'KEY': 'two'}) - self.assertEqual(d.getall('key'), ['two']) - - d.add('KEY', 'one') - self.assertEqual(2, len(d)) - self.assertEqual(d.getall('key'), ['two', 'one']) - - d.add('FOO', 'bar') - self.assertEqual(3, len(d)) - self.assertEqual(d.getall('foo'), ['bar']) - - def test_extend(self): - d = self.make_dict() - self.assertEqual(d, {}) - - d.extend([('KEY', 'one'), ('key', 'two')], key=3, foo='bar') - self.assertNotEqual(d, {'KEY': 'one', 'FOO': 'bar'}) - self.assertEqual(4, len(d)) - itms = d.items() - # we can't guarantee order of kwargs - self.assertTrue(('KEY', 'one') in itms) - self.assertTrue(('KEY', 'two') in itms) - self.assertTrue(('KEY', 3) in itms) - self.assertTrue(('FOO', 'bar') in itms) - - other = self.make_dict(bar='baz') - self.assertEqual(other, {'BAR': 'baz'}) - - d.extend(other) - self.assertIn(('BAR', 'baz'), d.items()) - - d.extend({'FOO': 'moo'}) - self.assertIn(('FOO', 'moo'), d.items()) - - d.extend() - self.assertEqual(6, len(d)) - - with self.assertRaises(TypeError): - d.extend('foo', 'bar') - - def test_extend_from_proxy(self): - d = self.make_dict([('a', 'a'), ('b', 'b')]) - proxy = self.proxy_cls(d) - - d2 = self.make_dict() - d2.extend(proxy) - - self.assertEqual([('A', 'a'), ('B', 'b')], list(d2.items())) - - def test_clear(self): - d = self.make_dict([('KEY', 'one')], key='two', foo='bar') - - d.clear() - self.assertEqual(d, {}) - self.assertEqual(list(d.items()), []) - - def test_del(self): - d = self.make_dict([('KEY', 'one'), ('key', 'two')], foo='bar') - - del d['key'] - self.assertEqual(d, {'FOO': 'bar'}) - self.assertEqual(list(d.items()), [('FOO', 'bar')]) - - with self.assertRaises(KeyError): - del d['key'] - - def test_set_default(self): - d = self.make_dict([('KEY', 'one'), ('key', 'two')], foo='bar') - self.assertEqual('one', d.setdefault('key', 'three')) - self.assertEqual('three', d.setdefault('otherkey', 'three')) - self.assertIn('otherkey', d) - self.assertEqual('three', d['OTHERKEY']) - - def test_popitem(self): - d = self.make_dict() - d.add('KEY', 'val1') - d.add('key', 'val2') - - self.assertEqual(('KEY', 'val1'), d.popitem()) - self.assertEqual([('KEY', 'val2')], list(d.items())) - - def test_popitem_empty_multidict(self): - d = self.make_dict() - - with self.assertRaises(KeyError): - d.popitem() - - def test_pop(self): - d = self.make_dict() - d.add('KEY', 'val1') - d.add('key', 'val2') - - self.assertEqual('val1', d.pop('KEY')) - self.assertFalse(d) - - def test_pop_default(self): - d = self.make_dict(OTHER='val') - - self.assertEqual('default', d.pop('key', 'default')) - self.assertIn('other', d) - - def test_pop_raises(self): - d = self.make_dict(OTHER='val') - - with self.assertRaises(KeyError): - d.pop('KEY') - - self.assertIn('other', d) - - def test_update(self): - d = self.make_dict() - d.add('KEY', 'val1') - d.add('key', 'val2') - d.add('key2', 'val3') - - d.update(key='val') - - self.assertEqual([('KEY2', 'val3'), ('KEY', 'val')], list(d.items())) - - -class TestPyMultiDictProxy(_TestProxy, unittest.TestCase): - - cls = _MultiDict - proxy_cls = _MultiDictProxy - - -class TestPyCIMultiDictProxy(_TestCIProxy, unittest.TestCase): - - cls = _CIMultiDict - proxy_cls = _CIMultiDictProxy - - -class PyMutableMultiDictTests(_BaseMutableMultiDictTests, unittest.TestCase): - - cls = _MultiDict - proxy_cls = _MultiDictProxy - - -class PyCIMutableMultiDictTests(_CIMutableMultiDictTests, _NonProxyCIMultiDict, - unittest.TestCase): - - cls = _CIMultiDict - upstr_cls = _upstr - proxy_cls = _CIMultiDictProxy - - -class TestMultiDictProxy(_TestProxy, unittest.TestCase): - - cls = MultiDict - proxy_cls = MultiDictProxy - - -class TestCIMultiDictProxy(_TestCIProxy, unittest.TestCase): - - cls = CIMultiDict - proxy_cls = CIMultiDictProxy - - -class MutableMultiDictTests(_BaseMutableMultiDictTests, unittest.TestCase): - - cls = MultiDict - proxy_cls = MultiDictProxy - - -class CIMutableMultiDictTests(_CIMutableMultiDictTests, _NonProxyCIMultiDict, - unittest.TestCase): - - cls = CIMultiDict - upstr_cls = upstr - proxy_cls = CIMultiDictProxy - - -class _UpStrMixin: - - cls = None - - def test_ctor(self): - s = self.cls() - self.assertEqual('', s) - - def test_ctor_str(self): - s = self.cls('a') - self.assertEqual('A', s) - - def test_ctor_str_uppercase(self): - s = self.cls('A') - self.assertEqual('A', s) - - def test_ctor_buffer(self): - s = self.cls(b'a') - self.assertEqual('A', s) - - def test_ctor_repr(self): - s = self.cls(None) - self.assertEqual('NONE', s) - - def test_upper(self): - s = self.cls('a') - self.assertIs(s, s.upper()) - - -class TestPyUpStr(_UpStrMixin, unittest.TestCase): - - cls = _upstr - - -class TestUpStr(_UpStrMixin, unittest.TestCase): - - cls = upstr - - -class TypesMixin: - - proxy = ciproxy = mdict = cimdict = None - - def test_proxies(self): - self.assertTrue(issubclass(self.ciproxy, self.proxy)) - - def test_dicts(self): - self.assertTrue(issubclass(self.cimdict, self.mdict)) - - def test_proxy_not_inherited_from_dict(self): - self.assertFalse(issubclass(self.proxy, self.mdict)) - - def test_dict_not_inherited_from_proxy(self): - self.assertFalse(issubclass(self.mdict, self.proxy)) - - def test_create_multidict_proxy_from_nonmultidict(self): - with self.assertRaises(TypeError): - self.proxy({}) - - def test_create_multidict_proxy_from_cimultidict(self): - d = self.cimdict(key='val') - p = self.proxy(d) - self.assertEqual(p, d) - - def test_create_cimultidict_proxy_from_nonmultidict(self): - with self.assertRaises(TypeError): - self.ciproxy({}) - - def test_create_ci_multidict_proxy_from_multidict(self): - d = self.mdict(key='val') - with self.assertRaises(TypeError): - self.ciproxy(d) - - -class TestPyTypes(TypesMixin, unittest.TestCase): - - proxy = _MultiDictProxy - ciproxy = _CIMultiDictProxy - mdict = _MultiDict - cimdict = _CIMultiDict - - -class TestTypes(TypesMixin, unittest.TestCase): - - proxy = MultiDictProxy - ciproxy = CIMultiDictProxy - mdict = MultiDict - cimdict = CIMultiDict diff --git a/tests/test_resolver.py b/tests/test_resolver.py new file mode 100644 index 00000000000..3239e6e0976 --- /dev/null +++ b/tests/test_resolver.py @@ -0,0 +1,114 @@ +import pytest +import asyncio +import socket +import ipaddress +from aiohttp.resolver import AsyncResolver, DefaultResolver +from unittest.mock import patch + +try: + import aiodns +except ImportError: + aiodns = None + + +class FakeResult: + def __init__(self, host): + self.host = host + + +@asyncio.coroutine +def fake_result(result): + return [FakeResult(host=h) + for h in result] + + +def fake_addrinfo(hosts): + @asyncio.coroutine + def fake(*args, **kwargs): + if not hosts: + raise socket.gaierror + + return list([(None, None, None, None, [h, 0]) + for h in hosts]) + + return fake + + +@pytest.mark.skipif(aiodns is None, reason="aiodns required") +def test_async_resolver_positive_lookup(loop): + @asyncio.coroutine + def go(): + with patch('aiodns.DNSResolver.query') as mock_query: + mock_query.return_value = fake_result(['127.0.0.1']) + resolver = AsyncResolver(loop=loop) + real = yield from resolver.resolve('www.python.org') + ipaddress.ip_address(real[0]['host']) + loop.run_until_complete(go()) + + +@pytest.mark.skipif(aiodns is None, reason="aiodns required") +def test_async_resolver_multiple_replies(loop): + @asyncio.coroutine + def go(): + with patch('aiodns.DNSResolver.query') as mock_query: + ips = ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'] + mock_query.return_value = fake_result(ips) + resolver = AsyncResolver(loop=loop) + real = yield from resolver.resolve('www.google.com') + ips = [ipaddress.ip_address(x['host']) for x in real] + assert len(ips) > 3, "Expecting multiple addresses" + loop.run_until_complete(go()) + + +@pytest.mark.skipif(aiodns is None, reason="aiodns required") +def test_async_negative_lookup(loop): + @asyncio.coroutine + def go(): + with patch('aiodns.DNSResolver.query') as mock_query: + mock_query.side_effect = aiodns.error.DNSError() + resolver = AsyncResolver(loop=loop) + try: + yield from resolver.resolve('doesnotexist.bla') + assert False, "Expecting aiodns.error.DNSError" + except aiodns.error.DNSError: + pass + + loop.run_until_complete(go()) + + +def test_default_resolver_positive_lookup(loop): + @asyncio.coroutine + def go(): + loop.getaddrinfo = fake_addrinfo(["127.0.0.1"]) + resolver = DefaultResolver(loop=loop) + real = yield from resolver.resolve('www.python.org') + ipaddress.ip_address(real[0]['host']) + + loop.run_until_complete(go()) + + +def test_default_resolver_multiple_replies(loop): + @asyncio.coroutine + def go(): + ips = ['127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'] + loop.getaddrinfo = fake_addrinfo(ips) + resolver = DefaultResolver(loop=loop) + real = yield from resolver.resolve('www.google.com') + ips = [ipaddress.ip_address(x['host']) for x in real] + assert len(ips) > 3, "Expecting multiple addresses" + loop.run_until_complete(go()) + + +def test_default_negative_lookup(loop): + @asyncio.coroutine + def go(): + ips = [] + loop.getaddrinfo = fake_addrinfo(ips) + resolver = DefaultResolver(loop=loop) + try: + yield from resolver.resolve('doesnotexist.bla') + assert False, "Expecting socket.gaierror" + except socket.gaierror: + pass + + loop.run_until_complete(go()) diff --git a/tests/test_signals.py b/tests/test_signals.py index 6b54d2f404b..4bfeb649287 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -1,6 +1,6 @@ import asyncio from unittest import mock -from aiohttp.multidict import CIMultiDict +from multidict import CIMultiDict from aiohttp.signals import Signal from aiohttp.web import Application from aiohttp.web import Request, Response diff --git a/tests/test_timeout.py b/tests/test_timeout.py index 7f7b46d0c48..963d62133e5 100644 --- a/tests/test_timeout.py +++ b/tests/test_timeout.py @@ -50,6 +50,21 @@ def run(): loop.run_until_complete(run()) +@pytest.mark.run_loop +def test_timeout_disable(loop): + @asyncio.coroutine + def long_running_task(): + yield from asyncio.sleep(0.1, loop=loop) + return 'done' + + t0 = loop.time() + with Timeout(None, loop=loop): + resp = yield from long_running_task() + assert resp == 'done' + dt = loop.time() - t0 + assert 0.09 < dt < 0.11, dt + + @pytest.mark.run_loop def test_timeout_not_relevant_exception(loop): yield from asyncio.sleep(0, loop=loop) diff --git a/tests/test_urldispatch.py b/tests/test_urldispatch.py index 427646a5410..593bdf5c2e8 100644 --- a/tests/test_urldispatch.py +++ b/tests/test_urldispatch.py @@ -6,12 +6,12 @@ from collections.abc import Sized, Container, Iterable, Mapping, MutableMapping from unittest import mock from urllib.parse import unquote +from multidict import CIMultiDict import aiohttp.web from aiohttp import hdrs from aiohttp.web import (UrlDispatcher, Request, Response, HTTPMethodNotAllowed, HTTPNotFound, HTTPCreated) -from aiohttp.multidict import CIMultiDict from aiohttp.protocol import HttpVersion, RawRequestMessage from aiohttp.web_urldispatcher import (_defaultExpectHandler, DynamicRoute, @@ -697,7 +697,7 @@ def test_resource_adapter_resolve_not_math(self): route = PlainRoute('GET', lambda req: None, None, '/path') self.router.register_route(route) resource = route.resource - self.assertEqual((None, {'GET'}), + self.assertEqual((None, set()), self.loop.run_until_complete( resource.resolve('GET', '/another/path'))) @@ -888,3 +888,27 @@ def test_static_route_points_to_file(self): here = pathlib.Path(aiohttp.__file__).parent / '__init__.py' with self.assertRaises(ValueError): self.router.add_static('/st', here) + + def test_404_for_resource_adapter(self): + route = self.router.add_static('/st', + os.path.dirname(aiohttp.__file__)) + resource = route.resource + ret = self.loop.run_until_complete( + resource.resolve('GET', '/unknown/path')) + self.assertEqual((None, set()), ret) + + def test_405_for_resource_adapter(self): + route = self.router.add_static('/st', + os.path.dirname(aiohttp.__file__)) + resource = route.resource + ret = self.loop.run_until_complete( + resource.resolve('POST', '/st/abc.py')) + self.assertEqual((None, {'GET'}), ret) + + def test_check_allowed_method_for_found_resource(self): + handler = self.make_handler() + resource = self.router.add_resource('/') + resource.add_route('GET', handler) + ret = self.loop.run_until_complete(resource.resolve('GET', '/')) + self.assertIsNotNone(ret[0]) + self.assertEqual({'GET'}, ret[1]) diff --git a/tests/test_web_exceptions.py b/tests/test_web_exceptions.py index b89c659e161..846f0a849c6 100644 --- a/tests/test_web_exceptions.py +++ b/tests/test_web_exceptions.py @@ -1,8 +1,8 @@ import collections import pytest import re +from multidict import CIMultiDict from unittest import mock -from aiohttp.multidict import CIMultiDict from aiohttp.web import Request from aiohttp.protocol import RawRequestMessage, HttpVersion11 @@ -163,3 +163,10 @@ def test_empty_body_205(): def test_empty_body_304(): resp = web.HTTPNoContent() resp.body is None + + +def test_link_header_451(buf, request): + resp = web.HTTPUnavailableForLegalReasons(link='http://warning.or.kr/') + + assert 'http://warning.or.kr/' == resp.link + assert '; rel="blocked-by"' == resp.headers['Link'] diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 16654347df5..ceb240b24cf 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -5,8 +5,8 @@ import os.path import socket import unittest +from multidict import MultiDict from aiohttp import log, web, request, FormData, ClientSession, TCPConnector -from aiohttp.multidict import MultiDict from aiohttp.protocol import HttpVersion, HttpVersion10, HttpVersion11 from aiohttp.streams import EOF_MARKER diff --git a/tests/test_web_request.py b/tests/test_web_request.py index b757df9f3c0..f076e70c0b6 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -1,8 +1,8 @@ import pytest from unittest import mock +from multidict import MultiDict, CIMultiDict from aiohttp.signals import Signal from aiohttp.web import Request -from aiohttp.multidict import MultiDict, CIMultiDict from aiohttp.protocol import HttpVersion from aiohttp.protocol import RawRequestMessage diff --git a/tests/test_web_response.py b/tests/test_web_response.py index 6e49572aa52..f434caf8981 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -3,8 +3,8 @@ import pytest import re from unittest import mock +from multidict import CIMultiDict from aiohttp import hdrs, signals -from aiohttp.multidict import CIMultiDict from aiohttp.web import ( ContentCoding, Request, StreamResponse, Response, json_response ) diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py new file mode 100644 index 00000000000..689bfbc2c36 --- /dev/null +++ b/tests/test_web_urldispatcher.py @@ -0,0 +1,65 @@ +import pytest +import os +import shutil +import tempfile +import functools +import asyncio +import aiohttp.web + + +@pytest.fixture(scope='function') +def tmp_dir_path(request): + """ + Give a path for a temporary directory + The directory is destroyed at the end of the test. + """ + # Temporary directory. + tmp_dir = tempfile.mkdtemp() + + def teardown(): + # Delete the whole directory: + shutil.rmtree(tmp_dir) + + request.addfinalizer(teardown) + return tmp_dir + + +@pytest.mark.run_loop +def test_access_root_of_static_handler(tmp_dir_path, create_app_and_client): + """ + Tests the operation of static file server. + Try to access the root of static file server, and make + sure that a proper not found error is returned. + """ + # Put a file inside tmp_dir_path: + my_file_path = os.path.join(tmp_dir_path, 'my_file') + with open(my_file_path, 'w') as fw: + fw.write('hello') + + app, client = yield from create_app_and_client() + + # Register global static route: + app.router.add_static('/', tmp_dir_path) + + # Request the root of the static directory. + # Expect an 404 error page. + r = yield from client.get('/') + assert r.status == 404 + # data = (yield from r.read()) + yield from r.release() + + +@pytest.mark.run_loop +def test_partialy_applied_handler(create_app_and_client): + app, client = yield from create_app_and_client() + + @asyncio.coroutine + def handler(data, request): + return aiohttp.web.Response(body=data) + + app.router.add_route('GET', '/', functools.partial(handler, b'hello')) + + r = yield from client.get('/') + data = (yield from r.read()) + assert data == b'hello' + yield from r.release() diff --git a/tests/test_websocket_writer.py b/tests/test_websocket_writer.py index 364f0df7198..eb32adb64d1 100644 --- a/tests/test_websocket_writer.py +++ b/tests/test_websocket_writer.py @@ -53,6 +53,10 @@ def test_close(transport, writer): writer.close(1001, b'msg') transport.write.assert_called_with(b'\x88\x05\x03\xe9msg') + # Test that Service Restart close code is also supported + writer.close(1012, b'msg') + transport.write.assert_called_with(b'\x88\x05\x03\xf4msg') + def test_send_text_masked(transport, writer): writer = websocket.WebSocketWriter(transport, diff --git a/tests/test_worker.py b/tests/test_worker.py index 492d6269352..79d909c4fe5 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -41,7 +41,12 @@ def test_run(worker, loop): worker.run() assert worker._run.called - assert loop._closed + is_closed = getattr(loop, 'is_closed') + if is_closed is not None: + closed = is_closed() + else: + closed = loop._closed + assert closed def test_handle_quit(worker):