Skip to content

Commit ec1421c

Browse files
fix: cleanup serializer (#399)
serialize_partially and deserialize_partially had a dead code path, using "json dump shapes" to determine to json dump something. However, the first if checking for str, float, or int already handles that. This dead code was found using hypothesis testing, included in the new tests/test_abstract.py. In the future, I'd like to augment the hardcoded test data in test_pydantic_aioredis with hypothesis data
1 parent 6390c53 commit ec1421c

File tree

8 files changed

+134
-29
lines changed

8 files changed

+134
-29
lines changed

.github/workflows/tests.yml

+2
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ jobs:
9292
${{ steps.pre-commit-cache.outputs.result }}-
9393
9494
- name: Run Nox
95+
env:
96+
HYPOTHESIS_PROFILE: ci
9597
run: |
9698
nox --force-color --python=${{ matrix.python }}
9799

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ coverage.xml
1111
__pycache__/
1212
examples/benchmarks/.benchmarks
1313
examples/benchmarks/.coverage
14+
.hypothesis

poetry.lock

+34-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pydantic_aioredis/abstract.py

-28
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import json
33
from datetime import date
44
from datetime import datetime
5-
from enum import Enum
65
from ipaddress import IPv4Address
76
from ipaddress import IPv4Network
87
from ipaddress import IPv6Address
@@ -15,32 +14,9 @@
1514
from uuid import UUID
1615

1716
from pydantic import BaseModel
18-
from pydantic.fields import SHAPE_DEFAULTDICT
19-
from pydantic.fields import SHAPE_DICT
20-
from pydantic.fields import SHAPE_FROZENSET
21-
from pydantic.fields import SHAPE_LIST
22-
from pydantic.fields import SHAPE_MAPPING
23-
from pydantic.fields import SHAPE_SEQUENCE
24-
from pydantic.fields import SHAPE_SET
25-
from pydantic.fields import SHAPE_TUPLE
26-
from pydantic.fields import SHAPE_TUPLE_ELLIPSIS
2717
from pydantic_aioredis.config import RedisConfig
2818
from redis import asyncio as aioredis
2919

30-
# JSON_DUMP_SHAPES are object types that are serialized to JSON using json.dumps
31-
JSON_DUMP_SHAPES = (
32-
SHAPE_LIST,
33-
SHAPE_SET,
34-
SHAPE_MAPPING,
35-
SHAPE_TUPLE,
36-
SHAPE_TUPLE_ELLIPSIS,
37-
SHAPE_SEQUENCE,
38-
SHAPE_FROZENSET,
39-
SHAPE_DICT,
40-
SHAPE_DEFAULTDICT,
41-
Enum,
42-
)
43-
4420
# STR_DUMP_SHAPES are object types that are serialized to strings using str(obj)
4521
# They are stored in redis as strings and rely on pydantic to deserialize them
4622
STR_DUMP_SHAPES = (IPv4Address, IPv4Network, IPv6Address, IPv6Network, UUID)
@@ -113,8 +89,6 @@ def serialize_partially(cls, data: Dict[str, Any]):
11389
continue
11490
if cls.__fields__[field].type_ not in [str, float, int]:
11591
data[field] = json.dumps(data[field], default=cls.json_default)
116-
if getattr(cls.__fields__[field], "shape", None) in JSON_DUMP_SHAPES:
117-
data[field] = json.dumps(data[field], default=cls.json_default)
11892
if getattr(cls.__fields__[field], "allow_none", False):
11993
if data[field] is None:
12094
data[field] = "None"
@@ -133,8 +107,6 @@ def deserialize_partially(cls, data: Dict[bytes, Any]):
133107
continue
134108
if cls.__fields__[field].type_ not in [str, float, int]:
135109
data[field] = json.loads(data[field], object_hook=cls.json_object_hook)
136-
if getattr(cls.__fields__[field], "shape", None) in JSON_DUMP_SHAPES:
137-
data[field] = json.loads(data[field], object_hook=cls.json_object_hook)
138110
if getattr(cls.__fields__[field], "allow_none", False):
139111
if data[field] == "None":
140112
data[field] = None

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pylint = "^2.13.9"
7575
setuptools-git-versioning = "^1.13.1"
7676
bandit = "^1.7.4"
7777
fakeredis = {extras = ["json"], version = "2.2.0"}
78+
hypothesis = "^6.61.0"
7879

7980
[tool.coverage.paths]
8081
source = ["pydantic_aioredis", "*/site-packages"]

test/conftest.py

+10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ async def redis_store():
2424
def pytest_configure(config):
2525
"""Configure our markers"""
2626
config.addinivalue_line("markers", "union_test: Tests for union types")
27+
config.addinivalue_line("markers", "hypothesis: Tests that use hypothesis")
2728

2829

2930
@pytest.hookimpl(trylast=True)
@@ -32,3 +33,12 @@ def pytest_collection_modifyitems(config, items):
3233
for item in items:
3334
if inspect.iscoroutinefunction(item.function):
3435
item.add_marker(pytest.mark.asyncio)
36+
37+
38+
import os
39+
from hypothesis import settings, Verbosity
40+
41+
settings.register_profile("ci", max_examples=5000)
42+
settings.register_profile("dev", max_examples=100)
43+
settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose)
44+
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "dev"))

test/test_abstract.py

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
"""Test methods in abstract.py. Uses hypothesis"""
2+
import json
3+
from datetime import date
4+
from datetime import datetime
5+
from ipaddress import IPv4Address
6+
from ipaddress import IPv6Address
7+
from typing import Dict
8+
from typing import List
9+
from typing import Tuple
10+
from typing import Union
11+
12+
import pytest
13+
from hypothesis import given
14+
from hypothesis import strategies as st
15+
from pydantic_aioredis.model import Model
16+
17+
18+
class SimpleModel(Model):
19+
_primary_key_field: str = "test_str"
20+
test_str: str
21+
test_int: int
22+
test_float: float
23+
test_date: date
24+
test_datetime: datetime
25+
test_ip_v4: IPv4Address
26+
test_ip_v6: IPv6Address
27+
test_list: List
28+
test_dict: Dict[str, Union[int, float]]
29+
test_tuple: Tuple[str]
30+
31+
32+
parameters = [
33+
(st.text, [], {}, "test_str", None),
34+
(st.integers, [], {}, "test_int", None),
35+
(st.floats, [], {"allow_nan": False}, "test_float", None),
36+
(st.dates, [], {}, "test_date", lambda x: json.dumps(x.isoformat())),
37+
(st.datetimes, [], {}, "test_datetime", lambda x: json.dumps(x.isoformat())),
38+
(st.ip_addresses, [], {"v": 4}, "test_ip_v4", lambda x: json.dumps(str(x))),
39+
(st.ip_addresses, [], {"v": 6}, "test_ip_v4", lambda x: json.dumps(str(x))),
40+
(
41+
st.lists,
42+
[st.tuples(st.integers(), st.floats())],
43+
{},
44+
"test_list",
45+
lambda x: json.dumps(x),
46+
),
47+
(
48+
st.dictionaries,
49+
[st.text(), st.tuples(st.integers(), st.floats())],
50+
{},
51+
"test_dict",
52+
lambda x: json.dumps(x),
53+
),
54+
(st.tuples, [st.text()], {}, "test_tuple", lambda x: json.dumps(x)),
55+
]
56+
57+
58+
@pytest.mark.parametrize(
59+
"strategy, strategy_args, strategy_kwargs, model_field, serialize_callable",
60+
parameters,
61+
)
62+
@given(st.data())
63+
def test_serialize_partially(
64+
strategy, strategy_args, strategy_kwargs, model_field, serialize_callable, data
65+
):
66+
value = data.draw(strategy(*strategy_args, **strategy_kwargs))
67+
serialized = SimpleModel.serialize_partially({model_field: value})
68+
if serialize_callable is None:
69+
assert serialized[model_field] == value
70+
else:
71+
assert serialized[model_field] == serialize_callable(value)
72+
73+
74+
def test_serialize_partially_skip_missing_filed():
75+
serialized = SimpleModel.serialize_partially({"unknown": "test"})
76+
assert serialized["unknown"] == "test"

test/test_pydantic_aioredis.py

+10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import pytest
1414
import pytest_asyncio
1515
from fakeredis.aioredis import FakeRedis
16+
from pydantic_aioredis.abstract import _AbstractModel
1617
from pydantic_aioredis.config import RedisConfig
1718
from pydantic_aioredis.model import Model
1819
from pydantic_aioredis.store import Store
@@ -191,6 +192,15 @@ def test_store_model(redis_store):
191192
redis_store.model("Notabook")
192193

193194

195+
def test_json_object_hook():
196+
class TestObj:
197+
def __init__(self, value: str):
198+
self.value = value
199+
200+
test_obj = TestObj("test")
201+
assert test_obj.value == _AbstractModel.json_object_hook(test_obj).value
202+
203+
194204
parameters = [
195205
(pytest.lazy_fixture("redis_store"), books, Book, "book:"),
196206
(pytest.lazy_fixture("redis_store"), extended_books, ExtendedBook, "extendedbook:"),

0 commit comments

Comments
 (0)