Circular References#
Below are examples of dealing with circular references when using the default functionality provided by the AutoPreserveReferences and ResolvePreservedReferences semantics (enabled by default). When these semantics are not used, circular references will simply blow up the execution stack and cause a RecursionError, or they will cause PreservedReference instances to be left wherever they appear in the serialized data in the user objects.
Basic Example - Failure#
from grave_settings.abstract import Serializable
from grave_settings.formatters.json import JsonFormatter
class MyObject(Serializable):
def __init__(self):
self.foo = self
def __str__(self):
return f'MyObject(id={id(self)}, foo={id(self.foo)})'
formatter = JsonFormatter()
obj_str = formatter.dumps(MyObject())
print(obj_str)
remade_obj = formatter.loads(obj_str)
print('-----------------')
print(remade_obj)
The above code will crash and raise PreservedReferenceNotDissolvedError. This is the expected behavior because the DetonateDanglingPreservedReferences semantic is enabled by default. At the end of the deserialization process the foo attribute will hold an object of type PreservedReference and we can take a look at it if we disable DetonateDanglingPreservedReferences
Looking at PreservedReference#
from grave_settings.abstract import Serializable
from grave_settings.formatters.json import JsonFormatter
from grave_settings.semantics import DetonateDanglingPreservedReferences
class MyObject(Serializable):
def __init__(self):
self.foo = self
def __str__(self):
return f'MyObject(id={id(self)}, foo={id(self.foo)})'
formatter = JsonFormatter()
formatter.add_semantics(DetonateDanglingPreservedReferences(False))
obj_str = formatter.dumps(MyObject())
print(obj_str)
remade_obj = formatter.loads(obj_str)
print('-----------------')
print(remade_obj)
print(remade_obj.foo)
{
"__class__": "__main__.MyObject",
"foo": {
"__class__": "grave_settings.formatter_settings.PreservedReference",
"ref": ""
}
}
-----------------
MyObject(id=140376620921168, foo=140376620918976)
PreservedReference(ref='', obj=None)
Since this circular reference is a simple as it gets, it may seem like an odd choice to not have some automatic remediation of our problem, here. In reality, I could not think of a good solution to automatically solving this problem that was reasonably efficient and could handle circular references nested in dicts, lists, managed / unmanaged objects, etc without enforcing strict rules about reference key paths and the interface of encapsulating objects like __getitem__ or object.__getattr__(). For now, we just have to take some extra steps to deal with them.
Note
There is a notably inefficient automatic process for fixing preserved references build into Serializable but it needs a NotifyFinalizedMethodName semantic to activate it.
Fixing a circular reference#
from grave_settings.abstract import Serializable
from grave_settings.formatters.json import JsonFormatter
from grave_settings.formatter_settings import PreservedReference, FormatterContext
from grave_settings.semantics import NotifyFinalizedMethodName
class MyObject(Serializable):
def __init__(self):
self.foo = self
@classmethod
def check_in_deserialization_context(cls, context: FormatterContext):
context.add_frame_semantics(NotifyFinalizedMethodName('finalize')) # [1]
def finalize(self, context: FormatterContext) -> None:
if isinstance(self.foo, PreservedReference):
self.foo = context.check_ref(self.foo) # [2]
def __str__(self):
return f'MyObject(id={id(self)}, foo={id(self.foo)})'
formatter = JsonFormatter()
obj_str = formatter.dumps(MyObject())
print(obj_str)
remade_obj = formatter.loads(obj_str)
print('-----------------')
print(remade_obj)
{
"__class__": "__main__.MyObject",
"foo": {
"__class__": "grave_settings.formatter_settings.PreservedReference",
"ref": ""
}
}
-----------------
MyObject(id=140552547823568, foo=140552547823568)
Note [1]
We add NotifyFinalizedMethodName to the frame to inform the formatter that the method finalize is responsible for ensuring the deserialization is wrapped up. It is in finalize that we will fix the circular reference
Note [2]
The FormatterContext has methods that make swapping a PreservedReference for its actual value easy.
Note
Adding the NotifyFinalizedMethodName semantic to the frame without defining finalize() will call the base-classes finalize() method.