/
enforce_validation_serializer.py
197 lines (160 loc) · 7.17 KB
/
enforce_validation_serializer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
from __future__ import absolute_import, print_function, unicode_literals
import inspect
from rest_framework import fields, serializers
from rest_framework.fields import empty
from ..utils import add_base_class_to_instance, get_class_name_with_new_suffix
class EnforceValidationFieldMixin(object):
"""
Custom DRF field mixin which allows to ignore validation error
if the field is not mandatory.
The field is mandatory when the parent serializer includes it
in the ``must_validate_fields`` list or ``must_validate_fields``
list is completely omitted. If the list is omitted, then all
fields in that serializer are mandatory and must validate.
"""
def run_validation(self, data=empty):
try:
return super(EnforceValidationFieldMixin, self).run_validation(data)
except serializers.ValidationError:
must_validate_fields = getattr(self.parent, 'must_validate_fields', None)
field_name = getattr(self, 'field_name')
# only re-raise validation error when this field must be validated
# as defined by must_validate_fields list on the parent serializer
# or if must_validate_fields is not defined
if must_validate_fields is None or field_name in must_validate_fields:
raise
else:
raise fields.SkipField(
'This field "{}" is being skipped as per enforce validation logic.'
''.format(field_name)
)
def _create_enforce_validation_serializer(serializer, strict_mode_by_default=True):
"""
Recursively creates a copy of a given serializer which enforces ``must_validate_fields``.
This function recursively copies serializers and replaces all fields
by adding ``EnforceValidationFieldMixin`` to their mro which then enforces validation
for fields defined in ``must_validate_fields`` and drops their data for all other fields.
Args:
serializer (Serializer): Serializer class or instance to be copied
strict_mode_by_default (bool): Whether serializer should use strict mode
when ``must_validate_fields`` is not defined.
If ``True``, then all fields must be validated by default
and if ``False``, then all fields can be dropped.
Returns:
Recursive copy of the ``serializer`` which will enforce ``must_validate_fields``.
"""
if inspect.isclass(serializer):
serializer = type(
get_class_name_with_new_suffix(
serializer,
'Serializer',
'EnforceValidationSerializer'
),
(serializer,),
{}
)
fields = serializer._declared_fields
declared_fields = None
else:
# when serializer is instance, we still want to create a modified
# serializer class copy since if we only adjust the fields
# on the instance, that can complicate introspecting,
# especially when ``_create_enforce_validation_serializer``
# is used as decorator
serializer = add_base_class_to_instance(
serializer,
new_name=get_class_name_with_new_suffix(
serializer.__class__,
'Serializer',
'EnforceValidationSerializer'
)
)
if isinstance(serializer, serializers.ListSerializer):
serializer.child = _create_enforce_validation_serializer(
serializer.child,
strict_mode_by_default=strict_mode_by_default
)
# kwargs are used to take a deepcopy of the fields
# so we need to adjust the child kwargs not to loose
# reference to our custom child serializer
if 'child' in serializer._kwargs:
serializer._kwargs['child'] = serializer.child
return serializer
fields = serializer.fields
declared_fields = serializer._declared_fields
if not strict_mode_by_default and not hasattr(serializer, 'must_validate_fields'):
serializer.must_validate_fields = []
# this is necessary for deepcopy to work when
# root serializer is instantiated it does deepcopy
# on serializer._declared_fields which re-instantiates
# all child fields hence must_validate_fields will be lost
# adding it to the class makes it persistent
if not inspect.isclass(serializer):
serializer.__class__.must_validate_fields = []
# cant use .items() since we need to adjust dictionary
# within the loop so we cant be looping over the dict
# at the same time
# Python 3 even raises exception for this:
# RuntimeError: dictionary changed size during iteration
for name in list(fields.keys()):
field = fields[name]
replacement = None
if isinstance(field, serializers.BaseSerializer):
replacement = _create_enforce_validation_serializer(
field,
strict_mode_by_default=strict_mode_by_default
)
elif isinstance(field, serializers.Field):
replacement = add_base_class_to_instance(
field,
EnforceValidationFieldMixin,
new_name=get_class_name_with_new_suffix(
field.__class__,
'Field',
'EnforceValidationField'
)
)
if replacement is not None:
if declared_fields is not None:
declared_fields[name] = replacement
if replacement.source == name:
replacement.source = None
fields[name] = replacement
return serializer
def create_enforce_validation_serializer(serializer=None, **kwargs):
"""
Public function that creates a copy of a serializer which enforces ``must_validate_fields``.
The difference between this function and ``_create_enforce_validation_serializer``
is that this function can be used both as a direct decorator and decorator with
parameters.
For example::
@create_enforce_validation_serializer
class MySerializer(BaseSerializer): pass
# or
@create_enforce_validation_serializer(param=value)
class MySerializer(BaseSerializer): pass
# or
create_enforce_validation_serializer(
MySerializer,
param=value
)
"""
# used as direct decorator so then simply return new serializer
# e.g. @decorator
# class MySerializer(...)
# or used as regular function
# e.g. function(Serializer, foo=bar)
if inspect.isclass(serializer) and issubclass(serializer, serializers.Serializer):
return _create_enforce_validation_serializer(serializer, **kwargs)
# used as decorator with parameters
# e.g. @decorator(foo=bar)
# class MySerializer(...)
elif serializer is None:
def inner(serializer):
return _create_enforce_validation_serializer(serializer, **kwargs)
return inner
else:
raise TypeError(
'create_enforce_validation_serializer can only be only on serializers. '
'It was called with "{}"'.format(type(serializer))
)