[Checkins] SVN: zc.copyversion/trunk/src/zc/copyversion/ initial checkin. Note that we hope to be embarrassed about the fact that we checked this project in, then almost immediately refactored it into zc.copy and zc.freeze. Otherwise we will be stuck with an unhelpful name for a longer time.

Gary Poster gary at zope.com
Tue Aug 15 16:50:22 EDT 2006


Log message for revision 69536:
  initial checkin.  Note that we hope to be embarrassed about the fact that we checked this project in, then almost immediately refactored it into zc.copy and zc.freeze.  Otherwise we will be stuck with an unhelpful name for a longer time.
  

Changed:
  A   zc.copyversion/trunk/src/zc/copyversion/README.txt
  A   zc.copyversion/trunk/src/zc/copyversion/__init__.py
  A   zc.copyversion/trunk/src/zc/copyversion/configure.zcml
  A   zc.copyversion/trunk/src/zc/copyversion/copier.py
  A   zc.copyversion/trunk/src/zc/copyversion/copier.txt
  A   zc.copyversion/trunk/src/zc/copyversion/interfaces.py
  A   zc.copyversion/trunk/src/zc/copyversion/overrides.zcml
  A   zc.copyversion/trunk/src/zc/copyversion/rwproperty.py
  A   zc.copyversion/trunk/src/zc/copyversion/rwproperty.txt
  A   zc.copyversion/trunk/src/zc/copyversion/subscribers.py
  A   zc.copyversion/trunk/src/zc/copyversion/tests.py
  A   zc.copyversion/trunk/src/zc/copyversion/versioning.py

-=-
Added: zc.copyversion/trunk/src/zc/copyversion/README.txt
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/README.txt	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/README.txt	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,248 @@
+====================
+Copy Version Control
+====================
+
+This package implements basic functionality for one kind of approach for copy-
+based version control: spellings to query whether an object can be versioned,
+to query whether it has been versioned, and to actually version an object.
+Further policies may be implemented above the basic code in this package; and
+much of the code in this package is offered as pluggable choices which can be
+omitted while still keeping the basic API.
+
+To discover whether an object is versionable, client code should ask if it
+provides zc.copyversion.interfaces.IVersionable.
+
+Site configurations or code that declares that an object is IVersionable is
+assuring that the object provides or can be adaptable to
+zc.copyversion.interfaces.IVersioning.  This interface has only two components:
+_z_versioned is a readonly boolean that returns whether the object has been
+versioned; and _z_version is a method that actually versions the object.  If
+the object is already versioned, it raises
+zc.copyversion.interfaces.VersionedError.  If the object is not in a state to
+be versioned, it may raise zc.copyversion.interfaces.VersioningError.
+If the versioning may succeed, the method should send a
+zc.copyversion.interfaces.IObjectVersionedEvent (such as
+zc.copyversion.interfaces.ObjectVersionedEvent).
+
+That's the heart of the package: an API and an agreement, with nothing to test
+directly.  One policy that this package does not directly support is that
+versioning an object might first create a copy and then version the copy
+rather than the original; or version the original but replace the copy in the
+location of the original; or make any other choices.  These approaches are
+intended to be implemented on top of--above--the zc.copyversion API.  This
+package provides much simpler capabilities.
+
+Conveniences
+============
+
+The package does provide two default implementations of IVersioning, and a few
+conveniences.
+
+One IVersioning implementation is for objects that are directly aware of this
+API (as opposed to having the functionality assembled from adapters and other
+components).
+
+    >>> from zc.copyversion import versioning
+    >>> v = versioning.Versioning()
+    >>> from zc.copyversion import interfaces
+    >>> from zope.interface.verify import verifyObject
+    >>> verifyObject(interfaces.IVersioning, v)
+    True
+    >>> verifyObject(interfaces.IVersionable, v)
+    True
+    >>> v._z_versioned
+    False
+    >>> v._z_versioned = True
+    Traceback (most recent call last):
+    ...
+    AttributeError: can't set attribute
+    >>> import pytz
+    >>> import datetime
+    >>> before = datetime.datetime.now(pytz.utc)
+    >>> v._z_version()
+    >>> before <= v._z_version_timestamp <= datetime.datetime.now(pytz.utc)
+    True
+    >>> v._z_versioned
+    True
+    >>> interfaces.IObjectVersionedEvent.providedBy(events[-1])
+    True
+    >>> events[-1].object is v
+    True
+    >>> v._z_version()
+    Traceback (most recent call last):
+    ...
+    VersionedError
+
+Another available implementation is an adapter, and stores the information in
+an annotation.  Here's a quick demo.
+
+    >>> import zope.annotation.interfaces
+    >>> from zope import interface, component
+    >>> class Demo(object):
+    ...     interface.implements(zope.annotation.interfaces.IAnnotatable)
+    ...
+    >>> import UserDict
+    >>> class DemoAnnotations(UserDict.UserDict):
+    ...     interface.implements(zope.annotation.interfaces.IAnnotations)
+    ...     component.adapts(Demo)
+    ...     def __init__(self, context):
+    ...         self.context = context
+    ...         self.data = getattr(context, '_z_demo', None)
+    ...         if self.data is None:
+    ...             self.data = context._z_demo = {}
+    ...
+    >>> component.provideAdapter(DemoAnnotations)
+    >>> component.provideAdapter(versioning.VersioningAdapter)
+    >>> d = Demo()
+    >>> verifyObject(interfaces.IVersioning, interfaces.IVersioning(d))
+    True
+    >>> verifyObject(interfaces.IVersionable, interfaces.IVersioning(d))
+    True
+    >>> interfaces.IVersioning(d)._z_versioned
+    False
+    >>> interfaces.IVersioning(d)._z_versioned = True
+    Traceback (most recent call last):
+    ...
+    AttributeError: can't set attribute
+    >>> before = datetime.datetime.now(pytz.utc)
+    >>> interfaces.IVersioning(d)._z_version()
+    >>> (before <= interfaces.IVersioning(d)._z_version_timestamp <=
+    ...  datetime.datetime.now(pytz.utc))
+    True
+    >>> interfaces.IVersioning(d)._z_versioned
+    True
+    >>> from zc.copyversion import interfaces
+    >>> interfaces.IObjectVersionedEvent.providedBy(events[-1])
+    True
+    >>> events[-1].object is d
+    True
+    >>> interfaces.IVersioning(d)._z_version()
+    Traceback (most recent call last):
+    ...
+    VersionedError
+
+The versioning module also contains three helpers for writing properties and
+methods that are version-aware.
+
+A 'method' function can generate a version-aware method that raises a
+VersionedError if the object has been versioned.
+
+'setproperty' and 'delproperty' functions can generate a version-aware
+descriptor that raises a VersionedError if the set or del methods are called
+on a versioned object.  These are rwproperties (see rwproperty.txt; imported
+from another project.)
+
+'makeProperty' generates a version-aware descriptor that does a simple
+get/set but raises VersionedError if the set is attempted on a versioned
+object.
+
+    >>> class BiggerDemo(Demo):
+    ...     counter = 0
+    ...     @versioning.method
+    ...     def increase(self):
+    ...         self.counter += 1
+    ...     _complex = 1
+    ...     @property
+    ...     def complex_property(self):
+    ...         return str(self._complex)
+    ...     @versioning.setproperty
+    ...     def complex_property(self, value):
+    ...         self._complex = value * 2
+    ...     versioning.makeProperty('simple_property')
+    ...
+    >>> d = BiggerDemo()
+    >>> d.counter
+    0
+    >>> d.complex_property
+    '1'
+    >>> d.simple_property # None
+    >>> d.increase()
+    >>> d.counter
+    1
+    >>> d.complex_property = 4
+    >>> d.complex_property
+    '8'
+    >>> d.simple_property = 'hi'
+    >>> d.simple_property
+    'hi'
+    >>> interfaces.IVersioning(d)._z_versioned
+    False
+    >>> interfaces.IVersioning(d)._z_version()
+    >>> interfaces.IVersioning(d)._z_versioned
+    True
+    >>> d.counter
+    1
+    >>> d.increase()
+    Traceback (most recent call last):
+    ...
+    VersionedError
+    >>> d.counter
+    1
+    >>> d.complex_property
+    '8'
+    >>> d.complex_property = 10
+    Traceback (most recent call last):
+    ...
+    VersionedError
+    >>> d.complex_property
+    '8'
+    >>> d.simple_property
+    'hi'
+    >>> d.simple_property = 'bye'
+    Traceback (most recent call last):
+    ...
+    VersionedError
+    >>> d.simple_property
+    'hi'
+
+Finally, it contains a subscriber that uses the zope.locking code to freeze
+objects when they are versioned.  When combined with packages such as
+zc.tokenpolicy, objects that are not version-aware can still effectively
+be governed by the versioned status for user interaction through a security
+proxy.
+
+    >>> import zope.locking.utility
+    >>> import zope.app.keyreference.interfaces
+    >>> import zope.locking.interfaces
+    >>> util = zope.locking.utility.TokenUtility()
+    >>> component.provideUtility(
+    ...     util, provides=zope.locking.interfaces.ITokenUtility)
+    >>> class DemoKeyReference(object):
+    ...     component.adapts(Demo)
+    ...     interface.implements(
+    ...         zope.app.keyreference.interfaces.IKeyReference)
+    ...     _class_counter = 0
+    ...     def __init__(self, context):
+    ...         self.context = context
+    ...         class_ = type(self)
+    ...         self._id = getattr(context, '__demo_key_reference__', None)
+    ...         if self._id is None:
+    ...             self._id = class_._class_counter
+    ...             context.__demo_key_reference__ = self._id
+    ...             class_._class_counter += 1
+    ...     key_type_id = 'zc.copyversion.README.DemoKeyReference'
+    ...     def __call__(self):
+    ...         return self.context
+    ...     def __hash__(self):
+    ...         return (self.key_type_id, self._id)
+    ...     def __cmp__(self, other):
+    ...         if self.key_type_id == other.key_type_id:
+    ...             return cmp(self._id, other._id)
+    ...         return cmp(self.key_type_id, other.key_type_id)
+    ...
+    >>> component.provideAdapter(DemoKeyReference)
+    >>> from zc.copyversion import subscribers
+    >>> component.provideHandler(subscribers.freezer)
+    >>> d = Demo()
+    >>> util.get(d) # None
+    >>> interfaces.IVersioning(d)._z_version()
+    >>> zope.locking.interfaces.IFreeze.providedBy(util.get(d))
+    True
+
+Our copyversion story still needs other components.
+
+- copy subscribers account for resetting objectlog and comments across copies.
+  These belong in the respective packages.
+
+- UI for making individual objects into frozen versions.  One approach might
+  belong in this package at some point.


Property changes on: zc.copyversion/trunk/src/zc/copyversion/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.copyversion/trunk/src/zc/copyversion/__init__.py
===================================================================

Added: zc.copyversion/trunk/src/zc/copyversion/configure.zcml
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/configure.zcml	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/configure.zcml	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,26 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    i18n_domain="zc.copyversion">
+
+  <class class=".versioning.VersioningAdapter">
+    <require permission="zope.View"
+             attributes="_z_versioned _z_version_timestamp" />
+    <require permission="zope.ManageContent" attributes="_z_version" />
+  </class>
+
+  <adapter factory=".versioning.VersioningAdapter"
+      trusted="1" />
+
+  <adapter factory=".copier.versiondata_copyfactory" />
+
+  <adapter factory=".copier.location_copyfactory" />
+
+  <!-- this doesn't work here, because of the mystery of the override machinery.
+       Add its contents yourself to a an overrides file specified in
+       site.zcml or a similar high level.
+  <includeOverrides file="overrides.zcml"/>
+  -->
+
+  <subscriber handler=".subscribers.freezer" />
+
+</configure>

Added: zc.copyversion/trunk/src/zc/copyversion/copier.py
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/copier.py	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/copier.py	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,133 @@
+# 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
+
+import zc.copyversion.interfaces
+
+# this can be used to rip off old versioning data and put new values in place
+ at zope.component.adapter(zc.copyversion.interfaces.IVersioningData)
+ at zope.interface.implementer(zc.copyversion.interfaces.ICopyHook)
+def versiondata_copyfactory(obj):
+    def factory(location, register):
+        return None
+    return factory
+
+# this is the part that replaces zope.location.pickling
+ at zope.component.adapter(zope.location.interfaces.ILocation)
+ at zope.interface.implementer(zc.copyversion.interfaces.ICopyHook)
+def location_copyfactory(obj):
+    def factory(location, register):
+        if not zope.location.location.inside(obj, location):
+            return obj
+        raise zc.copyversion.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 = zc.copyversion.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 zc.copyversion.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

Added: zc.copyversion/trunk/src/zc/copyversion/copier.txt
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/copier.txt	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/copier.txt	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,393 @@
+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 two adapters for use with the new copy
+function: one that gives the same ILocation behavior as locationCopy, and
+another that gives new zc.copyversion-specific behavior.
+
+The first 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
+some simpler uses.  A fairly simple hook is to support the use case that
+copies of versioned objects should not be versioned.  We do this by storing
+versioning on a special object that is converted to None when it is copied.
+
+Open up copier.py and look at the versiondata_copyfactory function.  It
+adapts zc.copyversion.interfaces.IVersioningData and implements
+zc.copyversion.interfaces.ICopyHook.  It returns a function that always returns
+None, no matter what the main location being copied is.
+
+Let's register it and look at an example.  Here's what happens if we copy a
+versioned object without the adapter.
+
+    >>> import zc.copyversion.versioning
+    >>> original = zc.copyversion.versioning.Versioning()
+    >>> original._z_version()
+    >>> original._z_versioned
+    True
+    >>> import zc.copyversion.copier
+    >>> copy = zc.copyversion.copier.copy(original)
+    >>> copy is original
+    False
+    >>> copy._z_versioned
+    True
+
+Generally, we want copies to not be versioned.  Let's register the adapter and
+try that again.
+
+    >>> import zope.component
+    >>> zope.component.provideAdapter(
+    ...     zc.copyversion.copier.versiondata_copyfactory)
+    >>> copy2 = zc.copyversion.copier.copy(original)
+    >>> copy2 is original
+    False
+    >>> copy2._z_versioned
+    False
+
+Much better.
+
+The ILocation adapter is just a tiny bit more complicated.  Look again in
+copier.py, now 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.copyversion.copier.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.copyversion.copier.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.copyversion.copier.location_copyfactory)
+    >>> c3 = zc.copyversion.copier.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.copyversion.copier.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.copyversion.copier.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.copyversion.interfaces
+    >>> @zope.component.adapter(Subobject)
+    ... @zope.interface.implementer(zc.copyversion.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.copyversion.copier.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.copyversion.copier.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.copyversion.copier.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.copyversion.copier.ObjectCopier(cO)
+    >>> copier2.copyableTo(container)
+    False
+    >>> container.x = 1
+    >>> copier2.copyableTo(container)
+    True
\ No newline at end of file


Property changes on: zc.copyversion/trunk/src/zc/copyversion/copier.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.copyversion/trunk/src/zc/copyversion/interfaces.py
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/interfaces.py	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/interfaces.py	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,56 @@
+from zope import interface
+import zope.component.interfaces
+
+class VersionedError(Exception):
+    """The object is already versioned and cannot be changed."""
+
+class VersioningError(Exception):
+    """The object is unable to be versioned at this time."""
+
+class IObjectVersionedEvent(zope.component.interfaces.IObjectEvent):
+    """The object is being versioned"""
+
+class ObjectVersionedEvent(zope.component.interfaces.ObjectEvent):
+    """Object was versioned"""
+
+    interface.implements(IObjectVersionedEvent)
+
+class IVersionable(interface.Interface):
+    """Marker interface specifying that it is desirable to adapt the object to
+    IVersioning"""
+
+class IVersioning(IVersionable):
+    _z_versioned = interface.Attribute(
+        """Boolean, whether the object is versioned.  Readonly""")
+
+    _z_version_timestamp = interface.Attribute(
+        "datetime.datetime in pytz.utc of when versioned, or None.  Readonly.")
+
+    def _z_version():
+        """sets _z_versioned to True and fires ObjectVersioned event.
+        raises VersionedError if _z_versioned is already True."""
+
+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.
+        """
+
+class IVersioningData(interface.Interface):
+    """An object used to store version data for another object.  Useful for
+    the copy hook.  Only of internal interest."""
+
+    _z_version_timestamp = interface.Attribute(
+        "datetime.datetime in pytz.utc of when versioned, or None.")
\ No newline at end of file

Added: zc.copyversion/trunk/src/zc/copyversion/overrides.zcml
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/overrides.zcml	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/overrides.zcml	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,11 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    >
+
+  <adapter
+      factory=".copier.ObjectCopier"
+      permission="zope.ManageContent"
+      trusted="y"
+      />
+
+</configure>
\ No newline at end of file

Added: zc.copyversion/trunk/src/zc/copyversion/rwproperty.py
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/rwproperty.py	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/rwproperty.py	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,75 @@
+# Read & write properties
+#
+# Copyright (c) 2006 by Philipp "philiKON" von Weitershausen
+#                       philikon at philikon.de
+#
+# Freely distributable under the terms of the Zope Public License, v2.1.
+#
+# See rwproperty.txt for detailed explanations
+#
+import sys
+
+__all__ = ['getproperty', 'setproperty', 'delproperty']
+
+class rwproperty(object):
+
+    def __new__(cls, func):
+        name = func.__name__
+
+        # ugly, but common hack
+        frame = sys._getframe(1)
+        locals = frame.f_locals
+
+        if name not in locals:
+            return cls.createProperty(func)
+
+        oldprop = locals[name]
+        if isinstance(oldprop, property):
+            return cls.enhanceProperty(oldprop, func)
+
+        raise TypeError("read & write properties cannot be mixed with "
+                        "other attributes except regular property objects.")
+
+    # this might not be particularly elegant, but it's easy on the eyes
+
+    @staticmethod
+    def createProperty(func):
+        raise NotImplementedError
+
+    @staticmethod
+    def enhanceProperty(oldprop, func):
+        raise NotImplementedError
+
+class getproperty(rwproperty):
+
+    @staticmethod
+    def createProperty(func):
+        return property(func)
+
+    @staticmethod
+    def enhanceProperty(oldprop, func):
+        return property(func, oldprop.fset, oldprop.fdel)
+
+class setproperty(rwproperty):
+
+    @staticmethod
+    def createProperty(func):
+        return property(None, func)
+
+    @staticmethod
+    def enhanceProperty(oldprop, func):
+        return property(oldprop.fget, func, oldprop.fdel)
+
+class delproperty(rwproperty):
+
+    @staticmethod
+    def createProperty(func):
+        return property(None, None, func)
+
+    @staticmethod
+    def enhanceProperty(oldprop, func):
+        return property(oldprop.fget, oldprop.fset, func)
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testfile('rwproperty.txt')

Added: zc.copyversion/trunk/src/zc/copyversion/rwproperty.txt
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/rwproperty.txt	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/rwproperty.txt	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,140 @@
+Read & write properties
+========================
+
+:Author:   Philipp von Weitershausen
+:Email:    philikon at philikon.de
+:License:  Zope Public License, v2.1
+
+Motivation
+----------
+
+Using method decorators and descriptors like ``property``, we can
+easily create computed attributes:
+
+  >>> class JamesBrown(object):
+  ...     @property
+  ...     def feel(self):
+  ...         return self._feel
+
+An attribute like this cannot be written, though.  You would have to
+do something like this:
+
+  >>> class JamesBrown(object):
+  ...     def _getFeel(self):
+  ...         return self._feel
+  ...     def _setFeel(self, feel):
+  ...         self._feel = feel
+  ...     feel = property(_getFeel, _setFeel)
+
+The problem with this approach is that it leaves the getter and setter
+sitting around in the class namespace.  It also lacks the compact
+spelling of a decorator solution.  To cope with that, some people like
+to write:
+
+  >>> class JamesBrown(object):
+  ...     @apply
+  ...     def feel():
+  ...         def get(self):
+  ...             return self._feel
+  ...         def set(self, feel):
+  ...             self._feel = feel
+  ...         return property(get, set)
+
+This spelling feels rather cumbersome, apart from the fact that
+``apply`` is `going to go away`_ in Python 3000.
+
+.. _going to go away: http://www.python.org/peps/pep-3000.html#id24
+
+
+Goal
+----
+
+There should be a way to declare a read & write property and still use
+the compact and easy decorator spelling.  The read & write properties
+should be as easy to use as the read-only property.  We explicitly
+don't want that immediately called function that really just helps us
+name the attribute and create a local scope for the getter and setter.
+
+
+Read & write property
+---------------------
+
+Read & write properties work like regular properties.  You simply
+define a method and then apply a decorator, except that you now don't
+use ``@property`` but ``@getproperty`` to mark the getter and
+``@setproperty`` to mark the setter:
+
+  >>> from rwproperty import getproperty, setproperty
+  >>> class JamesBrown(object):
+  ...     @getproperty
+  ...     def feel(self):
+  ...         return self._feel
+  ...     @setproperty
+  ...     def feel(self, feel):
+  ...         self._feel = feel
+
+  >>> i = JamesBrown()
+  >>> i.feel
+  Traceback (most recent call last):
+  ...
+  AttributeError: 'JamesBrown' object has no attribute '_feel'
+
+  >>> i.feel = "good"
+  >>> i.feel
+  'good'
+
+The order in which getters and setters are declared doesn't matter:
+
+  >>> from rwproperty import getproperty, setproperty
+  >>> class JamesBrown(object):
+  ...     @setproperty
+  ...     def feel(self, feel):
+  ...         self._feel = feel
+  ...     @getproperty
+  ...     def feel(self):
+  ...         return self._feel
+
+  >>> i = JamesBrown()
+  >>> i.feel = "good"
+  >>> i.feel
+  'good'
+
+Of course, deleters are also possible:
+
+  >>> from rwproperty import delproperty
+  >>> class JamesBrown(object):
+  ...     @setproperty
+  ...     def feel(self, feel):
+  ...         self._feel = feel
+  ...     @getproperty
+  ...     def feel(self):
+  ...         return self._feel
+  ...     @delproperty
+  ...     def feel(self):
+  ...         del self._feel
+
+  >>> i = JamesBrown()
+  >>> i.feel = "good"
+  >>> del i.feel
+  >>> i.feel
+  Traceback (most recent call last):
+  ...
+  AttributeError: 'JamesBrown' object has no attribute '_feel'
+
+
+Edge cases
+----------
+
+There might be a case where you're using a flavour of read & write
+properties and already have a non-property attribute of the same name
+defined:
+
+  >>> class JamesBrown(object):
+  ...     feel = "good"
+  ...     @getproperty
+  ...     def feel(self):
+  ...         return "so good"
+  ...
+  Traceback (most recent call last):
+  ...
+  TypeError: read & write properties cannot be mixed with other attributes except regular property objects.


Property changes on: zc.copyversion/trunk/src/zc/copyversion/rwproperty.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.copyversion/trunk/src/zc/copyversion/subscribers.py
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/subscribers.py	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/subscribers.py	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,16 @@
+from zope import component
+from zc.copyversion import interfaces
+import zope.locking.interfaces
+import zope.locking.tokens
+
+ at component.adapter(interfaces.IObjectVersionedEvent)
+def freezer(ev):
+    util = component.getUtility(zope.locking.interfaces.ITokenUtility)
+    obj = ev.object
+    token = util.get(obj)
+    if token is not None:
+        if zope.locking.interfaces.IEndable.providedBy(token):
+            token.end()
+        else:
+            return
+    util.register(zope.locking.tokens.Freeze(obj))

Added: zc.copyversion/trunk/src/zc/copyversion/tests.py
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/tests.py	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/tests.py	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,45 @@
+import unittest
+from zope.app.testing import placelesssetup
+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 setUp(test):
+    placelesssetup.setUp(test)
+    events = test.globs['events'] = []
+    import zope.event
+    zope.event.subscribers.append(events.append)
+
+def tearDown(test):
+    placelesssetup.tearDown(test)
+    events = test.globs.pop('events')
+    import zope.event
+    assert zope.event.subscribers.pop().__self__ is events
+    del events[:] # being paranoid
+
+def copierSetUp(test):
+    zope.testing.module.setUp(test, 'zc.copyversion.copier_txt')
+    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=setUp, tearDown=tearDown),
+        doctest.DocFileSuite(
+            'copier.txt',
+            setUp=copierSetUp,
+            tearDown=copierTearDown),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: zc.copyversion/trunk/src/zc/copyversion/versioning.py
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/versioning.py	2006-08-15 20:33:11 UTC (rev 69535)
+++ zc.copyversion/trunk/src/zc/copyversion/versioning.py	2006-08-15 20:50:21 UTC (rev 69536)
@@ -0,0 +1,106 @@
+import sys
+import datetime
+import pytz
+import persistent
+from zope import interface, event, component
+from zc.copyversion import interfaces
+import zope.annotation.interfaces
+from zope.cachedescriptors.property import Lazy
+# import rwproperty
+from zc.copyversion import rwproperty
+
+def method(f):
+    def wrapper(self, *args, **kwargs):
+        try: # micro-optimize for the "yes, I'm already versioned" story
+            versioned = self._z_versioned
+        except AttributeError:
+            versioned = interfaces.IVersioning(self)._z_versioned
+        if versioned:
+            raise interfaces.VersionedError
+        return f(self, *args, **kwargs)
+    return wrapper
+
+class setproperty(rwproperty.rwproperty):
+
+    @staticmethod
+    def createProperty(func):
+        return property(None, method(func))
+
+    @staticmethod
+    def enhanceProperty(oldprop, func):
+        return property(oldprop.fget, method(func), oldprop.fdel)
+
+class delproperty(rwproperty.rwproperty):
+
+    @staticmethod
+    def createProperty(func):
+        return property(None, None, method(func))
+
+    @staticmethod
+    def enhanceProperty(oldprop, func):
+        return property(oldprop.fget, oldprop.fset, method(func))
+
+def makeProperty(name, default=None):
+    protected = '_z_%s__' % name
+    sys._getframe(1).f_locals[name] = property(
+        lambda self: getattr(self, protected, default),
+        method(lambda self, value: setattr(self, protected, value)))
+
+class VersioningData(persistent.Persistent):
+    interface.implements(interfaces.IVersioningData)
+    def __init__(self):
+        self._z__version_timestamp = datetime.datetime.now(pytz.utc)
+
+    @property
+    def _z_version_timestamp(self):
+        return self._z__version_timestamp
+    
+
+class Versioning(object):
+    interface.implements(interfaces.IVersioning)
+
+    _z__versioning_data = None
+
+    @property
+    def _z_versioned(self):
+        return self._z__versioning_data is not None
+
+    @property
+    def _z_version_timestamp(self):
+        res = self._z__versioning_data
+        if res is not None:
+            return res._z_version_timestamp
+
+    @method
+    def _z_version(self):
+        self._z__versioning_data = VersioningData()
+        event.notify(interfaces.ObjectVersionedEvent(self))
+
+KEY = "zc.copyversion._z_version_timestamp"
+
+class VersioningAdapter(object):
+    interface.implements(interfaces.IVersioning)
+    component.adapts(zope.annotation.interfaces.IAnnotatable)
+
+    def __init__(self, context):
+        self.context = context
+
+    @Lazy
+    def annotations(self):
+        return zope.annotation.interfaces.IAnnotations(self.context)
+
+    @property
+    def _z_versioned(self):
+        return self.annotations.get(KEY) is not None
+
+    @property
+    def _z_version_timestamp(self):
+        res = self.annotations.get(KEY)
+        if res is not None:
+            return res._z_version_timestamp
+
+    @method
+    def _z_version(self):
+        self.annotations[KEY] = VersioningData()
+        event.notify(interfaces.ObjectVersionedEvent(self.context))
+



More information about the Checkins mailing list