# Released under the MIT License. See LICENSE for details.#"""Extra rarely-needed functionality related to dataclasses."""from__future__importannotationsimportdataclassesfromtypingimportTYPE_CHECKING,overrideifTYPE_CHECKING:fromtypingimportAny
[docs]defdataclass_diff(obj1:Any,obj2:Any)->str:"""Generate a string showing differences between two dataclass instances. Both must be of the exact same type. """diff=_diff(obj1,obj2,2)return' <no differences>'ifdiff==''elsediff
[docs]classDataclassDiff:"""Wraps dataclass_diff() in an object for efficiency. It is preferable to pass this to logging calls instead of the final diff string since the diff will never be generated if the associated logging level is not being emitted. """def__init__(self,obj1:Any,obj2:Any):self._obj1=obj1self._obj2=obj2@overridedef__repr__(self)->str:returndataclass_diff(self._obj1,self._obj2)
def_diff(obj1:Any,obj2:Any,indent:int)->str:assertdataclasses.is_dataclass(obj1)assertdataclasses.is_dataclass(obj2)iftype(obj1)isnottype(obj2):raiseTypeError(f'Passed objects are not of the same'f' type ({type(obj1)} and {type(obj2)}).')bits:list[str]=[]indentstr=' '*indentfields=dataclasses.fields(obj1)forfieldinfields:fieldname=field.nameval1=getattr(obj1,fieldname)val2=getattr(obj2,fieldname)# For nested dataclasses, dive in and do nice piecewise compares.if(dataclasses.is_dataclass(val1)anddataclasses.is_dataclass(val2)andtype(val1)istype(val2)):diff=_diff(val1,val2,indent+2)ifdiff!='':bits.append(f'{indentstr}{fieldname}:')bits.append(diff)# For all else just do a single line# (perhaps we could improve on this for other complex types)else:ifval1!=val2:bits.append(f'{indentstr}{fieldname}: {val1} -> {val2}')return'\n'.join(bits)