diff --git a/marshmallow/decorators.py b/marshmallow/decorators.py index 8b6df0df2..bcb043823 100644 --- a/marshmallow/decorators.py +++ b/marshmallow/decorators.py @@ -61,15 +61,16 @@ def validate_age(self, data): VALIDATES_SCHEMA = 'validates_schema' -def validates(field_name): +def validates(field_name, priority=0): """Register a field validator. :param str field_name: Name of the field that the method validates. """ - return tag_processor(VALIDATES, None, False, field_name=field_name) + return tag_processor(VALIDATES, None, False, priority=priority, field_name=field_name) -def validates_schema(fn=None, pass_many=False, pass_original=False, skip_on_field_errors=True): +def validates_schema(fn=None, pass_many=False, pass_original=False, skip_on_field_errors=True, + priority=0): """Register a schema-level validator. By default, receives a single object at a time, regardless of whether ``many=True`` @@ -85,11 +86,11 @@ def validates_schema(fn=None, pass_many=False, pass_original=False, skip_on_fiel .. versionchanged:: 3.0.0b1 ``skip_on_field_errors`` defaults to `True`. """ - return tag_processor(VALIDATES_SCHEMA, fn, pass_many, pass_original=pass_original, - skip_on_field_errors=skip_on_field_errors) + return tag_processor(VALIDATES_SCHEMA, fn, pass_many, priority=priority, + pass_original=pass_original, skip_on_field_errors=skip_on_field_errors) -def pre_dump(fn=None, pass_many=False): +def pre_dump(fn=None, pass_many=False, priority=0): """Register a method to invoke before serializing an object. The method receives the object to be serialized and returns the processed object. @@ -97,10 +98,10 @@ def pre_dump(fn=None, pass_many=False): is passed to the `Schema`. If ``pass_many=True``, the raw data (which may be a collection) and the value for ``many`` is passed. """ - return tag_processor(PRE_DUMP, fn, pass_many) + return tag_processor(PRE_DUMP, fn, pass_many, priority=priority) -def post_dump(fn=None, pass_many=False, pass_original=False): +def post_dump(fn=None, pass_many=False, pass_original=False, priority=0): """Register a method to invoke after serializing an object. The method receives the serialized object and returns the processed object. @@ -108,10 +109,10 @@ def post_dump(fn=None, pass_many=False, pass_original=False): argument passed to the Schema. If ``pass_many=True``, the raw data (which may be a collection) and the value for ``many`` is passed. """ - return tag_processor(POST_DUMP, fn, pass_many, pass_original=pass_original) + return tag_processor(POST_DUMP, fn, pass_many, priority=priority, pass_original=pass_original) -def pre_load(fn=None, pass_many=False): +def pre_load(fn=None, pass_many=False, priority=0): """Register a method to invoke before deserializing an object. The method receives the data to be deserialized and returns the processed data. @@ -119,10 +120,10 @@ def pre_load(fn=None, pass_many=False): argument passed to the Schema. If ``pass_many=True``, the raw data (which may be a collection) and the value for ``many`` is passed. """ - return tag_processor(PRE_LOAD, fn, pass_many) + return tag_processor(PRE_LOAD, fn, pass_many, priority=priority) -def post_load(fn=None, pass_many=False, pass_original=False): +def post_load(fn=None, pass_many=False, pass_original=False, priority=0): """Register a method to invoke after deserializing an object. The method receives the deserialized data and returns the processed data. @@ -130,7 +131,7 @@ def post_load(fn=None, pass_many=False, pass_original=False): argument passed to the Schema. If ``pass_many=True``, the raw data (which may be a collection) and the value for ``many`` is passed. """ - return tag_processor(POST_LOAD, fn, pass_many, pass_original=pass_original) + return tag_processor(POST_LOAD, fn, pass_many, priority=priority, pass_original=pass_original) def tag_processor(tag_name, fn, pass_many, **kwargs): diff --git a/marshmallow/schema.py b/marshmallow/schema.py index 0b5f04477..e74656cab 100644 --- a/marshmallow/schema.py +++ b/marshmallow/schema.py @@ -173,6 +173,16 @@ def _resolve_processors(self): # the processor was a descriptor or something. self.__processors__[tag].append(attr_name) + # Sort all processor tags by priority, descending + for tag, processors in self.__processors__.items(): + sorted_processors = sorted(processors, + key=lambda proc: self._get_priority(proc, tag)) + self.__processors__[tag] = sorted_processors + + def _get_priority(self, attr_name, tag): + processor = getattr(self, attr_name) + return processor.__marshmallow_kwargs__[tag].get('priority', 0) + class SchemaOpts(object): """class Meta options for the :class:`Schema`. Defines defaults.""" diff --git a/tests/test_decorators.py b/tests/test_decorators.py index ec7d3e959..846df2ec1 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -706,3 +706,29 @@ def raise_value_error(self, item): schema.dump(object()) assert exc.value.messages == {'foo': 'error'} schema.load({}) + +class TestProcessorPriorities: + + def test_pass_ordered_processors(self): + class MySchema(Schema): + data = fields.Field() + + @post_load(priority=1) + def first_post_load(self, ret): + ret['data'].append(1) + return ret + + @post_load(priority=10) + def third_post_load(self, ret): + ret['data'].append(10) + return ret + + @post_load(priority=5) + def second_post_load(self, ret): + ret['data'].append(5) + return ret + + schema = MySchema() + datum = {'data': []} + item_loaded = schema.load(datum).data + assert item_loaded['data'] == [1, 5, 10]