[PATCH 1 of 2 compound v3] Propagate skiprtypes to subentities cw_skip_copy_for while cloning

Sylvain Thenault sylvain.thenault at logilab.fr
Tue Dec 19 11:07:33 CET 2017


# HG changeset patch
# User Sylvain Thénault <sylvain.thenault at logilab.fr>
# Date 1513098203 -3600
#      Tue Dec 12 18:03:23 2017 +0100
# Node ID c06fb616ca38ee8a8ea7a9fb71bb8abff9ecc10e
# Parent  00a664463fdbc9db154056f064e767dc54e3f2ab
Propagate skiprtypes to subentities cw_skip_copy_for while cloning

We have to consider it at every levels of the container tree, not only for the
top-level entity.

While doing so, it sounded good to move creation of the list to a cached
property to avoid redoing this for each cloned entity of the tree.

diff --git a/cubicweb_compound/entities.py b/cubicweb_compound/entities.py
--- a/cubicweb_compound/entities.py
+++ b/cubicweb_compound/entities.py
@@ -1,6 +1,6 @@
-# copyright 2015-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr -- mailto:contact at logilab.fr
 #
 # This program is free software: you can redistribute it and/or modify it under
 # the terms of the GNU Lesser General Public License as published by the Free
 # Software Foundation, either version 2.1 of the License, or (at your option)
@@ -27,17 +27,19 @@ Assumptions:
 .. _container: https://www.cubicweb.org/project/cubicweb-container
 """
 
 from itertools import chain
 
+from logilab.common.decorators import cachedproperty
+
 from cubicweb.view import EntityAdapter
 from cubicweb.predicates import is_instance, partial_relation_possible
 
 from . import CompositeGraph
 
 
-def copy_entity(original, **attributes):
+def copy_entity(original, cw_skip_copy_for=None, **attributes):
     """Return a copy of an entity.
 
     Only attributes and non-composite relations are copied (relying on
     `entity.copy_relations()` for the latter).
     """
@@ -48,14 +50,23 @@ def copy_entity(original, **attributes):
                 or (rschema.type, 'subject') in original.cw_skip_copy_for):
             continue
         attr = rschema.type
         attrs.setdefault(attr, original.cw_attr_value(attr))
     clone = original._cw.create_entity(original.cw_etype, **attrs)
+    if cw_skip_copy_for is not None:
+        _extend_skip_copy_for(clone, cw_skip_copy_for)
     clone.copy_relations(original.eid)
     return clone
 
 
+def _extend_skip_copy_for(clone, cw_skip_copy_for):
+    # take care not modifying clone.cw_skip_copy_for **class attribute** to
+    # avoid undesired side effects (e.g. clone called with different
+    # skiprtypes value), so set an instance attribute.
+    clone.cw_skip_copy_for = set(clone.cw_skip_copy_for) | cw_skip_copy_for
+
+
 class IContainer(EntityAdapter):
     """Abstract adapter for entities which are a container root."""
     __abstract__ = True
     __regid__ = 'IContainer'
 
@@ -201,31 +212,35 @@ class IClonableAdapter(EntityAdapter):
         Return a dictionary mapping original entities to their clone.
         """
         assert clone.cw_etype == self.entity.cw_etype, \
             "clone entity type {} does not match with original's {}".format(
                 clone.cw_etype, self.entity.cw_etype)
-        related = self.graph.child_related(
-            self.entity, follow_relations=self.follow_relations)
-        # take care not modifying clone.cw_skip_copy_for class attribute to avoid undesired side
-        # effects (e.g. clone called with different skiprtypes value), set an instance attribute.
-        clone.cw_skip_copy_for = list(chain(
-            clone.cw_skip_copy_for,
-            ((rtype, 'subject') for rtype in self.skiprtypes),
-            ((rtype, 'object') for rtype in self.skiprtypes)))
+        related = list(self.graph.child_related(
+            self.entity, follow_relations=self.follow_relations))
+        _extend_skip_copy_for(clone, self.skip_swallow_copy_for)
         with self._cw.deny_all_hooks_but(*self.enabled_hook_categories):
             clone.copy_relations(self.entity.eid)
             clones = {self.entity: clone}
             for parent, (rtype, parent_role), child in related:
                 rel = rtype if parent_role == 'object' else 'reverse_' + rtype
                 kwargs = {rel: clones[parent]}
                 clone = clones.get(child)
                 if clone is not None:
                     clone.cw_set(**kwargs)
                 else:
-                    clones[child] = copy_entity(child, **kwargs)
+                    clones[child] = copy_entity(
+                        child, self.skip_swallow_copy_for, **kwargs)
             return clones
 
+    @cachedproperty
+    def skip_swallow_copy_for(self):
+        return set(chain(
+            # turn skiprtypes into a list suitable for Entity.cw_skip_copy_for
+            ((rtype, 'subject') for rtype in self.skiprtypes),
+            ((rtype, 'object') for rtype in self.skiprtypes),
+        ))
+
 
 def registration_callback(vreg):
     vreg.register_all(globals().values(), __name__)
     # Necessary during db-init or test mode.
     IClonableAdapter.clone_relations.clear()
diff --git a/test/data/schema.py b/test/data/schema.py
--- a/test/data/schema.py
+++ b/test/data/schema.py
@@ -105,5 +105,10 @@ class Group(EntityType):
 
 class member(RelationDefinition):
     """Indicates a member of a Group."""
     subject = 'Group'
     object = 'Agent'
+
+
+class see_also(RelationDefinition):
+    subject = 'Biography'
+    object = 'Event'
diff --git a/test/test_compound.py b/test/test_compound.py
--- a/test/test_compound.py
+++ b/test/test_compound.py
@@ -359,10 +359,20 @@ class CloneTC(CubicWebTC):
             orig_cw_skip_copy_for = bob.cw_skip_copy_for
             clone = clone_agent(cnx, u'bob', skiprtypes=('member',))
             self.assertFalse(clone.reverse_member)
             self.assertIs(bob.cw_skip_copy_for, orig_cw_skip_copy_for)
 
+    def test_clone_skiprtypes_sublevel(self):
+        with self.admin_access.repo_cnx() as cnx:
+            event = cnx.create_entity('Event')
+            bio = cnx.create_entity('Biography', event=event, see_also=event)
+            cnx.create_entity('Agent', name=u'bob', biography=bio)
+            cnx.commit()
+
+            clone = clone_agent(cnx, u'bob', skiprtypes=('see_also',))
+            self.assertEqual(clone.biography[0].see_also, ())
+
     def test_clone_full(self):
         with self.admin_access.repo_cnx() as cnx:
             agent = cnx.create_entity('Agent', name=u'bob')
             cnx.create_entity('OnlineAccount', reverse_account=agent)
             bio = cnx.create_entity('Biography', reverse_biography=agent)


More information about the saem-devel mailing list