Coverage for src/metador_core/schema/encoder.py: 100%
31 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-02 09:33 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-11-02 09:33 +0000
1"""Support for dynamically registered JSON encoders in pydantic.
3Preparation:
5For your top level BaseModel, set `DynEncoderModelMeta` as metaclass, e.g.
7```
8class MyBaseModel(BaseModel, metaclass=DynEncoderModelMeta):
9 ...
10```
12(If you already use a custom metaclass for your base model,
13add the `DynJsonEncoder` metaclass mixin)
15Usage:
17Decorate any class with `@json_encoder(ENCODER_FUNCTION)`.
19To add an encoder for some existing class you cannot decorate,
20use `add_json_encoder(ClassName, func)`.
22Note that `json_encoders` declared as intended by Pydantic in the `Config` section
23will always be prioritized over the dynamic encoder. This means, that the dynamic
24encoders are only triggered for classes that are not models themselves
25(because pydantic handles them already).
27Also note that to prevent bugs, you cannot override encoders for a class that already
28has a registered dynamic encoder. Use the normal pydantic mechanisms for
29cases where this is really needed.
31Ideally, design your classes in a way that there is a 1-to-1 relationship between
32class and desired JSON encoder (e.g. you can have different subclasses with
33different encoders).
34"""
36from typing import Any, Callable, Dict, Type
38from pydantic.main import ModelMetaclass
40_reg_json_encoders: Dict[Type, Callable[[Any], str]] = {}
41"""Global registry of declared JSON encoders."""
44def json_encoder(func):
45 """Decorate a class to register a new JSON encoder for it."""
47 def reg_encoder(cls):
48 if issubclass(cls.__class__, ModelMetaclass):
49 raise TypeError("This decorator does not work for pydantic models!")
50 if hasattr(cls, "__dataclass_fields__"):
51 raise TypeError("This decorator does not work for dataclasses!")
53 if cls in _reg_json_encoders:
54 raise ValueError(f"A JSON encoder function for {cls} already exists!")
56 _reg_json_encoders[cls] = func
57 return cls
59 return reg_encoder
62def add_json_encoder(cls, func):
63 """Register a JSON encoder function for a class."""
64 return json_encoder(func)(cls)
67# ----
70def _dynamize_encoder(encoder_func):
71 """Wrap the JSON encoder pydantic generates from the Config to support the dynamic registry."""
73 def wrapped_encoder(obj):
74 try:
75 # try the default lookup
76 return encoder_func(obj)
77 except TypeError as e:
78 if enc := _reg_json_encoders.get(type(obj)):
79 return enc(obj) # try dynamic lookup
80 raise e
82 return wrapped_encoder
85class DynJsonEncoderMetaMixin(type):
86 """Metaclass mixin to first look in dynamic encoder registry.
88 Combine this with (a subclass of) `ModelMetaClass` and use it for your custom base model.
89 """
91 def __init__(self, name, bases, dct):
92 super().__init__(name, bases, dct)
93 self.__json_encoder__ = staticmethod(_dynamize_encoder(self.__json_encoder__))
96class DynEncoderModelMetaclass(DynJsonEncoderMetaMixin, ModelMetaclass):
97 """Set this metaclass for your custom base model to enable dynamic encoders."""