codekingpro/portable-devtools
114k
1"""Decorators for registering schema pre-processing and post-processing methods.2These should be imported from the top-level `marshmallow` module.3 4Methods decorated with5`pre_load <marshmallow.decorators.pre_load>`, `post_load <marshmallow.decorators.post_load>`,6`pre_dump <marshmallow.decorators.pre_dump>`, `post_dump <marshmallow.decorators.post_dump>`,7and `validates_schema <marshmallow.decorators.validates_schema>` receive8``many`` as a keyword argument. In addition, `pre_load <marshmallow.decorators.pre_load>`,9`post_load <marshmallow.decorators.post_load>`,10and `validates_schema <marshmallow.decorators.validates_schema>` receive11``partial``. If you don't need these arguments, add ``**kwargs`` to your method12signature.13 14 15Example: ::16 17 from marshmallow import (18 Schema,19 pre_load,20 pre_dump,21 post_load,22 validates_schema,23 validates,24 fields,25 ValidationError,26 )27 28 29 class UserSchema(Schema):30 email = fields.Str(required=True)31 age = fields.Integer(required=True)32 33 @post_load34 def lowerstrip_email(self, item, many, **kwargs):35 item["email"] = item["email"].lower().strip()36 return item37 38 @pre_load(pass_many=True)39 def remove_envelope(self, data, many, **kwargs):40 namespace = "results" if many else "result"41 return data[namespace]42 43 @post_dump(pass_many=True)44 def add_envelope(self, data, many, **kwargs):45 namespace = "results" if many else "result"46 return {namespace: data}47 48 @validates_schema49 def validate_email(self, data, **kwargs):50 if len(data["email"]) < 3:51 raise ValidationError("Email must be more than 3 characters", "email")52 53 @validates("age")54 def validate_age(self, data, **kwargs):55 if data < 14:56 raise ValidationError("Too young!")57 58.. note::59 These decorators only work with instance methods. Class and static60 methods are not supported.61 62.. warning::63 The invocation order of decorated methods of the same type is not guaranteed.64 If you need to guarantee order of different processing steps, you should put65 them in the same processing method.66"""67 68from __future__ import annotations69 70import functools71from collections import defaultdict72from typing import Any, Callable, cast73 74PRE_DUMP = "pre_dump"75POST_DUMP = "post_dump"76PRE_LOAD = "pre_load"77POST_LOAD = "post_load"78VALIDATES = "validates"79VALIDATES_SCHEMA = "validates_schema"80 81 82class MarshmallowHook:83 __marshmallow_hook__: dict[str, list[tuple[bool, Any]]] | None = None84 85 86def validates(field_name: str) -> Callable[..., Any]:87 """Register a field validator.88 89 :param field_name: Name of the field that the method validates.90 """91 return set_hook(None, VALIDATES, field_name=field_name)92 93 94def validates_schema(95 fn: Callable[..., Any] | None = None,96 pass_many: bool = False, # noqa: FBT001, FBT00297 pass_original: bool = False, # noqa: FBT001, FBT00298 skip_on_field_errors: bool = True, # noqa: FBT001, FBT00299) -> Callable[..., Any]:100 """Register a schema-level validator.101 102 By default it receives a single object at a time, transparently handling the ``many``103 argument passed to the `Schema <marshmallow.Schema>`'s :func:`~marshmallow.Schema.validate` call.104 If ``pass_many=True``, the raw data (which may be a collection) is passed.105 106 If ``pass_original=True``, the original data (before unmarshalling) will be passed as107 an additional argument to the method.108 109 If ``skip_on_field_errors=True``, this validation method will be skipped whenever110 validation errors have been detected when validating fields.111 112 .. versionchanged:: 3.0.0b1113 ``skip_on_field_errors`` defaults to `True`.114 115 .. versionchanged:: 3.0.0116 ``partial`` and ``many`` are always passed as keyword arguments to117 the decorated method.118 """119 return set_hook(120 fn,121 VALIDATES_SCHEMA,122 many=pass_many,123 pass_original=pass_original,124 skip_on_field_errors=skip_on_field_errors,125 )126 127 128def pre_dump(129 fn: Callable[..., Any] | None = None,130 pass_many: bool = False, # noqa: FBT001, FBT002131) -> Callable[..., Any]:132 """Register a method to invoke before serializing an object. The method133 receives the object to be serialized and returns the processed object.134 135 By default it receives a single object at a time, transparently handling the ``many``136 argument passed to the `Schema <marshmallow.Schema>`'s :func:`~marshmallow.Schema.dump` call.137 If ``pass_many=True``, the raw data (which may be a collection) is passed.138 139 .. versionchanged:: 3.0.0140 ``many`` is always passed as a keyword arguments to the decorated method.141 """142 return set_hook(fn, PRE_DUMP, many=pass_many)143 144 145def post_dump(146 fn: Callable[..., Any] | None = None,147 pass_many: bool = False, # noqa: FBT001, FBT002148 pass_original: bool = False, # noqa: FBT001, FBT002149) -> Callable[..., Any]:150 """Register a method to invoke after serializing an object. The method151 receives the serialized object and returns the processed object.152 153 By default it receives a single object at a time, transparently handling the ``many``154 argument passed to the `Schema <marshmallow.Schema>`'s :func:`~marshmallow.Schema.dump` call.155 If ``pass_many=True``, the raw data (which may be a collection) is passed.156 157 If ``pass_original=True``, the original data (before serializing) will be passed as158 an additional argument to the method.159 160 .. versionchanged:: 3.0.0161 ``many`` is always passed as a keyword arguments to the decorated method.162 """163 return set_hook(fn, POST_DUMP, many=pass_many, pass_original=pass_original)164 165 166def pre_load(167 fn: Callable[..., Any] | None = None,168 pass_many: bool = False, # noqa: FBT001, FBT002169) -> Callable[..., Any]:170 """Register a method to invoke before deserializing an object. The method171 receives the data to be deserialized and returns the processed data.172 173 By default it receives a single object at a time, transparently handling the ``many``174 argument passed to the `Schema <marshmallow.Schema>`'s :func:`~marshmallow.Schema.load` call.175 If ``pass_many=True``, the raw data (which may be a collection) is passed.176 177 .. versionchanged:: 3.0.0178 ``partial`` and ``many`` are always passed as keyword arguments to179 the decorated method.180 """181 return set_hook(fn, PRE_LOAD, many=pass_many)182 183 184def post_load(185 fn: Callable[..., Any] | None = None,186 pass_many: bool = False, # noqa: FBT001, FBT002187 pass_original: bool = False, # noqa: FBT001, FBT002188) -> Callable[..., Any]:189 """Register a method to invoke after deserializing an object. The method190 receives the deserialized data and returns the processed data.191 192 By default it receives a single object at a time, transparently handling the ``many``193 argument passed to the `Schema <marshmallow.Schema>`'s :func:`~marshmallow.Schema.load` call.194 If ``pass_many=True``, the raw data (which may be a collection) is passed.195 196 If ``pass_original=True``, the original data (before deserializing) will be passed as197 an additional argument to the method.198 199 .. versionchanged:: 3.0.0200 ``partial`` and ``many`` are always passed as keyword arguments to201 the decorated method.202 """203 return set_hook(fn, POST_LOAD, many=pass_many, pass_original=pass_original)204 205 206def set_hook(207 fn: Callable[..., Any] | None,208 tag: str,209 many: bool = False, # noqa: FBT001, FBT002210 **kwargs: Any,211) -> Callable[..., Any]:212 """Mark decorated function as a hook to be picked up later.213 You should not need to use this method directly.214 215 .. note::216 Currently only works with functions and instance methods. Class and217 static methods are not supported.218 219 :return: Decorated function if supplied, else this decorator with its args220 bound.221 """222 # Allow using this as either a decorator or a decorator factory.223 if fn is None:224 return functools.partial(set_hook, tag=tag, many=many, **kwargs)225 226 # Set a __marshmallow_hook__ attribute instead of wrapping in some class,227 # because I still want this to end up as a normal (unbound) method.228 function = cast(MarshmallowHook, fn)229 try:230 hook_config = function.__marshmallow_hook__231 except AttributeError:232 function.__marshmallow_hook__ = hook_config = defaultdict(list)233 # Also save the kwargs for the tagged function on234 # __marshmallow_hook__, keyed by <tag>235 if hook_config is not None:236 hook_config[tag].append((many, kwargs))237 238 return fn239 