From 18521a409e637b05ac11b813d272b13a08ea8b31 Mon Sep 17 00:00:00 2001
From: Alexander Schlemmer <alexander@mail-schlemmer.de>
Date: Wed, 16 Mar 2022 09:38:47 +0100
Subject: [PATCH] FIX: fixed a problem with recursion in high level api

---
 src/caosdb/high_level_api.py     | 25 +++++++++++++-----
 unittests/test_high_level_api.py | 45 +++++++++++++++++++++++++++++---
 2 files changed, 61 insertions(+), 9 deletions(-)

diff --git a/src/caosdb/high_level_api.py b/src/caosdb/high_level_api.py
index 5d1f1160..697ef2d3 100644
--- a/src/caosdb/high_level_api.py
+++ b/src/caosdb/high_level_api.py
@@ -674,7 +674,7 @@ class CaosDBPythonEntity(object):
         return entity
         
 
-    def serialize(self, without_metadata: bool = False):
+    def serialize(self, without_metadata: bool = False, visited: dict = None):
         """
         Serialize necessary information into a dict.
 
@@ -682,19 +682,27 @@ class CaosDBPythonEntity(object):
                           If True don't set the metadata field in order to increase
                           readability. Not recommended if deserialization is needed.
         """
+
+        if visited is None:
+            visited = dict()
+
+        if self in visited:
+            return visited[self]
+        
         metadata: dict[str, Any] = dict()
         properties = dict()
         parents = list()
 
         # The full information to be returned:
         fulldict = dict()
+        visited[self] = fulldict
 
         # Add CaosDB role:
         fulldict["role"] = standard_type_for_high_level_type(self, True)
 
         for parent in self._parents:
             if isinstance(parent, CaosDBPythonEntity):
-                parents.append(parent.serialize())
+                parents.append(parent.serialize(without_metadata, visited))
             elif isinstance(parent, CaosDBPythonUnresolvedParent):
                 parents.append({"name": parent.name, "id": parent.id,
                                 "unresolved": True})
@@ -722,7 +730,7 @@ class CaosDBPythonEntity(object):
             if isinstance(val, CaosDBPythonUnresolvedReference):
                 properties[p] = {"id": val.id, "unresolved": True}
             elif isinstance(val, CaosDBPythonEntity):
-                properties[p] = val.serialize(without_metadata)
+                properties[p] = val.serialize(without_metadata, visited)
             elif isinstance(val, list):
                 serializedelements = []
                 for element in val:
@@ -732,7 +740,9 @@ class CaosDBPythonEntity(object):
                         elm["unresolved"] = True
                         serializedelements.append(elm)
                     elif isinstance(element, CaosDBPythonEntity):
-                        serializedelements.append(element.serialize(without_metadata))
+                        serializedelements.append(
+                            element.serialize(without_metadata,
+                                              visited))
                     else:
                         serializedelements.append(element)
                 properties[p] = serializedelements
@@ -749,8 +759,11 @@ class CaosDBPythonEntity(object):
     def __str__(self):
         return yaml.dump(self.serialize(False))
 
-    def __repr__(self):
-        return yaml.dump(self.serialize(True))
+    # This seemed like a good solution, but makes it difficult to
+    # compare python objects directly:
+    #
+    # def __repr__(self):
+    #     return yaml.dump(self.serialize(True))
 
 
 class CaosDBPythonRecord(CaosDBPythonEntity):
diff --git a/unittests/test_high_level_api.py b/unittests/test_high_level_api.py
index f00d3cc0..bca279cd 100644
--- a/unittests/test_high_level_api.py
+++ b/unittests/test_high_level_api.py
@@ -591,6 +591,45 @@ def test_deserialization():
     obj_des = CaosDBPythonEntity.deserialize(serial)
     assert obj.serialize() == obj_des.serialize()
 
-
-def test_recursion():
-    pass
+@pytest.fixture
+def get_record_container():
+    record_xml = """
+<Entities>
+  <Record id="109">
+    <Version id="da669fce50554b2835c3826cf717d6a4532f02de" head="true">
+      <Predecessor id="68534369c5fd05e5bb1d37801a3dbc1532a8e094"/>
+    </Version>
+    <Parent id="103" name="Experiment" description="General type for all experiments in our lab"/>
+    <Property id="104" name="alpha" description="A fictitious measurement" datatype="DOUBLE" unit="km" importance="FIX" flag="inheritance:FIX">16.0</Property>
+    <Property id="107" name="date" datatype="DATETIME" importance="FIX" flag="inheritance:FIX">2022-03-16</Property>
+    <Property id="108" name="identifier" datatype="TEXT" importance="FIX" flag="inheritance:FIX">Demonstration</Property>
+    <Property id="111" name="sources" description="The elements of this lists are scientific activities that this scientific activity is based on." datatype="LIST&lt;ScientificActivity&gt;" importance="FIX" flag="inheritance:FIX">
+      <Value>109</Value>
+    </Property>
+  </Record>
+</Entities>"""
+
+    c = db.Container.from_xml(record_xml)
+    return c
+
+def test_recursion(get_record_container):
+    r = convert_to_python_object(get_record_container[0])
+    r.resolve_references(r, get_record_container)
+    assert r.id == 109
+    assert r.sources[0].id == 109
+    assert r.sources[0].sources[0].id == 109
+    assert "&id001" in str(r)
+    assert "*id001" in str(r)
+
+    d = r.serialize(True)
+    assert r.sources[0] == r.sources[0].sources[0]
+    
+@pytest.mark.xfail
+def test_recursion_advanced(get_record_container):
+    # TODO:
+    # This currently fails, because resolve is done in a second step
+    # and therefore a new python object is created for the reference.
+    r = convert_to_python_object(get_record_container[0])
+    r.resolve_references(r, get_record_container)
+    d = r.serialize(True)
+    assert r == r.sources[0]
-- 
GitLab