mirror of
https://github.com/encode/django-rest-framework.git
synced 2026-01-10 18:50:56 +03:00
Merge af7d5f40f1 into 3f190b7ddc
This commit is contained in:
commit
d6e3e2cbd8
|
|
@ -471,12 +471,13 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is
|
|||
|
||||
A field class that validates a list of objects.
|
||||
|
||||
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None)`
|
||||
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None, max_depth=None)`
|
||||
|
||||
* `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
|
||||
* `allow_empty` - Designates if empty lists are allowed.
|
||||
* `min_length` - Validates that the list contains no fewer than this number of elements.
|
||||
* `max_length` - Validates that the list contains no more than this number of elements.
|
||||
* `max_depth` - Validates that nesting depth does not exceed this value. This applies to both the field schema depth and the raw input data depth. A value of 1 permits a flat structure (e.g., `[1, 2, 3]`) but rejects nested data (e.g., `[[1, 2]]`). Defaults to `None` (no limit).
|
||||
|
||||
For example, to validate a list of integers you might use something like the following:
|
||||
|
||||
|
|
@ -495,10 +496,11 @@ We can now reuse our custom `StringListField` class throughout our application,
|
|||
|
||||
A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.
|
||||
|
||||
**Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True)`
|
||||
**Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True, max_depth=None)`
|
||||
|
||||
* `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated.
|
||||
* `allow_empty` - Designates if empty dictionaries are allowed.
|
||||
* `max_depth` - Validates that nesting depth does not exceed this value. This applies to both the field schema depth and the raw input data depth. A value of 1 permits a flat structure (e.g., `{"a": 1}`) but rejects nested data (e.g., `{"a": {"b": 1}}`). Defaults to `None` (no limit).
|
||||
|
||||
For example, to create a field that validates a mapping of strings to strings, you would write something like this:
|
||||
|
||||
|
|
|
|||
|
|
@ -1650,7 +1650,8 @@ class ListField(Field):
|
|||
'not_a_list': _('Expected a list of items but got type "{input_type}".'),
|
||||
'empty': _('This list may not be empty.'),
|
||||
'min_length': _('Ensure this field has at least {min_length} elements.'),
|
||||
'max_length': _('Ensure this field has no more than {max_length} elements.')
|
||||
'max_length': _('Ensure this field has no more than {max_length} elements.'),
|
||||
'max_depth': _('List nesting depth exceeds maximum allowed depth of {max_depth}.')
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
|
|
@ -1658,6 +1659,7 @@ class ListField(Field):
|
|||
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||
self.max_length = kwargs.pop('max_length', None)
|
||||
self.min_length = kwargs.pop('min_length', None)
|
||||
self.max_depth = kwargs.pop('max_depth', None)
|
||||
|
||||
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
|
||||
assert self.child.source is None, (
|
||||
|
|
@ -1666,6 +1668,8 @@ class ListField(Field):
|
|||
)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self._current_depth = 0
|
||||
self._root_max_depth = self.max_depth
|
||||
self.child.bind(field_name='', parent=self)
|
||||
if self.max_length is not None:
|
||||
message = lazy_format(self.error_messages['max_length'], max_length=self.max_length)
|
||||
|
|
@ -1674,6 +1678,29 @@ class ListField(Field):
|
|||
message = lazy_format(self.error_messages['min_length'], min_length=self.min_length)
|
||||
self.validators.append(MinLengthValidator(self.min_length, message=message))
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
super().bind(field_name, parent)
|
||||
if self.max_depth is None and hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
|
||||
self._root_max_depth = parent._root_max_depth
|
||||
self._current_depth = parent._current_depth + 1
|
||||
self._propagate_depth_to_child()
|
||||
|
||||
def _propagate_depth_to_child(self):
|
||||
if self._root_max_depth is not None and hasattr(self.child, '_root_max_depth'):
|
||||
self.child._root_max_depth = self._root_max_depth
|
||||
self.child._current_depth = self._current_depth + 1
|
||||
if hasattr(self.child, '_propagate_depth_to_child'):
|
||||
self.child._propagate_depth_to_child()
|
||||
|
||||
def _check_data_depth(self, data, current_level):
|
||||
items = data.values() if isinstance(data, dict) else data
|
||||
for item in items:
|
||||
if isinstance(item, (list, tuple, dict)):
|
||||
next_level = current_level + 1
|
||||
if next_level > self._root_max_depth:
|
||||
self.fail('max_depth', max_depth=self._root_max_depth)
|
||||
self._check_data_depth(item, next_level)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
if self.field_name not in dictionary:
|
||||
if getattr(self.root, 'partial', False):
|
||||
|
|
@ -1699,6 +1726,12 @@ class ListField(Field):
|
|||
self.fail('not_a_list', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
if self._root_max_depth is not None:
|
||||
start_level = self._current_depth if self._current_depth > 0 else 1
|
||||
if start_level > self._root_max_depth:
|
||||
self.fail('max_depth', max_depth=self._root_max_depth)
|
||||
if self.max_depth is not None:
|
||||
self._check_data_depth(data, start_level)
|
||||
return self.run_child_validation(data)
|
||||
|
||||
def to_representation(self, data):
|
||||
|
|
@ -1730,11 +1763,13 @@ class DictField(Field):
|
|||
default_error_messages = {
|
||||
'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".'),
|
||||
'empty': _('This dictionary may not be empty.'),
|
||||
'max_depth': _('Dictionary nesting depth exceeds maximum allowed depth of {max_depth}.')
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.child = kwargs.pop('child', copy.deepcopy(self.child))
|
||||
self.allow_empty = kwargs.pop('allow_empty', True)
|
||||
self.max_depth = kwargs.pop('max_depth', None)
|
||||
|
||||
assert not inspect.isclass(self.child), '`child` has not been instantiated.'
|
||||
assert self.child.source is None, (
|
||||
|
|
@ -1743,8 +1778,33 @@ class DictField(Field):
|
|||
)
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self._current_depth = 0
|
||||
self._root_max_depth = self.max_depth
|
||||
self.child.bind(field_name='', parent=self)
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
super().bind(field_name, parent)
|
||||
if self.max_depth is None and hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
|
||||
self._root_max_depth = parent._root_max_depth
|
||||
self._current_depth = parent._current_depth + 1
|
||||
self._propagate_depth_to_child()
|
||||
|
||||
def _propagate_depth_to_child(self):
|
||||
if self._root_max_depth is not None and hasattr(self.child, '_root_max_depth'):
|
||||
self.child._root_max_depth = self._root_max_depth
|
||||
self.child._current_depth = self._current_depth + 1
|
||||
if hasattr(self.child, '_propagate_depth_to_child'):
|
||||
self.child._propagate_depth_to_child()
|
||||
|
||||
def _check_data_depth(self, data, current_level):
|
||||
items = data.values() if isinstance(data, dict) else data
|
||||
for item in items:
|
||||
if isinstance(item, (list, tuple, dict)):
|
||||
next_level = current_level + 1
|
||||
if next_level > self._root_max_depth:
|
||||
self.fail('max_depth', max_depth=self._root_max_depth)
|
||||
self._check_data_depth(item, next_level)
|
||||
|
||||
def get_value(self, dictionary):
|
||||
# We override the default field access in order to support
|
||||
# dictionaries in HTML forms.
|
||||
|
|
@ -1762,7 +1822,12 @@ class DictField(Field):
|
|||
self.fail('not_a_dict', input_type=type(data).__name__)
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
self.fail('empty')
|
||||
|
||||
if self._root_max_depth is not None:
|
||||
start_level = self._current_depth if self._current_depth > 0 else 1
|
||||
if start_level > self._root_max_depth:
|
||||
self.fail('max_depth', max_depth=self._root_max_depth)
|
||||
if self.max_depth is not None:
|
||||
self._check_data_depth(data, start_level)
|
||||
return self.run_child_validation(data)
|
||||
|
||||
def to_representation(self, value):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
#
|
||||
# Translators:
|
||||
msgid ""
|
||||
msgstr ""
|
||||
|
|
@ -357,6 +357,11 @@ msgstr "Ensure this field has at least {min_length} elements."
|
|||
msgid "Ensure this field has no more than {max_length} elements."
|
||||
msgstr "Ensure this field has no more than {max_length} elements."
|
||||
|
||||
#: fields.py:1654
|
||||
#, python-brace-format
|
||||
msgid "List nesting depth exceeds maximum allowed depth of {max_depth}."
|
||||
msgstr "List nesting depth exceeds maximum allowed depth of {max_depth}."
|
||||
|
||||
#: fields.py:1682
|
||||
#, python-brace-format
|
||||
msgid "Expected a dictionary of items but got type \"{input_type}\"."
|
||||
|
|
@ -366,6 +371,11 @@ msgstr "Expected a dictionary of items but got type \"{input_type}\"."
|
|||
msgid "This dictionary may not be empty."
|
||||
msgstr "This dictionary may not be empty."
|
||||
|
||||
#: fields.py:1733
|
||||
#, python-brace-format
|
||||
msgid "Dictionary nesting depth exceeds maximum allowed depth of {max_depth}."
|
||||
msgstr "Dictionary nesting depth exceeds maximum allowed depth of {max_depth}."
|
||||
|
||||
#: fields.py:1755
|
||||
msgid "Value must be valid JSON."
|
||||
msgstr "Value must be valid JSON."
|
||||
|
|
|
|||
|
|
@ -76,9 +76,9 @@ LIST_SERIALIZER_KWARGS = (
|
|||
'read_only', 'write_only', 'required', 'default', 'initial', 'source',
|
||||
'label', 'help_text', 'style', 'error_messages', 'allow_empty',
|
||||
'instance', 'data', 'partial', 'context', 'allow_null',
|
||||
'max_length', 'min_length'
|
||||
'max_length', 'min_length', 'max_depth'
|
||||
)
|
||||
LIST_SERIALIZER_KWARGS_REMOVE = ('allow_empty', 'min_length', 'max_length')
|
||||
LIST_SERIALIZER_KWARGS_REMOVE = ('allow_empty', 'min_length', 'max_length', 'max_depth')
|
||||
|
||||
ALL_FIELDS = '__all__'
|
||||
|
||||
|
|
@ -111,6 +111,10 @@ class BaseSerializer(Field):
|
|||
.data - Available.
|
||||
"""
|
||||
|
||||
default_error_messages = {
|
||||
'max_depth': _('Nesting depth exceeds maximum allowed depth of {max_depth}.')
|
||||
}
|
||||
|
||||
def __init__(self, instance=None, data=empty, **kwargs):
|
||||
self.instance = instance
|
||||
if data is not empty:
|
||||
|
|
@ -118,7 +122,51 @@ class BaseSerializer(Field):
|
|||
self.partial = kwargs.pop('partial', False)
|
||||
self._context = kwargs.pop('context', {})
|
||||
kwargs.pop('many', None)
|
||||
self.max_depth = kwargs.pop('max_depth', None)
|
||||
super().__init__(**kwargs)
|
||||
self._current_depth = 0
|
||||
self._root_max_depth = self.max_depth
|
||||
|
||||
def bind(self, field_name, parent):
|
||||
super().bind(field_name, parent)
|
||||
if self.max_depth is None and hasattr(parent, '_root_max_depth') and parent._root_max_depth is not None:
|
||||
self._root_max_depth = parent._root_max_depth
|
||||
self._current_depth = parent._current_depth + 1
|
||||
|
||||
def _propagate_depth_to_child(self):
|
||||
if self._root_max_depth is not None and 'fields' in self.__dict__:
|
||||
for field in self.__dict__['fields'].values():
|
||||
if hasattr(field, '_root_max_depth'):
|
||||
field._root_max_depth = self._root_max_depth
|
||||
field._current_depth = self._current_depth + 1
|
||||
if hasattr(field, '_propagate_depth_to_child'):
|
||||
field._propagate_depth_to_child()
|
||||
|
||||
def _check_data_depth(self, data, current_level):
|
||||
if isinstance(data, dict):
|
||||
for value in data.values():
|
||||
if isinstance(value, (list, tuple, dict)):
|
||||
next_level = current_level + 1
|
||||
if next_level > self._root_max_depth:
|
||||
message = self.error_messages['max_depth'].format(
|
||||
max_depth=self._root_max_depth
|
||||
)
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
}, code='max_depth')
|
||||
self._check_data_depth(value, next_level)
|
||||
elif isinstance(data, (list, tuple)):
|
||||
for item in data:
|
||||
if isinstance(item, (list, tuple, dict)):
|
||||
next_level = current_level + 1
|
||||
if next_level > self._root_max_depth:
|
||||
message = self.error_messages['max_depth'].format(
|
||||
max_depth=self._root_max_depth
|
||||
)
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
}, code='max_depth')
|
||||
self._check_data_depth(item, next_level)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
# We override this method in order to automatically create
|
||||
|
|
@ -373,6 +421,8 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
|
|||
fields = BindingDict(self)
|
||||
for key, value in self.get_fields().items():
|
||||
fields[key] = value
|
||||
if self._root_max_depth is not None:
|
||||
self._propagate_depth_to_child()
|
||||
return fields
|
||||
|
||||
@property
|
||||
|
|
@ -489,6 +539,9 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
|
|||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
}, code='invalid')
|
||||
if self._root_max_depth is not None and self.max_depth is not None:
|
||||
start_level = self._current_depth
|
||||
self._check_data_depth(data, start_level)
|
||||
|
||||
ret = {}
|
||||
errors = {}
|
||||
|
|
@ -654,6 +707,32 @@ class ListSerializer(BaseSerializer):
|
|||
"""
|
||||
return self.child.run_validation(data)
|
||||
|
||||
def _check_data_depth(self, data, current_level):
|
||||
if isinstance(data, (list, tuple)):
|
||||
for item in data:
|
||||
if isinstance(item, (list, tuple, dict)):
|
||||
next_level = current_level + 1
|
||||
if next_level > self._root_max_depth:
|
||||
message = self.error_messages['max_depth'].format(
|
||||
max_depth=self._root_max_depth
|
||||
)
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
}, code='max_depth')
|
||||
self._check_data_depth(item, next_level)
|
||||
elif isinstance(data, dict):
|
||||
for value in data.values():
|
||||
if isinstance(value, (list, tuple, dict)):
|
||||
next_level = current_level + 1
|
||||
if next_level > self._root_max_depth:
|
||||
message = self.error_messages['max_depth'].format(
|
||||
max_depth=self._root_max_depth
|
||||
)
|
||||
raise ValidationError({
|
||||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
}, code='max_depth')
|
||||
self._check_data_depth(value, next_level)
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""
|
||||
List of dicts of native values <- List of dicts of primitive datatypes.
|
||||
|
|
@ -669,6 +748,10 @@ class ListSerializer(BaseSerializer):
|
|||
api_settings.NON_FIELD_ERRORS_KEY: [message]
|
||||
}, code='not_a_list')
|
||||
|
||||
if self._root_max_depth is not None and self.max_depth is not None:
|
||||
start_level = self._current_depth
|
||||
self._check_data_depth(data, start_level)
|
||||
|
||||
if not self.allow_empty and len(data) == 0:
|
||||
message = self.error_messages['empty']
|
||||
raise ValidationError({
|
||||
|
|
|
|||
|
|
@ -2545,6 +2545,474 @@ class TestUnvalidatedDictField(FieldValues):
|
|||
field = serializers.DictField()
|
||||
|
||||
|
||||
class TestListFieldMaxDepth:
|
||||
def test_flat_list_with_max_depth_none(self):
|
||||
field = serializers.ListField(child=serializers.IntegerField(), max_depth=None)
|
||||
output = field.run_validation([1, 2, 3])
|
||||
assert output == [1, 2, 3]
|
||||
|
||||
def test_nested_list_with_max_depth_none(self):
|
||||
field = serializers.ListField(
|
||||
child=serializers.ListField(child=serializers.ListField(child=serializers.IntegerField())),
|
||||
max_depth=None
|
||||
)
|
||||
output = field.run_validation([[[1, 2]], [[3]]])
|
||||
assert output == [[[1, 2]], [[3]]]
|
||||
|
||||
def test_max_depth_zero_rejects_everything(self):
|
||||
field = serializers.ListField(child=serializers.IntegerField(), max_depth=0)
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation([1, 2, 3])
|
||||
assert 'max_depth' in str(exc_info.value.detail)
|
||||
|
||||
def test_max_depth_one_allows_flat_list(self):
|
||||
field = serializers.ListField(child=serializers.IntegerField(), max_depth=1)
|
||||
output = field.run_validation([1, 2, 3])
|
||||
assert output == [1, 2, 3]
|
||||
|
||||
def test_max_depth_one_rejects_nested_list(self):
|
||||
field = serializers.ListField(child=serializers.IntegerField(), max_depth=1)
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation([[1, 2]])
|
||||
assert 'max_depth' in str(exc_info.value.detail)
|
||||
|
||||
def test_max_depth_two_allows_one_level_nesting(self):
|
||||
field = serializers.ListField(
|
||||
child=serializers.ListField(child=serializers.IntegerField()),
|
||||
max_depth=2
|
||||
)
|
||||
output = field.run_validation([[1, 2], [3, 4]])
|
||||
assert output == [[1, 2], [3, 4]]
|
||||
|
||||
def test_max_depth_two_rejects_two_levels_nesting(self):
|
||||
field = serializers.ListField(
|
||||
child=serializers.ListField(child=serializers.IntegerField()),
|
||||
max_depth=2
|
||||
)
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation([[[1, 2]], [[3]]])
|
||||
assert 'max_depth' in str(exc_info.value.detail)
|
||||
|
||||
def test_deeply_nested_exceeds_max_depth(self):
|
||||
field = serializers.ListField(
|
||||
child=serializers.ListField(
|
||||
child=serializers.ListField(
|
||||
child=serializers.ListField(
|
||||
child=serializers.ListField(child=serializers.IntegerField())
|
||||
)
|
||||
)
|
||||
),
|
||||
max_depth=3
|
||||
)
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation([[[[[1]]]]])
|
||||
|
||||
def test_max_depth_with_mixed_nesting_list_and_dict(self):
|
||||
field = serializers.ListField(
|
||||
child=serializers.DictField(child=serializers.ListField(child=serializers.IntegerField())),
|
||||
max_depth=3
|
||||
)
|
||||
output = field.run_validation([{'a': [1, 2], 'b': [3]}])
|
||||
assert output == [{'a': [1, 2], 'b': [3]}]
|
||||
|
||||
def test_max_depth_with_mixed_nesting_exceeds_limit(self):
|
||||
field = serializers.ListField(
|
||||
child=serializers.DictField(
|
||||
child=serializers.ListField(child=serializers.ListField(child=serializers.IntegerField()))
|
||||
),
|
||||
max_depth=2
|
||||
)
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation([{'a': [[1, 2]]}])
|
||||
|
||||
def test_error_message_contains_max_depth_value(self):
|
||||
field = serializers.ListField(
|
||||
child=serializers.ListField(child=serializers.IntegerField()),
|
||||
max_depth=0
|
||||
)
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation([[1, 2]])
|
||||
error_msg = str(exc_info.value.detail[0])
|
||||
assert '0' in error_msg
|
||||
|
||||
|
||||
class TestDictFieldMaxDepth:
|
||||
def test_flat_dict_with_max_depth_none(self):
|
||||
field = serializers.DictField(child=serializers.IntegerField(), max_depth=None)
|
||||
output = field.run_validation({'a': 1, 'b': 2})
|
||||
assert output == {'a': 1, 'b': 2}
|
||||
|
||||
def test_nested_dict_with_max_depth_none(self):
|
||||
field = serializers.DictField(
|
||||
child=serializers.DictField(child=serializers.DictField(child=serializers.IntegerField())),
|
||||
max_depth=None
|
||||
)
|
||||
output = field.run_validation({'a': {'b': {'c': 1}}})
|
||||
assert output == {'a': {'b': {'c': 1}}}
|
||||
|
||||
def test_max_depth_zero_rejects_everything(self):
|
||||
field = serializers.DictField(child=serializers.IntegerField(), max_depth=0)
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation({'a': 1, 'b': 2})
|
||||
assert 'max_depth' in str(exc_info.value.detail)
|
||||
|
||||
def test_max_depth_one_allows_flat_dict(self):
|
||||
field = serializers.DictField(child=serializers.IntegerField(), max_depth=1)
|
||||
output = field.run_validation({'a': 1, 'b': 2})
|
||||
assert output == {'a': 1, 'b': 2}
|
||||
|
||||
def test_max_depth_one_rejects_two_levels_nesting(self):
|
||||
field = serializers.DictField(
|
||||
child=serializers.DictField(child=serializers.DictField(child=serializers.IntegerField())),
|
||||
max_depth=1
|
||||
)
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation({'a': {'b': {'c': 1}}})
|
||||
assert 'max_depth' in str(exc_info.value.detail)
|
||||
|
||||
def test_max_depth_with_mixed_nesting_dict_and_list(self):
|
||||
field = serializers.DictField(
|
||||
child=serializers.ListField(child=serializers.DictField(child=serializers.IntegerField())),
|
||||
max_depth=3
|
||||
)
|
||||
output = field.run_validation({'a': [{'b': 1, 'c': 2}]})
|
||||
assert output == {'a': [{'b': 1, 'c': 2}]}
|
||||
|
||||
def test_max_depth_with_mixed_nesting_exceeds_limit(self):
|
||||
field = serializers.DictField(
|
||||
child=serializers.ListField(
|
||||
child=serializers.DictField(child=serializers.DictField(child=serializers.IntegerField()))
|
||||
),
|
||||
max_depth=2
|
||||
)
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation({'a': [{'b': {'c': 1}}]})
|
||||
|
||||
def test_error_message_contains_max_depth_value(self):
|
||||
field = serializers.DictField(
|
||||
child=serializers.DictField(child=serializers.IntegerField()),
|
||||
max_depth=0
|
||||
)
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation({'a': {'b': 1}})
|
||||
error_msg = str(exc_info.value.detail)
|
||||
assert '0' in error_msg
|
||||
|
||||
|
||||
class TestMaxDepthEdgeCases:
|
||||
def test_field_reuse_does_not_leak_depth_state(self):
|
||||
child_field = serializers.ListField(child=serializers.IntegerField())
|
||||
field = serializers.ListField(child=child_field, max_depth=2)
|
||||
output1 = field.run_validation([[1, 2], [3, 4]])
|
||||
assert output1 == [[1, 2], [3, 4]]
|
||||
output2 = field.run_validation([[5, 6], [7, 8]])
|
||||
assert output2 == [[5, 6], [7, 8]]
|
||||
|
||||
def test_max_depth_with_empty_nested_structures(self):
|
||||
field = serializers.ListField(
|
||||
child=serializers.ListField(child=serializers.IntegerField()),
|
||||
max_depth=2
|
||||
)
|
||||
output = field.run_validation([[], []])
|
||||
assert output == [[], []]
|
||||
|
||||
def test_very_deep_nesting_rejected_immediately(self):
|
||||
child = serializers.IntegerField()
|
||||
for _ in range(10):
|
||||
child = serializers.ListField(child=child)
|
||||
field = serializers.ListField(child=child, max_depth=5)
|
||||
data = [1]
|
||||
for _ in range(10):
|
||||
data = [data]
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation(data)
|
||||
|
||||
|
||||
class TestMaxDepthDataInspection:
|
||||
def test_flat_field_rejects_deeply_nested_data(self):
|
||||
field = serializers.ListField(max_depth=1)
|
||||
field.run_validation([1, 2, 3])
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation([[1, 2]])
|
||||
assert 'max_depth' in str(exc_info.value.detail)
|
||||
|
||||
def test_flat_dict_field_rejects_deeply_nested_data(self):
|
||||
field = serializers.DictField(max_depth=1)
|
||||
field.run_validation({'a': 1, 'b': 2})
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation({'a': {'b': 1}})
|
||||
assert 'max_depth' in str(exc_info.value.detail)
|
||||
|
||||
def test_max_depth_zero_rejects_any_nesting_in_data(self):
|
||||
field = serializers.ListField(max_depth=0)
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation([1, 2, 3])
|
||||
|
||||
def test_data_depth_check_with_mixed_structures(self):
|
||||
field = serializers.ListField(max_depth=1)
|
||||
field.run_validation([1, 2, 3])
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation([{'a': 1}, [2], 3])
|
||||
|
||||
def test_dict_field_data_depth_with_nested_lists(self):
|
||||
field = serializers.DictField(max_depth=1)
|
||||
field.run_validation({'a': 1, 'b': 2})
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation({'a': [1, 2]})
|
||||
|
||||
def test_data_depth_respects_current_depth(self):
|
||||
inner = serializers.ListField(child=serializers.IntegerField())
|
||||
outer = serializers.ListField(child=inner, max_depth=2)
|
||||
outer.run_validation([[1, 2], [3, 4]])
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
outer.run_validation([[[1]]])
|
||||
|
||||
def test_max_depth_one_means_flat_only(self):
|
||||
field = serializers.ListField(child=serializers.IntegerField(), max_depth=1)
|
||||
field.run_validation([1, 2, 3])
|
||||
with pytest.raises(serializers.ValidationError) as exc_info:
|
||||
field.run_validation([[1, 2]])
|
||||
assert 'max_depth' in str(exc_info.value.detail)
|
||||
|
||||
def test_serializer_with_list_field_respects_depth(self):
|
||||
class TestSerializer(serializers.Serializer):
|
||||
data = serializers.ListField(child=serializers.IntegerField(), max_depth=1)
|
||||
|
||||
serializer = TestSerializer(data={'data': [1, 2, 3]})
|
||||
assert serializer.is_valid()
|
||||
|
||||
serializer = TestSerializer(data={'data': [[1, 2]]})
|
||||
assert not serializer.is_valid()
|
||||
assert 'max_depth' in str(serializer.errors)
|
||||
|
||||
def test_serializer_with_max_depth_inspects_raw_data(self):
|
||||
class TestSerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
value = serializers.IntegerField()
|
||||
|
||||
serializer = TestSerializer(data={'name': 'test', 'value': 1}, max_depth=1)
|
||||
assert serializer.is_valid()
|
||||
|
||||
serializer = TestSerializer(data={'name': 'test', 'value': {'nested': 1}}, max_depth=1)
|
||||
assert not serializer.is_valid()
|
||||
assert 'max_depth' in str(serializer.errors) or 'invalid' in str(serializer.errors)
|
||||
|
||||
def test_standalone_serializer_protects_against_deep_json(self):
|
||||
class SimpleSerializer(serializers.Serializer):
|
||||
data = serializers.CharField()
|
||||
|
||||
serializer = SimpleSerializer(data={'data': 'value'}, max_depth=1)
|
||||
assert serializer.is_valid()
|
||||
|
||||
deep_data = {'data': {'nested': {'deep': 'value'}}}
|
||||
serializer = SimpleSerializer(data=deep_data, max_depth=1)
|
||||
assert not serializer.is_valid()
|
||||
assert 'max_depth' in str(serializer.errors)
|
||||
|
||||
def test_list_serializer_many_true_respects_max_depth(self):
|
||||
class MySerializer(serializers.Serializer):
|
||||
name = serializers.CharField()
|
||||
|
||||
serializer = MySerializer(data=[{'name': 'test'}], many=True, max_depth=1)
|
||||
assert serializer.is_valid()
|
||||
|
||||
deep_list_data = [{'name': 'test'}, [1, 2, 3]]
|
||||
serializer = MySerializer(data=deep_list_data, many=True, max_depth=1)
|
||||
assert not serializer.is_valid()
|
||||
|
||||
def test_list_serializer_protects_against_deeply_nested_lists(self):
|
||||
class ItemSerializer(serializers.Serializer):
|
||||
value = serializers.IntegerField()
|
||||
|
||||
serializer = ItemSerializer(data=[{'value': 1}, {'value': 2}], many=True, max_depth=1)
|
||||
assert serializer.is_valid()
|
||||
|
||||
deep_data = [{'value': {'nested': 1}}]
|
||||
serializer = ItemSerializer(data=deep_data, many=True, max_depth=1)
|
||||
assert not serializer.is_valid()
|
||||
|
||||
|
||||
class TestMaxDepthWithSerializers:
|
||||
def test_list_field_containing_serializer_with_nested_list(self):
|
||||
class InnerSerializer(serializers.Serializer):
|
||||
numbers = serializers.ListField(child=serializers.IntegerField())
|
||||
|
||||
field = serializers.ListField(child=InnerSerializer(), max_depth=3)
|
||||
valid_data = [{'numbers': [1, 2]}, {'numbers': [3, 4]}]
|
||||
output = field.run_validation(valid_data)
|
||||
assert output == [{'numbers': [1, 2]}, {'numbers': [3, 4]}]
|
||||
|
||||
def test_list_field_containing_serializer_exceeds_max_depth(self):
|
||||
class InnerSerializer(serializers.Serializer):
|
||||
nested_list = serializers.ListField(
|
||||
child=serializers.ListField(child=serializers.IntegerField())
|
||||
)
|
||||
|
||||
field = serializers.ListField(child=InnerSerializer(), max_depth=3)
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation([{'nested_list': [[1, 2]]}])
|
||||
|
||||
def test_serializer_within_dict_field_respects_depth(self):
|
||||
class ValueSerializer(serializers.Serializer):
|
||||
data = serializers.ListField(child=serializers.IntegerField())
|
||||
|
||||
field = serializers.DictField(child=ValueSerializer(), max_depth=3)
|
||||
valid_data = {'key1': {'data': [1, 2]}, 'key2': {'data': [3, 4]}}
|
||||
output = field.run_validation(valid_data)
|
||||
assert output == {'key1': {'data': [1, 2]}, 'key2': {'data': [3, 4]}}
|
||||
|
||||
def test_deeply_nested_serializer_structure_rejected(self):
|
||||
class Level3Serializer(serializers.Serializer):
|
||||
values = serializers.ListField(child=serializers.IntegerField())
|
||||
|
||||
class Level2Serializer(serializers.Serializer):
|
||||
level3 = Level3Serializer()
|
||||
|
||||
class Level1Serializer(serializers.Serializer):
|
||||
level2 = Level2Serializer()
|
||||
|
||||
field = serializers.ListField(child=Level1Serializer(), max_depth=4)
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
field.run_validation([{'level2': {'level3': {'values': [1, 2]}}}])
|
||||
|
||||
|
||||
class TestListFieldBindWithExplicitMaxDepth:
|
||||
def test_child_listfield_keeps_explicit_max_depth_zero(self):
|
||||
inner_list = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
max_depth=0
|
||||
)
|
||||
outer_list = serializers.ListField(
|
||||
child=inner_list,
|
||||
max_depth=10
|
||||
)
|
||||
assert inner_list._root_max_depth == 0
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
outer_list.run_validation([[1, 2]])
|
||||
|
||||
|
||||
class TestListFieldBindWithoutExplicitMaxDepth:
|
||||
def test_child_without_max_depth_inherits_from_parent(self):
|
||||
inner_list = serializers.ListField(
|
||||
child=serializers.IntegerField()
|
||||
)
|
||||
outer_list = serializers.ListField(
|
||||
child=inner_list,
|
||||
max_depth=3
|
||||
)
|
||||
assert inner_list._root_max_depth == 3
|
||||
assert inner_list._current_depth == 1
|
||||
output = outer_list.run_validation([[1, 2]])
|
||||
assert output == [[1, 2]]
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
outer_list.run_validation([[[[1]]]])
|
||||
|
||||
def test_multiple_children_some_inherit_some_explicit(self):
|
||||
child_a = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
max_depth=2
|
||||
)
|
||||
child_b = serializers.ListField(
|
||||
child=serializers.IntegerField()
|
||||
)
|
||||
child_c = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
max_depth=4
|
||||
)
|
||||
|
||||
class MySerializerA(serializers.Serializer):
|
||||
field = child_a
|
||||
|
||||
class MySerializerB(serializers.Serializer):
|
||||
field = child_b
|
||||
|
||||
class MySerializerC(serializers.Serializer):
|
||||
field = child_c
|
||||
|
||||
s_a = MySerializerA(max_depth=5)
|
||||
s_b = MySerializerB(max_depth=5)
|
||||
s_c = MySerializerC(max_depth=5)
|
||||
|
||||
assert s_a.fields['field']._root_max_depth == 2
|
||||
assert s_b.fields['field']._root_max_depth == 5
|
||||
assert s_c.fields['field']._root_max_depth == 4
|
||||
|
||||
|
||||
class TestListFieldBindDepthPropagation:
|
||||
def test_depth_propagation_through_multiple_levels(self):
|
||||
level3 = serializers.ListField(child=serializers.IntegerField())
|
||||
level2 = serializers.ListField(child=level3)
|
||||
level1 = serializers.ListField(child=level2, max_depth=10)
|
||||
|
||||
assert level1._current_depth == 0
|
||||
assert level2._current_depth == 1
|
||||
assert level3._current_depth == 2
|
||||
assert level1._root_max_depth == 10
|
||||
assert level2._root_max_depth == 10
|
||||
assert level3._root_max_depth == 10
|
||||
|
||||
|
||||
class TestListFieldBindWithDictField:
|
||||
def test_listfield_child_of_dictfield_keeps_explicit_max_depth(self):
|
||||
list_field = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
max_depth=2
|
||||
)
|
||||
dict_field = serializers.DictField(
|
||||
child=list_field,
|
||||
max_depth=10
|
||||
)
|
||||
assert dict_field.child._root_max_depth == 2
|
||||
output = dict_field.run_validation({'key': [1, 2]})
|
||||
assert output == {'key': [1, 2]}
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
dict_field.run_validation({'key': [[1, 2]]})
|
||||
|
||||
|
||||
class TestListFieldBindWithSerializers:
|
||||
def test_listfield_in_serializer_with_explicit_max_depth(self):
|
||||
class MySerializer(serializers.Serializer):
|
||||
explicit_field = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
max_depth=2
|
||||
)
|
||||
inherit_field = serializers.ListField(
|
||||
child=serializers.IntegerField()
|
||||
)
|
||||
|
||||
serializer = MySerializer(max_depth=5)
|
||||
assert serializer.fields['explicit_field']._root_max_depth == 2
|
||||
assert serializer.fields['inherit_field']._root_max_depth == 5
|
||||
|
||||
def test_nested_serializers_with_listfield(self):
|
||||
class InnerSerializer(serializers.Serializer):
|
||||
values = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
max_depth=3
|
||||
)
|
||||
|
||||
class OuterSerializer(serializers.Serializer):
|
||||
inner = InnerSerializer(max_depth=10)
|
||||
|
||||
serializer = OuterSerializer(max_depth=20)
|
||||
values_field = serializer.fields['inner'].fields['values']
|
||||
assert values_field._root_max_depth == 3
|
||||
|
||||
|
||||
class TestListFieldBindEdgeCases:
|
||||
def test_field_max_depth_one(self):
|
||||
inner = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
max_depth=1
|
||||
)
|
||||
outer = serializers.ListField(child=inner, max_depth=10)
|
||||
assert inner._root_max_depth == 1
|
||||
output = outer.run_validation([[1, 2, 3]])
|
||||
assert output == [[1, 2, 3]]
|
||||
with pytest.raises(serializers.ValidationError):
|
||||
outer.run_validation([[[1]]])
|
||||
|
||||
|
||||
class TestHStoreField(FieldValues):
|
||||
"""
|
||||
Values for `ListField` with CharField as child.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user