Skip to content

Commit

Permalink
Collections support
Browse files Browse the repository at this point in the history
  • Loading branch information
Mogost committed Sep 3, 2024
1 parent 31c9e8d commit c06c599
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 15 deletions.
12 changes: 10 additions & 2 deletions constance/codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,20 @@ def _as(discriminator: str, v: Any) -> dict[str, Any]:
def dumps(obj, _dumps=json.dumps, cls=JSONEncoder, default_kwargs=None, **kwargs):
"""Serialize object to json string."""
default_kwargs = default_kwargs or {}
is_default_type = isinstance(obj, (str, int, bool, float, type(None)))
is_default_type = isinstance(obj, (list, dict, str, int, bool, float, type(None)))
return _dumps(
_as(DEFAULT_DISCRIMINATOR, obj) if is_default_type else obj, cls=cls, **dict(default_kwargs, **kwargs)
)


def loads(s, _loads=json.loads, **kwargs):
def loads(s, _loads=json.loads, *, first_level=True, **kwargs):
"""Deserialize json string to object."""
if first_level:
return _loads(s, object_hook=object_hook, **kwargs)
if isinstance(s, dict) and '__type__' not in s and '__value__' not in s:
return {k: loads(v, first_level=False) for k, v in s.items()}
if isinstance(s, list):
return list(loads(v, first_level=False) for v in s)
return _loads(s, object_hook=object_hook, **kwargs)


Expand All @@ -54,6 +60,8 @@ def object_hook(o: dict) -> Any:
if not codec:
raise ValueError(f'Unsupported type: {o["__type__"]}')
return codec[1](o['__value__'])
if '__type__' not in o and '__value__' not in o:
return o
logger.error('Cannot deserialize object: %s', o)
raise ValueError(f'Invalid object: {o}')

Expand Down
15 changes: 15 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
],
# note this intentionally uses a tuple so that we can test immutable
'email': ('django.forms.fields.EmailField',),
'array': ['django.forms.fields.CharField', {'widget': 'django.forms.Textarea'}],
'json': ['django.forms.fields.CharField', {'widget': 'django.forms.Textarea'}],
}

USE_TZ = True
Expand All @@ -68,6 +70,19 @@
'CHOICE_VALUE': ('yes', 'select yes or no', 'yes_no_null_select'),
'LINEBREAK_VALUE': ('Spam spam', 'eggs\neggs'),
'EMAIL_VALUE': ('test@example.com', 'An email', 'email'),
'LIST_VALUE': ([1, '1', date(2019, 1, 1)], 'A list', 'array'),
'JSON_VALUE': (
{
'key': 'value',
'key2': 2,
'key3': [1, 2, 3],
'key4': {'key': 'value'},
'key5': date(2019, 1, 1),
'key6': None,
},
'A JSON object',
'json',
),
}

DEBUG = True
Expand Down
16 changes: 16 additions & 0 deletions tests/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ def test_store(self):
self.assertEqual(self.config.TIMEDELTA_VALUE, timedelta(days=1, hours=2, minutes=3))
self.assertEqual(self.config.CHOICE_VALUE, 'yes')
self.assertEqual(self.config.EMAIL_VALUE, 'test@example.com')
self.assertEqual(self.config.LIST_VALUE, [1, '1', date(2019, 1, 1)])
self.assertEqual(
self.config.JSON_VALUE,
{
'key': 'value',
'key2': 2,
'key3': [1, 2, 3],
'key4': {'key': 'value'},
'key5': date(2019, 1, 1),
'key6': None,
},
)

# set values
self.config.INT_VALUE = 100
Expand All @@ -38,6 +50,8 @@ def test_store(self):
self.config.TIMEDELTA_VALUE = timedelta(days=2, hours=3, minutes=4)
self.config.CHOICE_VALUE = 'no'
self.config.EMAIL_VALUE = 'foo@bar.com'
self.config.LIST_VALUE = [1, date(2020, 2, 2)]
self.config.JSON_VALUE = {'key': 'OK'}

# read again
self.assertEqual(self.config.INT_VALUE, 100)
Expand All @@ -51,6 +65,8 @@ def test_store(self):
self.assertEqual(self.config.TIMEDELTA_VALUE, timedelta(days=2, hours=3, minutes=4))
self.assertEqual(self.config.CHOICE_VALUE, 'no')
self.assertEqual(self.config.EMAIL_VALUE, 'foo@bar.com')
self.assertEqual(self.config.LIST_VALUE, [1, date(2020, 2, 2)])
self.assertEqual(self.config.JSON_VALUE, {'key': 'OK'})

def test_nonexistent(self):
self.assertRaises(AttributeError, getattr, self.config, 'NON_EXISTENT')
Expand Down
28 changes: 15 additions & 13 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,21 @@ def test_list(self):
set(
dedent(
smart_str(
""" BOOL_VALUE True
EMAIL_VALUE test@example.com
INT_VALUE 1
LINEBREAK_VALUE Spam spam
DATE_VALUE 2010-12-24
TIME_VALUE 23:59:59
TIMEDELTA_VALUE 1 day, 2:03:00
STRING_VALUE Hello world
CHOICE_VALUE yes
DECIMAL_VALUE 0.1
DATETIME_VALUE 2010-08-23 11:29:24
FLOAT_VALUE 3.1415926536
"""
""" BOOL_VALUE\tTrue
EMAIL_VALUE\ttest@example.com
INT_VALUE\t1
LINEBREAK_VALUE\tSpam spam
DATE_VALUE\t2010-12-24
TIME_VALUE\t23:59:59
TIMEDELTA_VALUE\t1 day, 2:03:00
STRING_VALUE\tHello world
CHOICE_VALUE\tyes
DECIMAL_VALUE\t0.1
DATETIME_VALUE\t2010-08-23 11:29:24
FLOAT_VALUE\t3.1415926536
JSON_VALUE\t{'key': 'value', 'key2': 2, 'key3': [1, 2, 3], 'key4': {'key': 'value'}, 'key5': datetime.date(2019, 1, 1), 'key6': None}
LIST_VALUE\t[1, '1', datetime.date(2019, 1, 1)]
""" # noqa: E501
)
).splitlines()
),
Expand Down
23 changes: 23 additions & 0 deletions tests/test_codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def setUp(self):
self.boolean = True
self.none = None
self.timedelta = timedelta(days=1, hours=2, minutes=3)
self.list = [1, 2, self.date]
self.dict = {'key': self.date, 'key2': 1}

def test_serializes_and_deserializes_default_types(self):
self.assertEqual(dumps(self.datetime), '{"__type__": "datetime", "__value__": "2023-10-05T15:30:00"}')
Expand All @@ -37,6 +39,14 @@ def test_serializes_and_deserializes_default_types(self):
self.assertEqual(dumps(self.boolean), '{"__type__": "default", "__value__": true}')
self.assertEqual(dumps(self.none), '{"__type__": "default", "__value__": null}')
self.assertEqual(dumps(self.timedelta), '{"__type__": "timedelta", "__value__": 93780.0}')
self.assertEqual(
dumps(self.list),
'{"__type__": "default", "__value__": [1, 2, {"__type__": "date", "__value__": "2023-10-05"}]}',
)
self.assertEqual(
dumps(self.dict),
'{"__type__": "default", "__value__": {"key": {"__type__": "date", "__value__": "2023-10-05"}, "key2": 1}}',
)
for t in (
self.datetime,
self.date,
Expand All @@ -49,6 +59,8 @@ def test_serializes_and_deserializes_default_types(self):
self.boolean,
self.none,
self.timedelta,
self.dict,
self.list,
):
self.assertEqual(t, loads(dumps(t)))

Expand Down Expand Up @@ -88,3 +100,14 @@ def test_register_known_type(self):
register_type(int, 'new_custom_type', lambda o: o.value, lambda o: int(o))
with self.assertRaisesRegex(ValueError, 'Type with discriminator new_custom_type is already registered'):
register_type(int, 'new_custom_type', lambda o: o.value, lambda o: int(o))

def test_nested_collections(self):
data = {'key': [[[[{'key': self.date}]]]]}
self.assertEqual(
dumps(data),
(
'{"__type__": "default", '
'"__value__": {"key": [[[[{"key": {"__type__": "date", "__value__": "2023-10-05"}}]]]]}}'
),
)
self.assertEqual(data, loads(dumps(data)))
9 changes: 9 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,14 @@ def test_get_values(self):
'DECIMAL_VALUE': Decimal('0.1'),
'STRING_VALUE': 'Hello world',
'DATETIME_VALUE': datetime.datetime(2010, 8, 23, 11, 29, 24),
'LIST_VALUE': [1, '1', datetime.date(2019, 1, 1)],
'JSON_VALUE': {
'key': 'value',
'key2': 2,
'key3': [1, 2, 3],
'key4': {'key': 'value'},
'key5': datetime.date(2019, 1, 1),
'key6': None,
},
},
)

0 comments on commit c06c599

Please sign in to comment.