Skip to content

Commit 8497304

Browse files
Björn Friedrichsasvetlov
authored andcommitted
Move encoder and decoder to AbstractStorage (#252)
* moved encoder and decoder to AbstractStorage * docs: json.parse -> json.dumps * Added signature and version hint for encoder/decoder. Bumped version. * adjust versionadded position * loads/dumps -> encoder/decoder * Tests adjusted for macOS
1 parent 9704bca commit 8497304

File tree

7 files changed

+119
-45
lines changed

7 files changed

+119
-45
lines changed

aiohttp_session/__init__.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from aiohttp import web
99

1010

11-
__version__ = '2.2.0'
11+
__version__ = '2.3.0'
1212

1313

1414
class Session(MutableMapping):
@@ -163,14 +163,17 @@ class AbstractStorage(metaclass=abc.ABCMeta):
163163

164164
def __init__(self, *, cookie_name="AIOHTTP_SESSION",
165165
domain=None, max_age=None, path='/',
166-
secure=None, httponly=True):
166+
secure=None, httponly=True,
167+
encoder=json.dumps, decoder=json.loads):
167168
self._cookie_name = cookie_name
168169
self._cookie_params = dict(domain=domain,
169170
max_age=max_age,
170171
path=path,
171172
secure=secure,
172173
httponly=httponly)
173174
self._max_age = max_age
175+
self._encoder = encoder
176+
self._decoder = decoder
174177

175178
@property
176179
def cookie_name(self):
@@ -230,19 +233,21 @@ class SimpleCookieStorage(AbstractStorage):
230233

231234
def __init__(self, *, cookie_name="AIOHTTP_SESSION",
232235
domain=None, max_age=None, path='/',
233-
secure=None, httponly=True):
236+
secure=None, httponly=True,
237+
encoder=json.dumps, decoder=json.loads):
234238
super().__init__(cookie_name=cookie_name, domain=domain,
235239
max_age=max_age, path=path, secure=secure,
236-
httponly=httponly)
240+
httponly=httponly,
241+
encoder=encoder, decoder=decoder)
237242

238243
async def load_session(self, request):
239244
cookie = self.load_cookie(request)
240245
if cookie is None:
241246
return Session(None, data=None, new=True, max_age=self.max_age)
242247
else:
243-
data = json.loads(cookie)
248+
data = self._decoder(cookie)
244249
return Session(None, data=data, new=False, max_age=self.max_age)
245250

246251
async def save_session(self, request, response, session):
247-
cookie_data = json.dumps(self._get_session_data(session))
252+
cookie_data = self._encoder(self._get_session_data(session))
248253
self.save_cookie(response, cookie_data, max_age=session.max_age)

aiohttp_session/cookie_storage.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ class EncryptedCookieStorage(AbstractStorage):
1414

1515
def __init__(self, secret_key, *, cookie_name="AIOHTTP_SESSION",
1616
domain=None, max_age=None, path='/',
17-
secure=None, httponly=True):
17+
secure=None, httponly=True,
18+
encoder=json.dumps, decoder=json.loads):
1819
super().__init__(cookie_name=cookie_name, domain=domain,
1920
max_age=max_age, path=path, secure=secure,
20-
httponly=httponly)
21+
httponly=httponly,
22+
encoder=encoder, decoder=decoder)
2123

2224
if isinstance(secret_key, str):
2325
pass
@@ -31,7 +33,7 @@ async def load_session(self, request):
3133
return Session(None, data=None, new=True, max_age=self.max_age)
3234
else:
3335
try:
34-
data = json.loads(
36+
data = self._decoder(
3537
self._fernet.decrypt(
3638
cookie.encode('utf-8')).decode('utf-8'))
3739
return Session(None, data=data,
@@ -46,7 +48,7 @@ async def save_session(self, request, response, session):
4648
return self.save_cookie(response, '',
4749
max_age=session.max_age)
4850

49-
cookie_data = json.dumps(
51+
cookie_data = self._encoder(
5052
self._get_session_data(session)
5153
).encode('utf-8')
5254
self.save_cookie(

aiohttp_session/memcached_storage.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import json
21
import uuid
3-
2+
import json
43
from . import AbstractStorage, Session
54

65

@@ -10,13 +9,12 @@ class MemcachedStorage(AbstractStorage):
109
def __init__(self, memcached_conn, *, cookie_name="AIOHTTP_SESSION",
1110
domain=None, max_age=None, path='/',
1211
secure=None, httponly=True,
13-
encoder=json.dumps, decoder=json.loads,
14-
key_factory=lambda: uuid.uuid4().hex):
12+
key_factory=lambda: uuid.uuid4().hex,
13+
encoder=json.dumps, decoder=json.loads):
1514
super().__init__(cookie_name=cookie_name, domain=domain,
1615
max_age=max_age, path=path, secure=secure,
17-
httponly=httponly)
18-
self._encoder = encoder
19-
self._decoder = decoder
16+
httponly=httponly,
17+
encoder=encoder, decoder=decoder)
2018
self._key_factory = key_factory
2119
self.conn = memcached_conn
2220

aiohttp_session/nacl_storage.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ class NaClCookieStorage(AbstractStorage):
1313

1414
def __init__(self, secret_key, *, cookie_name="AIOHTTP_SESSION",
1515
domain=None, max_age=None, path='/',
16-
secure=None, httponly=True):
16+
secure=None, httponly=True,
17+
encoder=json.dumps, decoder=json.loads):
1718
super().__init__(cookie_name=cookie_name, domain=domain,
1819
max_age=max_age, path=path, secure=secure,
19-
httponly=httponly)
20+
httponly=httponly,
21+
encoder=encoder, decoder=decoder)
2022

2123
self._secretbox = nacl.secret.SecretBox(secret_key)
2224

@@ -25,7 +27,7 @@ async def load_session(self, request):
2527
if cookie is None:
2628
return Session(None, data=None, new=True, max_age=self.max_age)
2729
else:
28-
data = json.loads(
30+
data = self._decoder(
2931
self._secretbox.decrypt(cookie.encode('utf-8'),
3032
encoder=Base64Encoder).decode('utf-8')
3133
)
@@ -36,7 +38,7 @@ async def save_session(self, request, response, session):
3638
return self.save_cookie(response, session._mapping,
3739
max_age=session.max_age)
3840

39-
cookie_data = json.dumps(
41+
cookie_data = self._encoder(
4042
self._get_session_data(session)
4143
).encode('utf-8')
4244
nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)

aiohttp_session/redis_storage.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,16 @@ class RedisStorage(AbstractStorage):
1616
def __init__(self, redis_pool, *, cookie_name="AIOHTTP_SESSION",
1717
domain=None, max_age=None, path='/',
1818
secure=None, httponly=True,
19-
encoder=json.dumps, decoder=json.loads,
20-
key_factory=lambda: uuid.uuid4().hex):
19+
key_factory=lambda: uuid.uuid4().hex,
20+
encoder=json.dumps, decoder=json.loads):
2121
super().__init__(cookie_name=cookie_name, domain=domain,
2222
max_age=max_age, path=path, secure=secure,
23-
httponly=httponly)
23+
httponly=httponly,
24+
encoder=encoder, decoder=decoder)
2425
if aioredis is None:
2526
raise RuntimeError("Please install aioredis")
2627
if StrictVersion(aioredis.__version__).version < (1, 0):
2728
raise RuntimeError("aioredis<1.0 is not supported")
28-
self._encoder = encoder
29-
self._decoder = decoder
3029
self._key_factory = key_factory
3130
if isinstance(redis_pool, aioredis.pool.ConnectionsPool):
3231
warnings.warn(

docs/reference.rst

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ implement both :meth:`~AbstractStorage.load_session` and
154154

155155
.. class:: AbstractStorage(cookie_name="AIOHTTP_SESSION", *, \
156156
domain=None, max_age=None, path='/', \
157-
secure=None, httponly=True)
157+
secure=None, httponly=True, \
158+
encoder=json.dumps, decoder=json.loads)
158159

159160
Base class for session storage implementations.
160161

@@ -175,6 +176,14 @@ implement both :meth:`~AbstractStorage.load_session` and
175176
*httponly* -- cookie's http-only flag, :class:`bool` or ``None`` (the
176177
same as ``False``).
177178

179+
*encoder* -- session serializer.
180+
A callable with the following signature: `def encode(param: Any) -> str: ...`.
181+
Default is :func:`json.dumps`.
182+
183+
*decoder* -- session deserializer.
184+
A callable with the following signature: `def decode(param: str) -> Any: ...`.
185+
Default is :func:`json.loads`.
186+
178187
.. attribute:: max_age
179188

180189
Maximum age for session data, :class:`int` seconds or ``None``
@@ -189,6 +198,18 @@ implement both :meth:`~AbstractStorage.load_session` and
189198
:class:`dict` of cookie params: *domain*, *max_age*, *path*,
190199
*secure* and *httponly*.
191200

201+
.. attribute:: encoder
202+
203+
The JSON serializer that will be used to dump session cookie data.
204+
205+
.. versionadded:: 2.3
206+
207+
.. attribute:: decoder
208+
209+
The JSON deserializer that will be used to load session cookie data.
210+
211+
.. versionadded:: 2.3
212+
192213
.. method:: load_session(request)
193214

194215
An *abstract* :ref:`coroutine<coroutine>`, called by internal
@@ -217,7 +238,6 @@ implement both :meth:`~AbstractStorage.load_session` and
217238
*max_age* is cookie lifetime given from session. Storage defailt
218239
is used if the value is ``None``.
219240

220-
221241
Simple Storage
222242
--------------
223243

@@ -235,7 +255,8 @@ To use the storage you should push it into
235255
.. class:: SimpleCookieStorage(*, \
236256
cookie_name="AIOHTTP_SESSION", \
237257
domain=None, max_age=None, path='/', \
238-
secure=None, httponly=True)
258+
secure=None, httponly=True, \
259+
encoder=json.dumps, decoder=json.loads)
239260

240261
Create unencrypted cookie storage.
241262

@@ -265,7 +286,8 @@ To use the storage you should push it into
265286
.. class:: EncryptedCookieStorage(secret_key, *, \
266287
cookie_name="AIOHTTP_SESSION", \
267288
domain=None, max_age=None, path='/', \
268-
secure=None, httponly=True)
289+
secure=None, httponly=True, \
290+
encoder=json.dumps, decoder=json.loads)
269291

270292
Create encryted cookies storage.
271293

@@ -303,7 +325,8 @@ To use the storage you should push it into
303325
.. class:: NaClCookieStorage(secret_key, *, \
304326
cookie_name="AIOHTTP_SESSION", \
305327
domain=None, max_age=None, path='/', \
306-
secure=None, httponly=True)
328+
secure=None, httponly=True, \
329+
encoder=json.dumps, decoder=json.loads)
307330

308331
Create encryted cookies storage.
309332

@@ -339,9 +362,8 @@ To use the storage you need setup it first::
339362
cookie_name="AIOHTTP_SESSION", \
340363
domain=None, max_age=None, path='/', \
341364
secure=None, httponly=True, \
342-
encoder=json.dumps, \
343-
decoder=json.loads, \
344-
key_factory=lambda: uuid.uuid4().hex)
365+
key_factory=lambda: uuid.uuid4().hex, \
366+
encoder=json.dumps, decoder=json.loads)
345367

346368
Create Redis storage for user session data.
347369

@@ -377,9 +399,8 @@ To use the storage you need setup it first::
377399
cookie_name="AIOHTTP_SESSION", \
378400
domain=None, max_age=None, path='/', \
379401
secure=None, httponly=True, \
380-
encoder=json.dumps, \
381-
decoder=json.loads, \
382-
key_factory=lambda: uuid.uuid4().hex)
402+
key_factory=lambda: uuid.uuid4().hex, \
403+
encoder=json.dumps, decoder=json.loads)
383404

384405
Create Memcached storage for user session data.
385406

tests/conftest.py

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66
import time
77
import uuid
88
from docker import Client as DockerClient
9+
import socket
10+
11+
12+
@pytest.fixture(scope='session')
13+
def unused_port():
14+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
15+
s.bind(("", 0))
16+
s.listen(1)
17+
port = s.getsockname()[1]
18+
s.close()
19+
return port
920

1021

1122
@pytest.fixture(scope='session')
@@ -43,20 +54,39 @@ def pytest_addoption(parser):
4354
def redis_server(docker, session_id, loop, request):
4455
if not request.config.option.no_pull:
4556
docker.pull('redis:{}'.format('latest'))
57+
58+
"""
4659
container = docker.create_container(
4760
image='redis:{}'.format('latest'),
4861
name='redis-test-server-{}-{}'.format('latest', session_id),
4962
ports=[6379],
5063
detach=True,
5164
)
65+
"""
66+
67+
container_args = dict(
68+
image='redis:{}'.format('latest'),
69+
name='redis-test-server-{}-{}'.format('latest', session_id),
70+
ports=[6379],
71+
detach=True,
72+
)
73+
74+
# bound IPs do not work on OSX
75+
host = "127.0.0.1"
76+
host_port = unused_port()
77+
container_args['host_config'] = docker.create_host_config(
78+
port_bindings={6379: (host, host_port)})
79+
80+
container = docker.create_container(**container_args)
81+
5282
docker.start(container=container['Id'])
53-
inspection = docker.inspect_container(container['Id'])
54-
host = inspection['NetworkSettings']['IPAddress']
83+
# inspection = docker.inspect_container(container['Id'])
84+
# host = inspection['NetworkSettings']['IPAddress']
5585
delay = 0.001
5686
for i in range(100):
5787
try:
5888
conn = loop.run_until_complete(
59-
aioredis.create_connection((host, 6379), loop=loop)
89+
aioredis.create_connection((host, host_port), loop=loop)
6090
)
6191
loop.run_until_complete(conn.execute('SET', 'foo', 'bar'))
6292
break
@@ -65,7 +95,7 @@ def redis_server(docker, session_id, loop, request):
6595
delay *= 2
6696
else:
6797
pytest.fail("Cannot start redis server")
68-
container['redis_params'] = dict(address=(host, 6379))
98+
container['redis_params'] = dict(address=(host, host_port))
6999
yield container
70100

71101
docker.kill(container=container['Id'])
@@ -100,27 +130,44 @@ async def start(*args, no_loop=False, **kwargs):
100130
def memcached_server(docker, session_id, loop, request):
101131
if not request.config.option.no_pull:
102132
docker.pull('memcached:{}'.format('latest'))
133+
134+
"""
103135
container = docker.create_container(
104136
image='memcached:{}'.format('latest'),
105137
name='memcached-test-server-{}-{}'.format('latest', session_id),
106138
ports=[11211],
107139
detach=True,
108140
)
141+
"""
142+
143+
container_args = dict(
144+
image='memcached:{}'.format('latest'),
145+
name='memcached-test-server-{}-{}'.format('latest', session_id),
146+
ports=[11211],
147+
detach=True,
148+
)
149+
150+
# bound IPs do not work on OSX
151+
host = "127.0.0.1"
152+
host_port = unused_port()
153+
container_args['host_config'] = docker.create_host_config(
154+
port_bindings={11211: (host, host_port)})
155+
container = docker.create_container(**container_args)
109156
docker.start(container=container['Id'])
110-
inspection = docker.inspect_container(container['Id'])
111-
host = inspection['NetworkSettings']['IPAddress']
157+
# inspection = docker.inspect_container(container['Id'])
158+
# host = inspection['NetworkSettings']['IPAddress']
112159
delay = 0.001
113160
for i in range(100):
114161
try:
115-
conn = aiomcache.Client(host, 11211, loop=loop)
162+
conn = aiomcache.Client(host, host_port, loop=loop)
116163
loop.run_until_complete(conn.set(b'foo', b'bar'))
117164
break
118165
except ConnectionRefusedError as e:
119166
time.sleep(delay)
120167
delay *= 2
121168
else:
122169
pytest.fail("Cannot start memcached server")
123-
container['memcached_params'] = dict(host=host, port=11211)
170+
container['memcached_params'] = dict(host=host, port=host_port)
124171
yield container
125172

126173
docker.kill(container=container['Id'])

0 commit comments

Comments
 (0)