From 50b81c5b6a4073fcfdc7e9c9161fd5a98721394e Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Fri, 26 Jan 2018 15:32:22 +0200 Subject: [PATCH] Use attrs library for data classes (#2690) * Convert FileField to attrs usage * Convert RouteDef * Make RouteDef frozen * Convert WebSocketReady to attrs * Convert more * Convert more * Add changenote * Drop slots=True --- CHANGES/2690.feature | 1 + aiohttp/client.py | 3 ++- aiohttp/client_reqrep.py | 16 +++++++++++----- aiohttp/helpers.py | 16 +++++++++++++--- aiohttp/web_request.py | 15 ++++++++++++--- aiohttp/web_urldispatcher.py | 13 ++++++++----- aiohttp/web_ws.py | 8 ++++++-- requirements/ci-wheel.txt | 1 + setup.py | 2 +- tests/test_client_request.py | 4 +++- tests/test_helpers.py | 18 ++++++++++-------- tests/test_web_websocket.py | 8 ++++---- 12 files changed, 72 insertions(+), 33 deletions(-) create mode 100644 CHANGES/2690.feature diff --git a/CHANGES/2690.feature b/CHANGES/2690.feature new file mode 100644 index 00000000000..5ced83a4f26 --- /dev/null +++ b/CHANGES/2690.feature @@ -0,0 +1 @@ +Use ``attrs`` library for data classes, replace `namedtuple`. \ No newline at end of file diff --git a/aiohttp/client.py b/aiohttp/client.py index f259935c536..2f35fe134ea 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -271,7 +271,8 @@ async def _request(self, method, url, *, elif self._trust_env: for scheme, proxy_info in proxies_from_env().items(): if scheme == url.scheme: - proxy, proxy_auth = proxy_info + proxy = proxy_info.proxy + proxy_auth = proxy_info.proxy_auth break req = self._request_class( diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 7068a0ab4e6..b75988ab1aa 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -1,6 +1,5 @@ import asyncio import codecs -import collections import io import json import sys @@ -11,6 +10,7 @@ from http.cookies import CookieError, Morsel, SimpleCookie from types import MappingProxyType +import attr from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy from yarl import URL @@ -39,12 +39,18 @@ __all__ = ('ClientRequest', 'ClientResponse', 'RequestInfo', 'Fingerprint') -ContentDisposition = collections.namedtuple( - 'ContentDisposition', ('type', 'parameters', 'filename')) +@attr.s(frozen=True) +class ContentDisposition: + type = attr.ib(type=str) + parameters = attr.ib(type=MappingProxyType) + filename = attr.ib(type=str) -RequestInfo = collections.namedtuple( - 'RequestInfo', ('url', 'method', 'headers')) +@attr.s(frozen=True) +class RequestInfo: + url = attr.ib(type=URL) + method = attr.ib(type=str) + headers = attr.ib(type=CIMultiDictProxy) class Fingerprint: diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index c9798371644..839452b491a 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -21,6 +21,8 @@ from urllib.request import getproxies import async_timeout +import attr +from multidict import MultiDict from yarl import URL from . import hdrs @@ -141,7 +143,10 @@ def netrc_from_env(): return netrc_obj -ProxyInfo = namedtuple('ProxyInfo', 'proxy proxy_auth') +@attr.s(frozen=True) +class ProxyInfo: + proxy = attr.ib(type=str) + proxy_auth = attr.ib(type=BasicAuth) def proxies_from_env(): @@ -187,7 +192,12 @@ def isasyncgenfunction(obj): return False -MimeType = namedtuple('MimeType', 'type subtype suffix parameters') +@attr.s(frozen=True) +class MimeType: + type = attr.ib(type=str) + subtype = attr.ib(type=str) + suffix = attr.ib(type=str) + parameters = attr.ib(type=MultiDict) def parse_mimetype(mimetype): @@ -214,7 +224,7 @@ def parse_mimetype(mimetype): continue key, value = item.split('=', 1) if '=' in item else (item, '') params.append((key.lower().strip(), value.strip(' "'))) - params = dict(params) + params = MultiDict(params) fulltype = parts[0].strip().lower() if fulltype == '*': diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index fe3012c5b96..9eee747bdcf 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -1,6 +1,7 @@ import asyncio import collections import datetime +import io import json import re import socket @@ -13,7 +14,8 @@ from types import MappingProxyType from urllib.parse import parse_qsl -from multidict import CIMultiDict, MultiDict, MultiDictProxy +import attr +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy from yarl import URL from . import hdrs, multipart @@ -24,8 +26,15 @@ __all__ = ('BaseRequest', 'FileField', 'Request') -FileField = collections.namedtuple( - 'Field', 'name filename file content_type headers') + +@attr.s(frozen=True) +class FileField: + name = attr.ib(type=str) + filename = attr.ib(type=str) + file = attr.ib(type=io.BufferedReader) + content_type = attr.ib(type=str) + headers = attr.ib(type=CIMultiDictProxy) + _TCHAR = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-" # '-' at the end to prevent interpretation as range in a char class diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index f8a263388b2..784dbb81d0b 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -8,16 +8,13 @@ import os import re import warnings -from collections import namedtuple from collections.abc import Container, Iterable, Sequence, Sized from contextlib import contextmanager from functools import wraps from pathlib import Path from types import MappingProxyType -# do not use yarl.quote/unquote directly, -# use `URL(path).raw_path` instead of `quote(path)` -# Escaping of the URLs need to be consitent with the escaping done by yarl +import attr from yarl import URL from . import hdrs @@ -40,7 +37,13 @@ PATH_SEP = re.escape('/') -class RouteDef(namedtuple('_RouteDef', 'method, path, handler, kwargs')): +@attr.s(frozen=True, repr=False) +class RouteDef: + method = attr.ib(type=str) + path = attr.ib(type=str) + handler = attr.ib() + kwargs = attr.ib() + def __repr__(self): info = [] for name, value in sorted(self.kwargs.items()): diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py index ceaf09b4c56..e31ab2e2ba0 100644 --- a/aiohttp/web_ws.py +++ b/aiohttp/web_ws.py @@ -3,9 +3,9 @@ import binascii import hashlib import json -from collections import namedtuple import async_timeout +import attr from multidict import CIMultiDict from . import hdrs @@ -28,7 +28,11 @@ MsgType = WSMsgType -class WebSocketReady(namedtuple('WebSocketReady', 'ok protocol')): +@attr.s(frozen=True) +class WebSocketReady: + ok = attr.ib(type=bool) + protocol = attr.ib(type=str) + def __bool__(self): return self.ok diff --git a/requirements/ci-wheel.txt b/requirements/ci-wheel.txt index 519df1a3623..1567e9b7262 100644 --- a/requirements/ci-wheel.txt +++ b/requirements/ci-wheel.txt @@ -1,3 +1,4 @@ +attrs==17.4.0 async-timeout==2.0.0 brotlipy==0.7.0 cchardet==2.1.1 diff --git a/setup.py b/setup.py index 9833f184721..239a0518b50 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ def build_extension(self, ext): raise RuntimeError('Unable to determine version.') -install_requires = ['chardet', 'multidict>=4.0.0', +install_requires = ['attrs>=17.4.0', 'chardet', 'multidict>=4.0.0', 'async_timeout>=1.2.0', 'yarl>=1.0.0'] diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 87f24c6e5f8..45bc03f563c 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -98,7 +98,9 @@ def test_version_default(make_request): def test_request_info(make_request): req = make_request('get', 'http://python.org/') - assert req.request_info == (URL('http://python.org/'), 'GET', req.headers) + assert req.request_info == aiohttp.RequestInfo(URL('http://python.org/'), + 'GET', + req.headers) def test_version_err(make_request): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8aefcea02e9..a181b83638b 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -15,23 +15,25 @@ # ------------------- parse_mimetype ---------------------------------- @pytest.mark.parametrize('mimetype, expected', [ - ('', ('', '', '', {})), - ('*', ('*', '*', '', {})), - ('application/json', ('application', 'json', '', {})), + ('', helpers.MimeType('', '', '', {})), + ('*', helpers.MimeType('*', '*', '', {})), + ('application/json', helpers.MimeType('application', 'json', '', {})), ( 'application/json; charset=utf-8', - ('application', 'json', '', {'charset': 'utf-8'}) + helpers.MimeType('application', 'json', '', {'charset': 'utf-8'}) ), ( '''application/json; charset=utf-8;''', - ('application', 'json', '', {'charset': 'utf-8'}) + helpers.MimeType('application', 'json', '', {'charset': 'utf-8'}) ), ( 'ApPlIcAtIoN/JSON;ChaRseT="UTF-8"', - ('application', 'json', '', {'charset': 'UTF-8'}) + helpers.MimeType('application', 'json', '', {'charset': 'UTF-8'}) ), - ('application/rss+xml', ('application', 'rss', 'xml', {})), - ('text/plain;base64', ('text', 'plain', '', {'base64': ''})) + ('application/rss+xml', + helpers.MimeType('application', 'rss', 'xml', {})), + ('text/plain;base64', + helpers.MimeType('text', 'plain', '', {'base64': ''})) ]) def test_parse_mimetype(mimetype, expected): result = helpers.parse_mimetype(mimetype) diff --git a/tests/test_web_websocket.py b/tests/test_web_websocket.py index f92e8b9546a..ef91ef7f09a 100644 --- a/tests/test_web_websocket.py +++ b/tests/test_web_websocket.py @@ -203,26 +203,26 @@ def test_bool_websocket_not_ready(): def test_can_prepare_ok(make_request): req = make_request('GET', '/', protocols=True) ws = WebSocketResponse(protocols=('chat',)) - assert(True, 'chat') == ws.can_prepare(req) + assert WebSocketReady(True, 'chat') == ws.can_prepare(req) def test_can_prepare_unknown_protocol(make_request): req = make_request('GET', '/') ws = WebSocketResponse() - assert (True, None) == ws.can_prepare(req) + assert WebSocketReady(True, None) == ws.can_prepare(req) def test_can_prepare_invalid_method(make_request): req = make_request('POST', '/') ws = WebSocketResponse() - assert (False, None) == ws.can_prepare(req) + assert WebSocketReady(False, None) == ws.can_prepare(req) def test_can_prepare_without_upgrade(make_request): req = make_request('GET', '/', headers=CIMultiDict({})) ws = WebSocketResponse() - assert (False, None) == ws.can_prepare(req) + assert WebSocketReady(False, None) == ws.can_prepare(req) async def test_can_prepare_started(make_request):