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):
|
def dumps(obj, _dumps=json.dumps, cls=JSONEncoder, default_kwargs=None, **kwargs):
|
||||||
"""Serialize object to json string."""
|
"""Serialize object to json string."""
|
||||||
default_kwargs = default_kwargs or {}
|
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(
|
return _dumps(
|
||||||
_as(DEFAULT_DISCRIMINATOR, obj) if is_default_type else obj, cls=cls, **dict(default_kwargs, **kwargs)
|
_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."""
|
"""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)
|
return _loads(s, object_hook=object_hook, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,6 +60,8 @@ def object_hook(o: dict) -> Any:
|
||||||
if not codec:
|
if not codec:
|
||||||
raise ValueError(f'Unsupported type: {o["__type__"]}')
|
raise ValueError(f'Unsupported type: {o["__type__"]}')
|
||||||
return codec[1](o['__value__'])
|
return codec[1](o['__value__'])
|
||||||
|
if '__type__' not in o and '__value__' not in o:
|
||||||
|
return o
|
||||||
logger.error('Cannot deserialize object: %s', o)
|
logger.error('Cannot deserialize object: %s', o)
|
||||||
raise ValueError(f'Invalid object: {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
|
the default please set the :setting:`CONSTANCE_BACKEND` setting to the appropriate
|
||||||
dotted path.
|
dotted path.
|
||||||
|
|
||||||
|
Configuration values are stored in JSON format and automatically serialized/deserialized
|
||||||
|
on access.
|
||||||
|
|
||||||
Redis
|
Redis
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,8 @@ The supported types are:
|
||||||
* ``datetime``
|
* ``datetime``
|
||||||
* ``date``
|
* ``date``
|
||||||
* ``time``
|
* ``time``
|
||||||
|
* ``list``
|
||||||
|
* ``dict``
|
||||||
|
|
||||||
For example, to force a value to be handled as a string:
|
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
|
# note this intentionally uses a tuple so that we can test immutable
|
||||||
'email': ('django.forms.fields.EmailField',),
|
'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
|
USE_TZ = True
|
||||||
|
|
@ -68,6 +70,19 @@ CONSTANCE_CONFIG = {
|
||||||
'CHOICE_VALUE': ('yes', 'select yes or no', 'yes_no_null_select'),
|
'CHOICE_VALUE': ('yes', 'select yes or no', 'yes_no_null_select'),
|
||||||
'LINEBREAK_VALUE': ('Spam spam', 'eggs\neggs'),
|
'LINEBREAK_VALUE': ('Spam spam', 'eggs\neggs'),
|
||||||
'EMAIL_VALUE': ('test@example.com', 'An email', 'email'),
|
'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
|
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.TIMEDELTA_VALUE, timedelta(days=1, hours=2, minutes=3))
|
||||||
self.assertEqual(self.config.CHOICE_VALUE, 'yes')
|
self.assertEqual(self.config.CHOICE_VALUE, 'yes')
|
||||||
self.assertEqual(self.config.EMAIL_VALUE, 'test@example.com')
|
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
|
# set values
|
||||||
self.config.INT_VALUE = 100
|
self.config.INT_VALUE = 100
|
||||||
|
|
@ -38,6 +50,8 @@ class StorageTestsMixin:
|
||||||
self.config.TIMEDELTA_VALUE = timedelta(days=2, hours=3, minutes=4)
|
self.config.TIMEDELTA_VALUE = timedelta(days=2, hours=3, minutes=4)
|
||||||
self.config.CHOICE_VALUE = 'no'
|
self.config.CHOICE_VALUE = 'no'
|
||||||
self.config.EMAIL_VALUE = 'foo@bar.com'
|
self.config.EMAIL_VALUE = 'foo@bar.com'
|
||||||
|
self.config.LIST_VALUE = [1, date(2020, 2, 2)]
|
||||||
|
self.config.JSON_VALUE = {'key': 'OK'}
|
||||||
|
|
||||||
# read again
|
# read again
|
||||||
self.assertEqual(self.config.INT_VALUE, 100)
|
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.TIMEDELTA_VALUE, timedelta(days=2, hours=3, minutes=4))
|
||||||
self.assertEqual(self.config.CHOICE_VALUE, 'no')
|
self.assertEqual(self.config.CHOICE_VALUE, 'no')
|
||||||
self.assertEqual(self.config.EMAIL_VALUE, 'foo@bar.com')
|
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):
|
def test_nonexistent(self):
|
||||||
self.assertRaises(AttributeError, getattr, self.config, 'NON_EXISTENT')
|
self.assertRaises(AttributeError, getattr, self.config, 'NON_EXISTENT')
|
||||||
|
|
|
||||||
|
|
@ -30,19 +30,21 @@ class CliTestCase(TransactionTestCase):
|
||||||
set(
|
set(
|
||||||
dedent(
|
dedent(
|
||||||
smart_str(
|
smart_str(
|
||||||
""" BOOL_VALUE True
|
""" BOOL_VALUE\tTrue
|
||||||
EMAIL_VALUE test@example.com
|
EMAIL_VALUE\ttest@example.com
|
||||||
INT_VALUE 1
|
INT_VALUE\t1
|
||||||
LINEBREAK_VALUE Spam spam
|
LINEBREAK_VALUE\tSpam spam
|
||||||
DATE_VALUE 2010-12-24
|
DATE_VALUE\t2010-12-24
|
||||||
TIME_VALUE 23:59:59
|
TIME_VALUE\t23:59:59
|
||||||
TIMEDELTA_VALUE 1 day, 2:03:00
|
TIMEDELTA_VALUE\t1 day, 2:03:00
|
||||||
STRING_VALUE Hello world
|
STRING_VALUE\tHello world
|
||||||
CHOICE_VALUE yes
|
CHOICE_VALUE\tyes
|
||||||
DECIMAL_VALUE 0.1
|
DECIMAL_VALUE\t0.1
|
||||||
DATETIME_VALUE 2010-08-23 11:29:24
|
DATETIME_VALUE\t2010-08-23 11:29:24
|
||||||
FLOAT_VALUE 3.1415926536
|
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()
|
).splitlines()
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ class TestJSONSerialization(TestCase):
|
||||||
self.boolean = True
|
self.boolean = True
|
||||||
self.none = None
|
self.none = None
|
||||||
self.timedelta = timedelta(days=1, hours=2, minutes=3)
|
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):
|
def test_serializes_and_deserializes_default_types(self):
|
||||||
self.assertEqual(dumps(self.datetime), '{"__type__": "datetime", "__value__": "2023-10-05T15:30:00"}')
|
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.boolean), '{"__type__": "default", "__value__": true}')
|
||||||
self.assertEqual(dumps(self.none), '{"__type__": "default", "__value__": null}')
|
self.assertEqual(dumps(self.none), '{"__type__": "default", "__value__": null}')
|
||||||
self.assertEqual(dumps(self.timedelta), '{"__type__": "timedelta", "__value__": 93780.0}')
|
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 (
|
for t in (
|
||||||
self.datetime,
|
self.datetime,
|
||||||
self.date,
|
self.date,
|
||||||
|
|
@ -49,6 +59,8 @@ class TestJSONSerialization(TestCase):
|
||||||
self.boolean,
|
self.boolean,
|
||||||
self.none,
|
self.none,
|
||||||
self.timedelta,
|
self.timedelta,
|
||||||
|
self.dict,
|
||||||
|
self.list,
|
||||||
):
|
):
|
||||||
self.assertEqual(t, loads(dumps(t)))
|
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))
|
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'):
|
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))
|
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'),
|
'DECIMAL_VALUE': Decimal('0.1'),
|
||||||
'STRING_VALUE': 'Hello world',
|
'STRING_VALUE': 'Hello world',
|
||||||
'DATETIME_VALUE': datetime.datetime(2010, 8, 23, 11, 29, 24),
|
'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