Created
August 22, 2021 11:02
-
-
Save svermeulen/2d5f97ffe190d6db7490e072890c4112 to your computer and use it in GitHub Desktop.
Dataclass base class example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from jsonpickle.unpickler import loadclass | |
import jsonpickle | |
import typeguard # type: ignore | |
from dataclasses import is_dataclass | |
import inspect | |
# The entire purpose of this class is just to hook into the unpickle event | |
# so that we can call validate_member_types | |
# We want to be as strict as possible when deserializing | |
class PodPickleHandler(jsonpickle.handlers.BaseHandler): | |
def flatten(self, obj, data): | |
data['__dict__'] = self.context.flatten(obj.__dict__, reset=False) | |
return data | |
def restore(self, data): | |
module_and_type = data['py/object'] | |
cls = loadclass(module_and_type) | |
if hasattr(cls, '__new__'): | |
obj = cls.__new__(cls) | |
else: | |
obj = object.__new__(cls) | |
obj.__dict__ = self.context.restore(data['__dict__'], reset=False) | |
obj.validate_member_types() | |
return obj | |
class Pod: | |
""" | |
Provides the following: | |
- Makes the class immutable by default after the initial values are set | |
- Automatically does type checking of member values using typeguard library | |
- Automatically adds a clone() method which produces a new instance of the immutable type | |
""" | |
def __post_init__(self, is_immutable:bool=True): | |
# Note here that __post_init__ is not called when the object is unpickled | |
# However it still works in this case, because _is_read_only is unpickled | |
# last, since it is initialled added in __post_init__ when it was first | |
# serialized | |
# So after unpickling, it will be correctly set to true | |
self._is_read_only = is_immutable | |
assert is_dataclass(type(self)), \ | |
f"Expected class '{type(self)}' to be marked as @dataclass. This is a requirement when deriving from Pod" | |
def __setattr__(self, name, value): | |
if getattr(self, '_is_read_only', False): | |
raise AttributeError( | |
f"Attempted to change field '{name}' on immutable type '{type(self)}'") | |
# Don't type check private members | |
if not name.startswith("_"): | |
self._typecheck_member_candidate_value(name, value) | |
object.__setattr__(self, name, value) | |
def __init_subclass__(cls, **kwargs): | |
def clone(self, **overrides): | |
new_values = {k:v for k, v, in overrides.items()} | |
for k, v in self.__dict__.items(): | |
if k in overrides or k.startswith("_"): | |
continue | |
new_values[k] = v | |
return cls(**new_values) | |
cls.clone = clone | |
PodPickleHandler.handles(cls) | |
def _get_expected_member_type(self, name): | |
cls = type(self) | |
for cls in reversed(inspect.getmro(cls)): | |
member_type_map = getattr(cls, '__annotations__', {}) | |
for key, value in member_type_map.items(): | |
if key == name: | |
return value | |
return None | |
def validate_member_types(self): | |
members_map = self._get_member_type_map() | |
for member_name, member_type in members_map.items(): | |
# getattr throws an exception here if the attribute doesn't exist, which | |
# is what we want | |
member_value = getattr(self, member_name) | |
typeguard.check_type(member_name, member_value, member_type) | |
def _get_member_type_map(self): | |
cls = type(self) | |
result = {} | |
for cls in reversed(inspect.getmro(cls)): | |
member_type_map = getattr(cls, '__annotations__', {}) | |
for key, value in member_type_map.items(): | |
if key not in result: | |
result[key] = value | |
return result | |
def _typecheck_member_candidate_value(self, name, value): | |
expected_type = self._get_expected_member_type(name) | |
if expected_type is None: | |
raise AttributeError( | |
f"Type '{self.__class__}' has not declared a member with name '{name}'") | |
typeguard.check_type(name, value, expected_type) | |
@dataclass | |
class Foo(Pod): | |
bar:str | |
qux:int | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment