diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index a12f986e04..0fbec4c9e3 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -23,7 +23,7 @@ from collections import deque, OrderedDict, defaultdict from collections.abc import MutableSequence, MutableSet, MutableMapping try: - from pickle import dumps, loads + from pickle import dumps, loads, UnpicklingError except ImportError: from pickle import dumps, loads from django.core.exceptions import ObjectDoesNotExist @@ -633,12 +633,12 @@ def to_pickle(data): # not one of the base types if hasattr(item, "__serialize_dbobjs__"): # Allows custom serialization of any dbobjects embedded in - # the item that Evennia will otherwise not found (these would + # the item that Evennia will otherwise not find (these would # otherwise lead to an error). Use the dbserialize helper from # this method. try: item.__serialize_dbobjs__() - except TypeError: + except TypeError as err: # we catch typerrors so we can handle both classes (requiring # classmethods) and instances pass @@ -725,9 +725,13 @@ def from_pickle(data, db_obj=None): # use the dbunserialize helper in this module. try: item.__deserialize_dbobjs__() - except TypeError: + except (TypeError, UnpicklingError): # handle recoveries both of classes (requiring classmethods - # or instances + # or instances. Unpickling errors can happen when re-loading the + # data from cache (because the hidden entity was already + # deserialized and stored back on the object, unpickling it + # again fails). TODO: Maybe one could avoid this retry in a + # more graceful way? pass return item diff --git a/evennia/utils/tests/test_dbserialize.py b/evennia/utils/tests/test_dbserialize.py index 480893c466..5cd5ffceda 100644 --- a/evennia/utils/tests/test_dbserialize.py +++ b/evennia/utils/tests/test_dbserialize.py @@ -15,9 +15,7 @@ class TestDbSerialize(TestCase): """ def setUp(self): - self.obj = DefaultObject( - db_key="Tester", - ) + self.obj = DefaultObject(db_key="Tester") self.obj.save() def test_constants(self): @@ -117,3 +115,57 @@ class TestDbSerialize(TestCase): self.assertEqual(self.obj.db.test, {"a": [1, 2, 3]}) self.obj.db.test |= {"b": [5, 6]} self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]}) + + +class _InvalidContainer: + """Container not saveable in Attribute (if obj is dbobj, it 'hides' it)""" + def __init__(self, obj): + self.hidden_obj = obj + + +class _ValidContainer(_InvalidContainer): + """Container possible to save in Attribute (handles hidden dbobj explicitly)""" + def __serialize_dbobjs__(self): + self.hidden_obj = dbserialize.dbserialize(self.hidden_obj) + def __deserialize_dbobjs__(self): + self.hidden_obj = dbserialize.dbunserialize(self.hidden_obj) + + +class DbObjWrappers(TestCase): + """ + Test the `__serialize_dbobjs__` and `__deserialize_dbobjs__` methods. + + """ + def setUp(self): + super().setUp() + self.dbobj1 = DefaultObject(db_key="Tester1") + self.dbobj1.save() + self.dbobj2 = DefaultObject(db_key="Tester2") + self.dbobj2.save() + + def test_dbobj_hidden_obj__fail(self): + with self.assertRaises(TypeError): + self.dbobj1.db.testarg = _InvalidContainer(self.dbobj1) + + def test_consecutive_fetch(self): + con =_ValidContainer(self.dbobj2) + self.dbobj1.db.testarg = con + attrobj = self.dbobj1.attributes.get("testarg", return_obj=True) + + self.assertEqual(attrobj.value, con) + self.assertEqual(attrobj.value, con) + self.assertEqual(attrobj.value.hidden_obj, self.dbobj2) + + def test_dbobj_hidden_obj__success(self): + con =_ValidContainer(self.dbobj2) + self.dbobj1.db.testarg = con + + # accessing the same data twice + res1 = self.dbobj1.db.testarg + res2 = self.dbobj1.db.testarg + + self.assertEqual(res1, res2) + self.assertEqual(res1, con) + self.assertEqual(res2, con) + self.assertEqual(res1.hidden_obj, self.dbobj2) + self.assertEqual(res2.hidden_obj, self.dbobj2)