Skip to content

Commit b0372f6

Browse files
Replace aioredis with redis 4.3 or higher (#701)
* Change aioredis extra dependency to use redis-py 4.3 or later - redis-py now includes the aioredis as an official asyncio client since 4.2.0. * Adapt existing codes to redis 4.3 - Drop use of distutils (deprecated in Python 3.10) * Add news fragment * Apply review comments - Since this module encodes/decodes the get/set data explicitly, I set the generic type parameter of Redis to bytes. - As you've made as a PR to typeshed, VERSION is not recognized by mypy currently. Temporarily let mypy ignore the check error. * Fix mistake * Update test mocking * Update types-redis to remove a 'type: ignore' directive * Fix type errors following types-redis 4.3.1 * Upgrade the development requirements * Reveal type of the encoded value * I don't prefer the Hungarian notation, but let's allow to avoid reusing a single variable for multiple different types. * Now redis-py explicitly declares VERSION as an int-tuple. * ref: https://github.com/redis/redis-py/blob/bea7299/redis/__init__.py#L54 * Fix further type errors in test suite * Apply `__future__.annotations` flag * Fix another missing lint error * Update tests/test_redis_storage.py * Update aiohttp_session/redis_storage.py * Update redis_storage.py * Update test_redis_storage.py Co-authored-by: Sam Bull <[email protected]>
1 parent 118deb3 commit b0372f6

File tree

8 files changed

+50
-53
lines changed

8 files changed

+50
-53
lines changed

.isort.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ include_trailing_comma=True
44
multi_line_output=3
55
force_grid_wrap=0
66
combine_as_imports=True
7+
known_first_party=aiohttp_session

aiohttp_session/redis_storage.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import json
22
import uuid
3-
from distutils.version import StrictVersion
43
from typing import Any, Callable, Optional
54

65
from aiohttp import web
76

87
from . import AbstractStorage, Session
98

109
try:
11-
import aioredis
10+
from redis import asyncio as aioredis
11+
from redis import VERSION as REDIS_VERSION
1212
except ImportError: # pragma: no cover
1313
aioredis = None # type: ignore[assignment]
1414

@@ -18,7 +18,7 @@ class RedisStorage(AbstractStorage):
1818

1919
def __init__(
2020
self,
21-
redis_pool: "aioredis.Redis",
21+
redis_pool: "aioredis.Redis[bytes]",
2222
*,
2323
cookie_name: str = "AIOHTTP_SESSION",
2424
domain: Optional[str] = None,
@@ -43,13 +43,13 @@ def __init__(
4343
decoder=decoder,
4444
)
4545
if aioredis is None:
46-
raise RuntimeError("Please install aioredis")
46+
raise RuntimeError("Please install redis")
4747
# May have installed aioredis separately (without aiohttp-session[aioredis]).
48-
if StrictVersion(aioredis.__version__).version < (2, 0):
49-
raise RuntimeError("aioredis<2.0 is not supported")
48+
if REDIS_VERSION < (4, 3):
49+
raise RuntimeError("redis<4.3 is not supported")
5050
self._key_factory = key_factory
5151
if not isinstance(redis_pool, aioredis.Redis):
52-
raise TypeError(f"Expected aioredis.Redis got {type(redis_pool)}")
52+
raise TypeError(f"Expected redis.asyncio.Redis got {type(redis_pool)}")
5353
self._redis = redis_pool
5454

5555
async def load_session(self, request: web.Request) -> Session:
@@ -58,12 +58,12 @@ async def load_session(self, request: web.Request) -> Session:
5858
return Session(None, data=None, new=True, max_age=self.max_age)
5959
else:
6060
key = str(cookie)
61-
data = await self._redis.get(self.cookie_name + "_" + key)
62-
if data is None:
61+
data_bytes = await self._redis.get(self.cookie_name + "_" + key)
62+
if data_bytes is None:
6363
return Session(None, data=None, new=True, max_age=self.max_age)
64-
data = data.decode("utf-8")
64+
data_str = data_bytes.decode("utf-8")
6565
try:
66-
data = self._decoder(data)
66+
data = self._decoder(data_str)
6767
except ValueError:
6868
data = None
6969
return Session(key, data=data, new=False, max_age=self.max_age)
@@ -82,9 +82,9 @@ async def save_session(
8282
key = str(key)
8383
self.save_cookie(response, key, max_age=session.max_age)
8484

85-
data = self._encoder(self._get_session_data(session))
85+
data_str = self._encoder(self._get_session_data(session))
8686
await self._redis.set(
8787
self.cookie_name + "_" + key,
88-
data,
88+
data_str,
8989
ex=session.max_age,
9090
)

changes/701.fix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace aioredis with redis 4.3 or higher in favor of the official asyncio support

demo/redis_storage.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ async def handler(request: web.Request) -> web.Response:
1818

1919
async def redis_pool(app: web.Application) -> AsyncIterator[None]:
2020
redis_address = "redis://127.0.0.1:6379"
21-
async with aioredis.from_url( # type: ignore[no-untyped-call]
21+
async with aioredis.from_url(
2222
redis_address,
2323
timeout=1,
2424
) as redis:

requirements-dev.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-e .
22
aiohttp==3.8.3
33
aiomcache==0.7.0
4-
aioredis==2.0.1
4+
redis==4.3.3
55
attrs==22.1.0
66
chardet==5.0.0
77
cryptography==38.0.1
@@ -22,5 +22,6 @@ pytest-cov==4.0.0
2222
pytest-mock==3.10.0
2323
pytest-sugar==0.9.5
2424
sphinx==4.3.2
25+
types-redis==4.3.1
2526
typing-extensions==4.4.0
2627
yarl==1.8.1

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def read(f):
2121

2222
install_requires = ["aiohttp>=3.8", 'typing_extensions>=3.7.4; python_version<"3.8"']
2323
extras_require = {
24-
"aioredis": ["aioredis>=2.0.0"],
24+
"aioredis": ["redis>=4.3.1"],
2525
"aiomcache": ["aiomcache>=0.5.2"],
2626
"pycrypto": ["cryptography"],
2727
"secure": ["cryptography"],

tests/conftest.py

+8-10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
import asyncio
24
import gc
35
import socket
@@ -7,20 +9,16 @@
79
from typing import Iterator
810

911
import aiomcache
10-
import aioredis
1112
import pytest
1213
from docker import DockerClient, from_env as docker_from_env, models as docker_models
14+
from redis import asyncio as aioredis
1315

1416
if sys.version_info >= (3, 8):
1517
from typing import TypedDict
1618
else:
1719
from typing_extensions import TypedDict
1820

1921

20-
# TODO: Remove once fixed: https://github.com/aio-libs/aioredis-py/issues/1115
21-
aioredis.Redis.__del__ = lambda *args: None # type: ignore
22-
23-
2422
class _ContainerInfo(TypedDict):
2523
host: str
2624
port: int
@@ -106,10 +104,10 @@ def redis_server( # type: ignore[misc] # No docker types.
106104
delay = 0.1
107105
for _i in range(20):
108106
try:
109-
conn = aioredis.from_url(f"redis://{host}:{port}") # type: ignore[no-untyped-call] # noqa
107+
conn = aioredis.from_url(f"redis://{host}:{port}")
110108
event_loop.run_until_complete(conn.set("foo", "bar"))
111109
break
112-
except aioredis.exceptions.ConnectionError:
110+
except aioredis.ConnectionError:
113111
time.sleep(delay)
114112
delay *= 2
115113
finally:
@@ -134,15 +132,15 @@ def redis_url(redis_server: _ContainerInfo) -> str: # type: ignore[misc]
134132
def redis(
135133
event_loop: asyncio.AbstractEventLoop,
136134
redis_url: str,
137-
) -> Iterator[aioredis.Redis]:
138-
async def start(pool: aioredis.ConnectionPool) -> aioredis.Redis:
135+
) -> Iterator[aioredis.Redis[bytes]]:
136+
async def start(pool: aioredis.ConnectionPool) -> aioredis.Redis[bytes]:
139137
return aioredis.Redis(connection_pool=pool)
140138

141139
asyncio.set_event_loop(event_loop)
142140
pool = aioredis.ConnectionPool.from_url(redis_url)
143141
redis = event_loop.run_until_complete(start(pool))
144142
yield redis
145-
event_loop.run_until_complete(redis.close()) # type: ignore[no-untyped-call]
143+
event_loop.run_until_complete(redis.close())
146144
event_loop.run_until_complete(pool.disconnect())
147145

148146

tests/test_redis_storage.py

+23-27
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
from __future__ import annotations
2+
13
import asyncio
24
import json
35
import time
46
import uuid
57
from typing import Any, Callable, Dict, MutableMapping, Optional, cast
68

7-
import aioredis
89
import pytest
910
from aiohttp import web
1011
from aiohttp.test_utils import TestClient
1112
from pytest_mock import MockFixture
13+
from redis import asyncio as aioredis
1214

1315
from aiohttp_session import Handler, Session, get_session, session_middleware
1416
from aiohttp_session.redis_storage import RedisStorage
@@ -18,7 +20,7 @@
1820

1921
def create_app(
2022
handler: Handler,
21-
redis: aioredis.Redis,
23+
redis: aioredis.Redis[bytes],
2224
max_age: Optional[int] = None,
2325
key_factory: Callable[[], str] = lambda: uuid.uuid4().hex,
2426
) -> web.Application:
@@ -31,7 +33,7 @@ def create_app(
3133

3234

3335
async def make_cookie(
34-
client: TestClient, redis: aioredis.Redis, data: Dict[Any, Any]
36+
client: TestClient, redis: aioredis.Redis[bytes], data: Dict[Any, Any]
3537
) -> None:
3638
session_data = {"session": data, "created": int(time.time())}
3739
value = json.dumps(session_data)
@@ -40,23 +42,21 @@ async def make_cookie(
4042
client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": key})
4143

4244

43-
async def make_cookie_with_bad_value(client: TestClient, redis: aioredis.Redis) -> None:
45+
async def make_cookie_with_bad_value(client: TestClient, redis: aioredis.Redis[bytes]) -> None:
4446
key = uuid.uuid4().hex
4547
await redis.set("AIOHTTP_SESSION_" + key, "")
4648
client.session.cookie_jar.update_cookies({"AIOHTTP_SESSION": key})
4749

4850

49-
async def load_cookie(client: TestClient, redis: aioredis.Redis) -> Any:
51+
async def load_cookie(client: TestClient, redis: aioredis.Redis[bytes]) -> Any:
5052
cookies = client.session.cookie_jar.filter_cookies(client.make_url("/"))
5153
key = cookies["AIOHTTP_SESSION"]
52-
encoded = await redis.get("AIOHTTP_SESSION_" + key.value)
53-
s = encoded.decode("utf-8")
54-
value = json.loads(s)
55-
return value
54+
value_bytes = await redis.get("AIOHTTP_SESSION_" + key.value)
55+
return None if value_bytes is None else json.loads(value_bytes)
5656

5757

5858
async def test_create_new_session(
59-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
59+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
6060
) -> None:
6161
async def handler(request: web.Request) -> web.StreamResponse:
6262
session = await get_session(request)
@@ -72,7 +72,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
7272

7373

7474
async def test_load_existing_session(
75-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
75+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
7676
) -> None:
7777
async def handler(request: web.Request) -> web.StreamResponse:
7878
session = await get_session(request)
@@ -89,7 +89,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
8989

9090

9191
async def test_load_bad_session(
92-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
92+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
9393
) -> None:
9494
async def handler(request: web.Request) -> web.StreamResponse:
9595
session = await get_session(request)
@@ -106,7 +106,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
106106

107107

108108
async def test_change_session(
109-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
109+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
110110
) -> None:
111111
async def handler(request: web.Request) -> web.StreamResponse:
112112
session = await get_session(request)
@@ -133,7 +133,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
133133

134134

135135
async def test_clear_cookie_on_session_invalidation(
136-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
136+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
137137
) -> None:
138138
async def handler(request: web.Request) -> web.StreamResponse:
139139
session = await get_session(request)
@@ -154,7 +154,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
154154

155155

156156
async def test_create_cookie_in_handler(
157-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
157+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
158158
) -> None:
159159
async def handler(request: web.Request) -> web.StreamResponse:
160160
session = await get_session(request)
@@ -181,7 +181,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
181181

182182

183183
async def test_set_ttl_on_session_saving(
184-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
184+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
185185
) -> None:
186186
async def handler(request: web.Request) -> web.StreamResponse:
187187
session = await get_session(request)
@@ -201,7 +201,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
201201

202202

203203
async def test_set_ttl_manually_set(
204-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
204+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
205205
) -> None:
206206
async def handler(request: web.Request) -> web.StreamResponse:
207207
session = await get_session(request)
@@ -222,7 +222,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
222222

223223

224224
async def test_create_new_session_if_key_doesnt_exists_in_redis(
225-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
225+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
226226
) -> None:
227227
async def handler(request: web.Request) -> web.StreamResponse:
228228
session = await get_session(request)
@@ -236,7 +236,7 @@ async def handler(request: web.Request) -> web.StreamResponse:
236236

237237

238238
async def test_create_storage_with_custom_key_factory(
239-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
239+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
240240
) -> None:
241241
async def handler(request: web.Request) -> web.StreamResponse:
242242
session = await get_session(request)
@@ -259,7 +259,7 @@ def key_factory() -> str:
259259

260260

261261
async def test_redis_session_fixation(
262-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
262+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
263263
) -> None:
264264
async def login(request: web.Request) -> web.StreamResponse:
265265
session = await get_session(request)
@@ -288,7 +288,7 @@ async def test_redis_from_create_pool(redis_url: str) -> None:
288288
async def handler(request: web.Request) -> web.StreamResponse:
289289
pass
290290

291-
redis = aioredis.from_url(redis_url) # type: ignore[no-untyped-call]
291+
redis = aioredis.from_url(redis_url)
292292
create_app(handler=handler, redis=redis)
293293
await redis.close()
294294

@@ -314,17 +314,13 @@ async def test_old_aioredis_version(mocker: MockFixture) -> None:
314314
async def handler(request: web.Request) -> web.StreamResponse:
315315
pass
316316

317-
class Dummy:
318-
def __init__(self, *args: object, **kwargs: object) -> None:
319-
self.version = (0, 3)
320-
321-
mocker.patch("aiohttp_session.redis_storage.StrictVersion", Dummy)
317+
mocker.patch("aiohttp_session.redis_storage.REDIS_VERSION", (0, 3, "dev0"))
322318
with pytest.raises(RuntimeError):
323319
create_app(handler=handler, redis=None) # type: ignore[arg-type]
324320

325321

326322
async def test_load_session_dont_load_expired_session(
327-
aiohttp_client: AiohttpClient, redis: aioredis.Redis
323+
aiohttp_client: AiohttpClient, redis: aioredis.Redis[bytes]
328324
) -> None:
329325
async def handler(request: web.Request) -> web.StreamResponse:
330326
session = await get_session(request)

0 commit comments

Comments
 (0)