Skip to content

Commit

Permalink
Use attrs library for data classes (#2690)
Browse files Browse the repository at this point in the history
* Convert FileField to attrs usage

* Convert RouteDef

* Make RouteDef frozen

* Convert WebSocketReady to attrs

* Convert more

* Convert more

* Add changenote

* Drop slots=True
  • Loading branch information
asvetlov authored Jan 26, 2018
1 parent 93bad20 commit 50b81c5
Show file tree
Hide file tree
Showing 12 changed files with 72 additions and 33 deletions.
1 change: 1 addition & 0 deletions CHANGES/2690.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use ``attrs`` library for data classes, replace `namedtuple`.
3 changes: 2 additions & 1 deletion aiohttp/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
16 changes: 11 additions & 5 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import asyncio
import codecs
import collections
import io
import json
import sys
Expand All @@ -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

Expand Down Expand Up @@ -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:
Expand Down
16 changes: 13 additions & 3 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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):
Expand All @@ -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 == '*':
Expand Down
15 changes: 12 additions & 3 deletions aiohttp/web_request.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import asyncio
import collections
import datetime
import io
import json
import re
import socket
Expand All @@ -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
Expand All @@ -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
Expand Down
13 changes: 8 additions & 5 deletions aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()):
Expand Down
8 changes: 6 additions & 2 deletions aiohttp/web_ws.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions requirements/ci-wheel.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
attrs==17.4.0
async-timeout==2.0.0
brotlipy==0.7.0
cchardet==2.1.1
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']


Expand Down
4 changes: 3 additions & 1 deletion tests/test_client_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
18 changes: 10 additions & 8 deletions tests/test_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions tests/test_web_websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 50b81c5

Please sign in to comment.