Package: dataclass-as-data
Dataclass As DataThis is a simple package for configurable conversion of dataclasses to a data representation, typically a dict or a tuple.
The behaviour for how a dataclass is converted to and from data can be configured to differ from the default per dataclass if desired.This package only supports simple primitive types, other dataclasses, and the primitive genericsdict[...],list[...],tuple[...],Union[...], andOptional[...]as type annotations by default.
The conversion of other types can be configured using theconverterskeyword-only argument.InstallThis package supportsPython 3.9and above.pipinstalldataclass-as-dataQuick Startimportdataclassesfromdataclass_as_dataimportas_data,from_data# Create a dataclass@dataclasses.dataclassclassPerson:name:strage:int# Create a dataclass objectperson=Person("Simon",21)>>>personPerson(name='Simon',age=21)# Call as_data with the dataclass object to convert it to a dictionarydata=as_data(person)>>>data{'name':'Simon','age':21}# Call from_data with the dataclass and the data to get the object instance back>>>from_data(Person,data)Person(name='Simon',age=21)Dataclasses can be nested within dataclasses, which are recursively converted to their data representation.@dataclasses.dataclassclassFriends:people:list[Person]# All dataclasses are converted recursively>>>as_data(Friends([Person("Sunset",22),Person("Starlight",20)])){'people':[{'name':'Sunset','age':22},{'name':'Starlight','age':20}]}>>>from_data(Friends,{'people':[{'name':'Sunset','age':22},{'name':'Starlight','age':20}]})Friends(people=[Person(name='Sunset',age=22),Person(name='Starlight',age=20)])Configuring as_data and from_dataTo change what data is constructed when usingas_dataandfrom_data, override theas_datamethod andfrom_dataclass methods in your dataclass.
It is good practice to forward on theconverterskeyword argument on when doing this if you would liketype convertersto still apply,
but this is optional.Note:you must use one ofas_dict,as_tuple,from_dict, orfrom_tuple(notas_dataorfrom_data)
if you wish to use the default behaviour and modify it.fromdataclass_as_dataimportas_data,as_dict,from_data,from_dict@dataclasses.dataclassclassConfig:VERSION=(1,0)version:tuple[int,int]=VERSIONdefas_data(self,*,converters)->dict:# Ensure correct version when converting to dataassertself.version==self.VERSION,"Incorrect version!"returnas_dict(self,converters=converters)# use as_dict to otherwise use default behaviour@classmethoddeffrom_data(cls,data:dict,*,converters):# Update version on data loadifdata['version']<cls.VERSION:data['version']=cls.VERSIONreturnfrom_dict(cls,data,converters=converters)# use from_dict to otherwise use default behaviour# Now these methods are called instead>>>as_data(Config((0,1)))AssertionError:Incorrectversion!>>from_data(Config,{'version':(0,1)})Config(version=(1,0))DataAsTupleIf you'd simply like a dataclass to be represented as a tuple instead of a dict when callingas_data,
inherent from theDataAsTupleabstract base class.fromdataclass_as_dataimportas_data,DataAsTuple# Create a dataclass inheriting from DataAsTuple@dataclasses.dataclassclassPerson(DataAsTuple):name:strage:int# Calling as_data now returns a tuple>>>as_data(Person("Summer",24))("Summer",24)This merely overridesas_dataandfrom_datato useas_tupleandfrom_tuplefor you respectively.fromdataclass_as_dataimportas_tuple,from_tuple# Same as inheriting from DataAsTuple@dataclasses.dataclassclassPerson:name:strage:intdefas_data(self,*,converters):returnas_tuple(self,converters=converters)@classmethoddeffrom_data(cls,data:tuple,*,converters):returnfrom_tuple(cls,data,converters=converters)Custom convertersdataclass_as_dataalso provides multiple ways to customise how non-dataclass types are converted.By default, no conversion is performed for other class types.
The only exception is when the class type and the data type passed tofrom_datadon't match, in which case it will convert the
data back to the class type by passing it as a single argument to its constructor.Simple property transformationfrom_datasupports very basic custom property converters in the form of single-input functions.
These converters are called on the relevant properties whenfrom_datais called.
Note that all regular types, such asint, are also technically treated this way by default.fromdataclass_as_dataimportfrom_datadeflower_str(value)->str:"""Convert to lowercase str"""returnstr(value).lower()@dataclasses.dataclassclassEmployee:id:intname:lower_str# The `lower_str` converter is called on the value of the `name` parameter>>>from_data(Employee,{'id':123,'name':"Sylvester"})Employee(id=123,name='sylvester')# The string value of `id` is coerced into an int>>>from_data(Employee,{'id':"456",'name':"Sunny"})Employee(id=456,name='sunny')Class conversionIf you have your own classes that you'd like to use in type hints,
you can also configure regular classes to be converted to and from data in a certain way by defining anas_datamethod andfrom_dataclass method.Note:these cannot use theas_dict,from_dict,as_tuple, orfrom_tuplefunctions as they only take a dataclass.fromdataclass_as_dataimportas_data,from_dataclassName:def__init__(self,full_name):self.first,self.last=full_name.split(" ")defas_data(self):returnf"{self.last},{self.first}"@classmethoddeffrom_data(cls,data):last,first=data.split(", ")returncls(f"{first}{last}")def__repr__(self):returnf"{type(self).__name__}('{self.first}{self.last}')"@dataclasses.dataclassclassStudent:name:Name# Data for the class is now represented and converted as desired>>>as_data(Student(Name("Silver Spoon"))){'name':'Spoon, Silver'}>>>from_data(Student,{'name':'Spoon, Silver'})Student(name=Name('Silver Spoon'))Type conversionHow any type is converted can be defined by passing a dictionary of types to their converters to
theconvertersargument supported byas_dataandfrom_data.
This takes a dictionary of types to single-input converter functions to be used during conversion for that type.fromdataclass_as_dataimportas_data,from_data@dataclasses.dataclassclassRandomNumberGenerator:seed:bytesdefbytes_to_str(_bytes:bytes)->str:return_bytes.decode('utf-8')defstr_to_bytes(string:str)->bytes:returnbytes(string,'utf-8')# `bytes` objects are now represented as `str`s>>>as_data(RandomNumberGenerator(b'Sigmath Bytes'),converters={bytes:bytes_to_str}){'seed':'Sigmath Bytes'}>>>from_data(RandomNumberGenerator,{'seed':'Sigmath Bytes'},converters={bytes:str_to_bytes})RandomNumberGenerator(seed=b'Sigmath Bytes')These converters also accept an optional keyword-only argument. If this argument is specified, the converter will also be
applied to all subclasses. Which subclass is being converted will be passed through this argument.This argument does not need to be used to enable subclass-matching for converters.fromdataclass_as_dataimportas_data,from_dataclassID(int):def__repr__(self):returnf"{type(self).__name__}({super().__repr__()})"@dataclasses.dataclassclassUser:name:strid:IDdefint_to_bytes(value:int,*,_cls)->bytes:returnvalue.to_bytes(1,'little')defbytes_to_int(_bytes:bytes,*,cls)->int:returncls.from_bytes(_bytes,'little')# `int` objects and their subclasses are now represented as `bytes`>>>as_data(User("Siggy",ID(123)),converters={int:int_to_bytes}){'name':'Siggy','id':b'{'}>>>from_data(User,{'name':'Siggy','id':b'{'},converters={int:bytes_to_int})User(name='Siggy',id=ID(123))