mirror of
https://github.com/jazzband/django-constance.git
synced 2026-03-16 22:40:24 +00:00
Collections support (#581)
--------- Co-authored-by: Sebastian Manger <manger@netcloud.ch>
This commit is contained in:
parent
31c9e8d043
commit
bb0dc4676f
8 changed files with 93 additions and 15 deletions
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
@ -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}')
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ configuration values. By default it uses the Redis backend. To override
|
|||
the default please set the :setting:`CONSTANCE_BACKEND` setting to the appropriate
|
||||
dotted path.
|
||||
|
||||
Configuration values are stored in JSON format and automatically serialized/deserialized
|
||||
on access.
|
||||
|
||||
Redis
|
||||
-----
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@ The supported types are:
|
|||
* ``datetime``
|
||||
* ``date``
|
||||
* ``time``
|
||||
* ``list``
|
||||
* ``dict``
|
||||
|
||||
For example, to force a value to be handled as a string:
|
||||
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ CONSTANCE_ADDITIONAL_FIELDS = {
|
|||
],
|
||||
# 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
|
||||
|
|
@ -68,6 +70,19 @@ CONSTANCE_CONFIG = {
|
|||
'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
|
||||
|
|
|
|||
|
|
@ -25,6 +25,18 @@ class StorageTestsMixin:
|
|||
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
|
||||
|
|
@ -38,6 +50,8 @@ class StorageTestsMixin:
|
|||
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)
|
||||
|
|
@ -51,6 +65,8 @@ class StorageTestsMixin:
|
|||
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')
|
||||
|
|
|
|||
|
|
@ -30,19 +30,21 @@ class CliTestCase(TransactionTestCase):
|
|||
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()
|
||||
),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class TestJSONSerialization(TestCase):
|
|||
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"}')
|
||||
|
|
@ -37,6 +39,14 @@ class TestJSONSerialization(TestCase):
|
|||
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,
|
||||
|
|
@ -49,6 +59,8 @@ class TestJSONSerialization(TestCase):
|
|||
self.boolean,
|
||||
self.none,
|
||||
self.timedelta,
|
||||
self.dict,
|
||||
self.list,
|
||||
):
|
||||
self.assertEqual(t, loads(dumps(t)))
|
||||
|
||||
|
|
@ -88,3 +100,14 @@ class TestJSONSerialization(TestCase):
|
|||
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)))
|
||||
|
|
|
|||
|
|
@ -51,5 +51,14 @@ class UtilsTestCase(TestCase):
|
|||
'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,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue