Skip to content

Instantly share code, notes, and snippets.

@svermeulen
Created August 22, 2021 11:02
Show Gist options
  • Save svermeulen/2d5f97ffe190d6db7490e072890c4112 to your computer and use it in GitHub Desktop.
Save svermeulen/2d5f97ffe190d6db7490e072890c4112 to your computer and use it in GitHub Desktop.
Dataclass base class example
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