[Checkins] SVN: zc.copy/trunk/src/zc/copy/ The copy side of copyversion.

Gary Poster gary at zope.com
Wed Aug 23 16:38:14 EDT 2006


Log message for revision 69737:
  The copy side of copyversion.
  

Changed:
  A   zc.copy/trunk/src/zc/copy/README.txt
  A   zc.copy/trunk/src/zc/copy/__init__.py
  A   zc.copy/trunk/src/zc/copy/configure.zcml
  A   zc.copy/trunk/src/zc/copy/interfaces.py
  A   zc.copy/trunk/src/zc/copy/overrides.zcml
  A   zc.copy/trunk/src/zc/copy/tests.py

-=-
Copied: zc.copy/trunk/src/zc/copy/README.txt (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/copier.txt)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/copier.txt	2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.copy/trunk/src/zc/copy/README.txt	2006-08-23 20:38:14 UTC (rev 69737)
@@ -0,0 +1,413 @@
+The copier module has two main components: a generic replacement for
+zope.location.pickling.locationCopy called zc.copyversion.copier.copy,
+and a replacement for zope.copypastemove.ObjectCopier that uses the new
+copy function. Additionally, the module contains an adapter for use with
+the new copy function that gives the same ILocation behavior as
+locationCopy.
+
+These three components (the new copy, the new ObjectCopier, and the
+ILocation adapter) are appropriate for inclusion in Zope 3, should that be
+desired.
+
+The heart of the module, then, is the new copy function.  Like locationCopy,
+this function uses pickling to perform the copy; however, instead of the
+hard-wired heuristic in locationCopy to determine what should be copied and
+what should remain static, this function uses adapters for pluggable behavior.
+
+Also, copy automatically sets __parent__ and __name__ of the object copy to 
+be None, if values exist for them.  If you do not want this behavior, a `clone`
+method does not include this logic.  For most use with classic Zope 3
+locations, however, you will want to use `copy`.  We'll look at a bit at both
+functions in this document.
+
+The clone function (and thus the copy function that wraps clone) uses
+pickle to copy the object and all its subobjects recursively.  As each
+object and subobject is pickled, the function tries to adapt it to
+zc.copyversion.interfaces.ICopyHook. If a copy hook is found, the
+recursive copy is halted.  The hook is called with two values: the
+main, top-level object that is being copied; and a callable that
+supports registering functions to be called after the copy is made. 
+The copy hook should return the exact object or subobject that should
+be used at this point in the copy, or raise
+zc.copyversion.interfaces.ResumeCopy to resume copying the object or
+subobject recursively after all.
+
+We'll examine the callable a bit later: first let's examine a simple
+use.  A simple hook is to support the use case of resetting the state of
+data that should be changed in a copy--for instance, a log, or freezing or
+versioning data.  The canonical way to do this is by storing the changable
+data on a special sub-object of the object that is to be copied.  We'll
+look at a simple case of a subobject that should be converted to None when it
+is copied--the way that the zc.freeze copier hook works.  Also see the
+zc.objectlog copier module for a similar example.
+
+So, here is a simple object that stores a boolean on a special object.
+
+    >>> class Demo(object):
+    ...     _frozen = None
+    ...     def isFrozen(self):
+    ...         return self._frozen is not None
+    ...     def freeze(self):
+    ...         self._frozen = Data()
+    ...
+    >>> class Data(object):
+    ...     pass
+    ...
+
+Here's what happens if we copy one of these objects without a copy hook.
+
+    >>> original = Demo()
+    >>> original.isFrozen()
+    False
+    >>> original.freeze()
+    >>> original.isFrozen()
+    True
+    >>> import zc.copy
+    >>> copy = zc.copy.copy(original)
+    >>> copy is original
+    False
+    >>> copy.isFrozen()
+    True
+
+Now let's make a super-simple copy hook that always returns None, no
+matter what the main location being copied is.  We'll register it and
+make another copy.
+
+    >>> import zope.component
+    >>> import zope.interface
+    >>> import zc.copy.interfaces
+    >>> def _factory(location, register):
+    ...     return None
+    >>> @zope.component.adapter(Data)
+    ... @zope.interface.implementer(zc.copy.interfaces.ICopyHook)
+    ... def data_copyfactory(obj):
+    ...     return _factory
+    ...
+
+    >>> zope.component.provideAdapter(data_copyfactory)
+    >>> copy2 = zc.copy.copy(original)
+    >>> copy2 is original
+    False
+    >>> copy2.isFrozen()
+    False
+
+Much better.
+
+The ILocation adapter is just a tiny bit more complicated.  Look in
+__init__.py at location_copyfactory.  Here, if the object implements
+ILocation and is not 'inside' the main object being copied, it is used
+directly, and not copied.  Otherwise, the hook raises ResumeCopy to
+cancel itself.
+
+[the following is adapted from a doctest in zope.location.pickling]
+
+For example, suppose we have an object (location) hierarchy like this::
+
+           o1
+          /  \
+        o2    o3
+        |     |
+        o4    o5
+
+    >>> from zope.location.location import Location
+    >>> o1 = Location()
+    >>> o1.o2 = Location(); o1.o2.__parent__ = o1
+    >>> o1.o3 = Location(); o1.o3.__parent__ = o1
+    >>> o1.o2.o4 = Location(); o1.o2.o4.__parent__ = o1.o2
+    >>> o1.o3.o5 = Location(); o1.o3.o5.__parent__ = o1.o3
+
+In addition, o3 has a non-location reference to o4.
+
+    >>> o1.o3.o4 = o1.o2.o4
+
+When we copy o3, we want to get a copy of o3 and o5, with
+references to o1 and o4.  Without our adapter, this won't happen.
+
+    >>> c3 = zc.copy.copy(o1.o3)
+    >>> c3 is o1.o3 # it /is/ a copy
+    False
+    >>> o1.o3.o4 is o1.o2.o4
+    True
+    >>> c3.o4 is o1.o2.o4
+    False
+
+The c3.__parent__ will be None, because we used copy, rather than clone.
+
+    >>> o1.o3.__parent__ is o1
+    True
+    >>> c3.__parent__ is None
+    True
+
+If we had used clone, then the __parent__ would also have been included.
+
+    >>> another3 = zc.copy.clone(o1.o3)
+    >>> another3.__parent__ is o1 # the __parent__ has also been copied.
+    False
+    >>> another3.__parent__ is None
+    False
+
+In Zope 3, that would effectively mean that any object that was transitively
+linked with __parent__ links to the root of the Zope application would get the
+*entire Zope database* copied.  Not good.  Using the `clone` method, you'll
+see the objects; the `copy` method still makes the copy, but rips it off at the
+end, so it can be *very* inefficient.  And in fact, with our first c3, we do
+have a copy of o1, just hidden away.
+
+    >>> o1.o3.o4.__parent__.__parent__ is o1
+    True
+    >>> c3.o4.__parent__.__parent__ is o1
+    False
+    >>> c3.o4.__parent__.__parent__ is None
+    False
+
+How can we fix all this?  Register our adapter and the results are as we wish.
+
+    >>> zope.component.provideAdapter(
+    ...     zc.copy.location_copyfactory)
+    >>> c3 = zc.copy.copy(o1.o3)
+    >>> c3 is o1.o3
+    False
+    >>> c3.__parent__ is None # because we used `copy`, not `clone`
+    True
+    >>> c3.o4 is o1.o2.o4
+    True
+    >>> c3.o5 is o1.o3.o5
+    False
+    >>> c3.o5.__parent__ is c3
+    True
+
+If we used clone, then we could see that the adapter also handled c3.__parent__
+the right way.
+
+    >>> another3 = zc.copy.clone(o1.o3)
+    >>> another3.__parent__ is o1
+    True
+
+[end variation of zope.location.pickling test]
+
+Our final step in the tour of the copy method is to look at the registration
+function that the hook can use.  It is useful for resetting objects within the
+new copy--for instance, back references such as __parent__ pointers.  This is
+used concretely in the zc.objectlog.copier module; we will come up with a
+similar but artificial example here.
+
+Imagine an object with a subobject that is "located" (i.e., zope.location) on
+the parent and should be replaced whenever the main object is copied.
+
+    >>> class Subobject(Location):
+    ...     def __init__(self):
+    ...         self.counter = 0
+    ...     def __call__(self):
+    ...         res = self.counter
+    ...         self.counter += 1
+    ...         return res
+    ...
+    >>> o = Location()
+    >>> s = Subobject()
+    >>> import zope.location.location
+    >>> o.subobject = s
+    >>> zope.location.locate(s, o, 'subobject')
+    >>> s.__parent__ is o
+    True
+    >>> o.subobject()
+    0
+    >>> o.subobject()
+    1
+    >>> o.subobject()
+    2
+
+Without an ICopyHook, this will simply duplicate the subobject, with correct
+new pointers.
+
+    >>> c = zc.copy.copy(o)
+    >>> c.subobject.__parent__ is c
+    True
+
+Note that the subobject has also copied state.
+
+    >>> c.subobject()
+    3
+    >>> o.subobject()
+    3
+
+Our goal will be to make the counters restart when they are copied.  We'll do
+that with a copy hook.
+
+This copy hook is different: it provides an object to replace the old object,
+but then it needs to set it up further after the copy is made.  This is
+accomplished by registering a callable, `reparent` here, that sets up the
+__parent__.  The callable is passed a function that can translate something
+from the original object into the equivalent on the new object.  We use this
+to find the new parent, so we can set it.
+
+    >>> import zope.component
+    >>> import zope.interface
+    >>> import zc.copy.interfaces
+    >>> @zope.component.adapter(Subobject)
+    ... @zope.interface.implementer(zc.copy.interfaces.ICopyHook)
+    ... def subobject_copyfactory(original):
+    ...     def factory(location, register):
+    ...         obj = Subobject()
+    ...         def reparent(translate):
+    ...             obj.__parent__ = translate(original.__parent__)
+    ...         register(reparent)
+    ...         return obj
+    ...     return factory
+    ...
+    >>> zope.component.provideAdapter(subobject_copyfactory)
+
+Now when we copy, the new subobject will have the correct, revised __parent__,
+but will be otherwise reset (here, just the counter)
+
+    >>> c = zc.copy.copy(o)
+    >>> c.subobject.__parent__ is c
+    True
+    >>> c.subobject()
+    0
+    >>> o.subobject()
+    4
+
+ObjectCopier
+============
+
+The ObjectCopier in the copier module is simply a variation on the
+ObjectCopier in zope.copypastemove, with the change that it uses the
+zc.copy.copy function rather than zope.location.pickling.locationCopy. 
+With the location-based copy hook described above already installed, the
+copier should have the same behavior. In that vein, the following is
+adapted from the test in zope/copypastemove/__init__.py.
+
+To use an object copier, pass a contained `object` to the class.
+The contained `object` should implement `IContained`.  It should be
+contained in a container that has an adapter to `INameChooser`.
+
+    >>> from zope.copypastemove import ExampleContainer
+    >>> from zope.app.container.contained import Contained
+    >>> ob = Contained()
+    >>> container = ExampleContainer()
+    >>> container[u'foo'] = ob
+    >>> copier = zc.copy.ObjectCopier(ob)
+
+In addition to moving objects, object copiers can tell you if the
+object is movable:
+
+    >>> copier.copyable()
+    True
+
+which, at least for now, they always are.  A better question to
+ask is whether we can copy to a particular container. Right now,
+we can always copy to a container of the same class:
+
+    >>> container2 = ExampleContainer()
+    >>> copier.copyableTo(container2)
+    True
+    >>> copier.copyableTo({})
+    Traceback (most recent call last):
+    ...
+    TypeError: Container is not a valid Zope container.
+
+Of course, once we've decided we can copy an object, we can use
+the copier to do so:
+
+    >>> copier.copyTo(container2)
+    u'foo'
+    >>> list(container)
+    [u'foo']
+    >>> list(container2)
+    [u'foo']
+    >>> ob.__parent__ is container
+    True
+    >>> container2[u'foo'] is ob
+    False
+    >>> container2[u'foo'].__parent__ is container2
+    True
+    >>> container2[u'foo'].__name__
+    u'foo'
+
+We can also specify a name:
+
+    >>> copier.copyTo(container2, u'bar')
+    u'bar'
+    >>> l = list(container2)
+    >>> l.sort()
+    >>> l
+    [u'bar', u'foo']
+
+    >>> ob.__parent__ is container
+    True
+    >>> container2[u'bar'] is ob
+    False
+    >>> container2[u'bar'].__parent__ is container2
+    True
+    >>> container2[u'bar'].__name__
+    u'bar'
+
+But we may not use the same name given, if the name is already in
+use:
+
+    >>> copier.copyTo(container2, u'bar')
+    u'bar_'
+    >>> l = list(container2)
+    >>> l.sort()
+    >>> l
+    [u'bar', u'bar_', u'foo']
+    >>> container2[u'bar_'].__name__
+    u'bar_'
+
+
+If we try to copy to an invalid container, we'll get an error:
+
+    >>> copier.copyTo({})
+    Traceback (most recent call last):
+    ...
+    TypeError: Container is not a valid Zope container.
+
+Do a test for preconditions:
+
+    >>> import zope.interface
+    >>> import zope.schema
+    >>> def preNoZ(container, name, ob):
+    ...     "Silly precondition example"
+    ...     if name.startswith("Z"):
+    ...         raise zope.interface.Invalid("Invalid name.")
+
+    >>> class I1(zope.interface.Interface):
+    ...     def __setitem__(name, on):
+    ...         "Add an item"
+    ...     __setitem__.precondition = preNoZ
+
+    >>> from zope.app.container.interfaces import IContainer
+    >>> class C1(object):
+    ...     zope.interface.implements(I1, IContainer)
+    ...     def __repr__(self):
+    ...         return 'C1'
+
+    >>> container3 = C1()
+    >>> copier.copyableTo(container3, 'ZDummy')
+    False
+    >>> copier.copyableTo(container3, 'newName')
+    True
+
+And a test for constraints:
+
+    >>> def con1(container):
+    ...     "silly container constraint"
+    ...     if not hasattr(container, 'x'):
+    ...         return False
+    ...     return True
+    ...
+    >>> class I2(zope.interface.Interface):
+    ...     __parent__ = zope.schema.Field(constraint=con1)
+    ...
+    >>> class constrainedObject(object):
+    ...     zope.interface.implements(I2)
+    ...     def __init__(self):
+    ...         self.__name__ = 'constrainedObject'
+    ...
+    >>> cO = constrainedObject()
+    >>> copier2 = zc.copy.ObjectCopier(cO)
+    >>> copier2.copyableTo(container)
+    False
+    >>> container.x = 1
+    >>> copier2.copyableTo(container)
+    True

Copied: zc.copy/trunk/src/zc/copy/__init__.py (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/copier.py)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/copier.py	2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.copy/trunk/src/zc/copy/__init__.py	2006-08-23 20:38:14 UTC (rev 69737)
@@ -0,0 +1,125 @@
+# replaces code in zope.copypastemove and in zope.location.pickling
+
+import tempfile
+import cPickle
+import zope.component
+import zope.event
+import zope.lifecycleevent
+import zope.copypastemove
+import zope.app.container.interfaces
+import zope.app.container.constraints
+import zope.location
+import zope.location.location
+import zope.location.interfaces
+
+from zc.copy import interfaces
+
+# this is the part that replaces zope.location.pickling
+ at zope.component.adapter(zope.location.interfaces.ILocation)
+ at zope.interface.implementer(interfaces.ICopyHook)
+def location_copyfactory(obj):
+    def factory(location, register):
+        if not zope.location.location.inside(obj, location):
+            return obj
+        raise interfaces.ResumeCopy
+    return factory
+
+# this is a more general form of zope.location.pickling.copyLocation
+def clone(loc):
+    tmp = tempfile.TemporaryFile()
+    persistent = CopyPersistent(loc)
+
+    # Pickle the object to a temporary file
+    pickler = cPickle.Pickler(tmp, 2)
+    pickler.persistent_id = persistent.id
+    pickler.dump(loc)
+
+    # Now load it back
+    tmp.seek(0)
+    unpickler = cPickle.Unpickler(tmp)
+    unpickler.persistent_load = persistent.load
+
+    res = unpickler.load()
+    # run the registered cleanups
+    def convert(obj):
+        return unpickler.memo[pickler.memo[id(obj)][0]]
+    for call in persistent.registered:
+        call(convert)
+    return res
+
+def copy(loc):
+    res = clone(loc)
+    if getattr(res, '__parent__', None) is not None:
+        try:
+            res.__parent__ = None
+        except AttributeError:
+            pass
+    if getattr(res, '__name__', None) is not None:
+        try:
+            res.__name__ = None
+        except AttributeError:
+            pass
+    return res
+
+class CopyPersistent(object):
+    def __init__(self, location):
+        self.location = location
+        self.pids_by_id = {}
+        self.others_by_pid = {}
+        self.load = self.others_by_pid.get
+        self.registered = []
+
+    def id(self, obj):
+        hook = interfaces.ICopyHook(obj, None)
+        if hook is not None:
+            oid = id(obj)
+            if oid in self.pids_by_id:
+                return self.pids_by_id[oid]
+            try:
+                res = hook(self.location, self.registered.append)
+            except interfaces.ResumeCopy:
+                pass
+            else:
+                pid = len(self.others_by_pid)
+    
+                # The following is needed to overcome a bug
+                # in pickle.py. The pickle checks the boolean value
+                # of the id, rather than whether it is None.
+                pid += 1
+    
+                self.pids_by_id[oid] = pid
+                self.others_by_pid[pid] = res
+                return pid
+        return None
+
+# this is a generic object copier that uses the new copier above.
+class ObjectCopier(zope.copypastemove.ObjectCopier):
+
+    def copyTo(self, target, new_name=None):
+        """Copy this object to the `target` given.
+
+        Returns the new name within the `target`.
+
+        Typically, the `target` is adapted to `IPasteTarget`.
+        After the copy is added to the `target` container, publish
+        an `IObjectCopied` event in the context of the target container.
+        If a new object is created as part of the copying process, then
+        an `IObjectCreated` event should be published.
+        """
+        obj = self.context
+        container = obj.__parent__
+
+        orig_name = obj.__name__
+        if new_name is None:
+            new_name = orig_name
+
+        zope.app.container.constraints.checkObject(target, new_name, obj)
+
+        chooser = zope.app.container.interfaces.INameChooser(target)
+        new_name = chooser.chooseName(new_name, obj)
+
+        new = copy(obj)
+        zope.event.notify(zope.lifecycleevent.ObjectCopiedEvent(new, obj))
+
+        target[new_name] = new
+        return new_name

Copied: zc.copy/trunk/src/zc/copy/configure.zcml (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/configure.zcml)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/configure.zcml	2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.copy/trunk/src/zc/copy/configure.zcml	2006-08-23 20:38:14 UTC (rev 69737)
@@ -0,0 +1,14 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="zc.copyversion">
+
+  <adapter factory=".location_copyfactory" />
+
+  <!-- this usually doesn't work here, because it is beneath the level in
+       which zope.copypastemove is loaded.
+       Add its contents yourself to an overrides file specified in
+       site.zcml or a similar high level.
+  <includeOverrides file="overrides.zcml"/>
+  -->
+
+</configure>

Copied: zc.copy/trunk/src/zc/copy/interfaces.py (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/interfaces.py)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/interfaces.py	2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.copy/trunk/src/zc/copy/interfaces.py	2006-08-23 20:38:14 UTC (rev 69737)
@@ -0,0 +1,19 @@
+from zope import interface
+
+class ResumeCopy(Exception):
+    """do not use the hook: continue copying recursively
+    (see ICopyHook.__call__)"""
+
+class ICopyHook(interface.Interface):
+    """an adapter to an object that is being copied"""
+    def __call__(location, register):
+        """Given the top-level location that is being copied, return the
+        version of the adapted object that should be used in the new copy.
+
+        raising ResumeCopy means that you are foregoing the hook: the
+        adapted object will continue to be recursively copied.
+
+        If you need to have a post-creation cleanup, register a callable with
+        `register`.  This callable must take a single argument: a callable that,
+        given an object from the original, returns the equivalent in the copy.
+        """

Copied: zc.copy/trunk/src/zc/copy/overrides.zcml (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/overrides.zcml)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/overrides.zcml	2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.copy/trunk/src/zc/copy/overrides.zcml	2006-08-23 20:38:14 UTC (rev 69737)
@@ -0,0 +1,11 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    >
+
+  <adapter
+      factory=".ObjectCopier"
+      permission="zope.ManageContent"
+      trusted="y"
+      />
+
+</configure>

Added: zc.copy/trunk/src/zc/copy/tests.py
===================================================================
--- zc.copy/trunk/src/zc/copy/tests.py	2006-08-23 20:21:36 UTC (rev 69736)
+++ zc.copy/trunk/src/zc/copy/tests.py	2006-08-23 20:38:14 UTC (rev 69737)
@@ -0,0 +1,28 @@
+import unittest
+import zope.testing.module
+from zope.testing import doctest
+from zope.component import testing, eventtesting
+from zope.app.container.tests.placelesssetup import PlacelessSetup
+
+container_setup = PlacelessSetup()
+
+def copierSetUp(test):
+    zope.testing.module.setUp(test, 'zc.copy.doctest')
+    testing.setUp(test)
+    eventtesting.setUp(test)
+    container_setup.setUp()
+
+def copierTearDown(test):
+    zope.testing.module.tearDown(test)
+    testing.tearDown(test)
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite(
+            'README.txt',
+            setUp=copierSetUp,
+            tearDown=copierTearDown),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')



More information about the Checkins mailing list