[Checkins] SVN: zc.freeze/trunk/src/zc/freeze/ the 'version' side
of copyversion
Gary Poster
gary at zope.com
Wed Aug 23 17:51:35 EDT 2006
Log message for revision 69739:
the 'version' side of copyversion
Changed:
A zc.freeze/trunk/src/zc/freeze/README.txt
A zc.freeze/trunk/src/zc/freeze/__init__.py
A zc.freeze/trunk/src/zc/freeze/configure.zcml
A zc.freeze/trunk/src/zc/freeze/copier.py
A zc.freeze/trunk/src/zc/freeze/copier.txt
A zc.freeze/trunk/src/zc/freeze/interfaces.py
A zc.freeze/trunk/src/zc/freeze/rwproperty.py
A zc.freeze/trunk/src/zc/freeze/subscribers.py
A zc.freeze/trunk/src/zc/freeze/subscribers.txt
A zc.freeze/trunk/src/zc/freeze/tests.py
-=-
Copied: zc.freeze/trunk/src/zc/freeze/README.txt (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/README.txt)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/README.txt 2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.freeze/trunk/src/zc/freeze/README.txt 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,197 @@
+========
+Freezing
+========
+
+This package implements basic functionality for freezing objects:
+spellings to query whether an object can be frozen, to query whether it
+has been frozen, and to actually freeze 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 freezable, client code should ask if it
+provides zc.freeze.interfaces.IFreezable.
+
+Site configurations or code that declares that an object is IFreezable
+is assuring that the object provides or can be adaptable to
+zc.freeze.interfaces.IFreezing. This interface has only three elements:
+_z_frozen is a readonly boolean that returns whether the object has been
+versioned; _z_freeze_datetime is a readonly datetime in pytz.utc
+specifying when the object was frozen (or None, if it is not yet
+frozen); and _z_freeze is a method that actually freezes the object. If
+the object is already frozen, it raises
+zc.freeze.interfaces.FrozenError. If the object is not in a state to be
+frozen, it may raise zc.freeze.interfaces.FreezeError. If the freezing
+may succeed, the method should send a
+zc.freeze.interfaces.IObjectFrozenEvent (such as
+zc.freeze.interfaces.ObjectFrozenEvent).
+
+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
+freezing 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.freeze API. This
+package provides much simpler capabilities.
+
+Conveniences
+============
+
+The package does provide two default implementations of IFreezing, and a few
+conveniences.
+
+One IFreezing implementation is for objects that are directly aware of this
+API (as opposed to having the functionality assembled from adapters and other
+components).
+
+ >>> import zc.freeze
+ >>> v = zc.freeze.Freezing()
+ >>> from zc.freeze import interfaces
+ >>> from zope.interface.verify import verifyObject
+ >>> verifyObject(interfaces.IFreezing, v)
+ True
+ >>> verifyObject(interfaces.IFreezable, v)
+ True
+ >>> v._z_frozen
+ False
+ >>> v._z_frozen = True
+ Traceback (most recent call last):
+ ...
+ AttributeError: can't set attribute
+ >>> import pytz
+ >>> import datetime
+ >>> before = datetime.datetime.now(pytz.utc)
+ >>> v._z_freeze()
+ >>> before <= v._z_freeze_timestamp <= datetime.datetime.now(pytz.utc)
+ True
+ >>> v._z_frozen
+ True
+ >>> interfaces.IObjectFrozenEvent.providedBy(events[-1])
+ True
+ >>> events[-1].object is v
+ True
+ >>> v._z_freeze()
+ Traceback (most recent call last):
+ ...
+ FrozenError
+
+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(zc.freeze.FreezingAdapter)
+ >>> d = Demo()
+ >>> verifyObject(interfaces.IFreezing, interfaces.IFreezing(d))
+ True
+ >>> verifyObject(interfaces.IFreezable, interfaces.IFreezing(d))
+ True
+ >>> interfaces.IFreezing(d)._z_frozen
+ False
+ >>> interfaces.IFreezing(d)._z_frozen = True
+ Traceback (most recent call last):
+ ...
+ AttributeError: can't set attribute
+ >>> before = datetime.datetime.now(pytz.utc)
+ >>> interfaces.IFreezing(d)._z_freeze()
+ >>> (before <= interfaces.IFreezing(d)._z_freeze_timestamp <=
+ ... datetime.datetime.now(pytz.utc))
+ True
+ >>> interfaces.IFreezing(d)._z_frozen
+ True
+ >>> interfaces.IObjectFrozenEvent.providedBy(events[-1])
+ True
+ >>> events[-1].object is d
+ True
+ >>> interfaces.IFreezing(d)._z_freeze()
+ Traceback (most recent call last):
+ ...
+ FrozenError
+
+The zc.freeze module also contains three helpers for writing properties and
+methods that are freeze-aware.
+
+A 'method' function can generate a freeze-aware method that raises a
+FrozenError if the object has been frozen.
+
+'setproperty' and 'delproperty' functions can generate a freeze-aware
+descriptor that raises a FrozenError if the set or del methods are called
+on a frozen object. These are rwproperties (see rwproperty.txt; imported
+from another project.)
+
+'makeProperty' generates a freeze-aware descriptor that does a simple
+get/set but raises FrozenError if the set is attempted on a frozen
+object.
+
+ >>> class BiggerDemo(Demo):
+ ... counter = 0
+ ... @zc.freeze.method
+ ... def increase(self):
+ ... self.counter += 1
+ ... _complex = 1
+ ... @property
+ ... def complex_property(self):
+ ... return str(self._complex)
+ ... @zc.freeze.setproperty
+ ... def complex_property(self, value):
+ ... self._complex = value * 2
+ ... zc.freeze.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.IFreezing(d)._z_frozen
+ False
+ >>> interfaces.IFreezing(d)._z_freeze()
+ >>> interfaces.IFreezing(d)._z_frozen
+ True
+ >>> d.counter
+ 1
+ >>> d.increase()
+ Traceback (most recent call last):
+ ...
+ FrozenError
+ >>> d.counter
+ 1
+ >>> d.complex_property
+ '8'
+ >>> d.complex_property = 10
+ Traceback (most recent call last):
+ ...
+ FrozenError
+ >>> d.complex_property
+ '8'
+ >>> d.simple_property
+ 'hi'
+ >>> d.simple_property = 'bye'
+ Traceback (most recent call last):
+ ...
+ FrozenError
+ >>> d.simple_property
+ 'hi'
Copied: zc.freeze/trunk/src/zc/freeze/__init__.py (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/versioning.py)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/versioning.py 2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.freeze/trunk/src/zc/freeze/__init__.py 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,108 @@
+import sys
+import datetime
+import pytz
+import persistent
+from zope import interface, event, component
+import zope.annotation.interfaces
+from zope.cachedescriptors.property import Lazy
+
+from zc.freeze import interfaces
+# import rwproperty
+from zc.freeze import rwproperty
+
+def method(f):
+ def wrapper(self, *args, **kwargs):
+ try: # micro-optimize for the "yes, I'm already versioned" story
+ frozen = self._z_frozen
+ except AttributeError:
+ frozen = interfaces.IFreezing(self)._z_frozen
+ if frozen:
+ raise interfaces.FrozenError
+ 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 Data(persistent.Persistent):
+ interface.implements(interfaces.IData)
+ def __init__(self):
+ self._z__freeze_timestamp = datetime.datetime.now(pytz.utc)
+
+ @property
+ def _z_freeze_timestamp(self):
+ return self._z__freeze_timestamp
+
+
+class Freezing(object):
+ interface.implements(interfaces.IFreezing)
+
+ _z__freezing_data = None
+
+ @property
+ def _z_frozen(self):
+ return self._z__freezing_data is not None
+
+ @property
+ def _z_freeze_timestamp(self):
+ res = self._z__freezing_data
+ if res is not None:
+ return res._z_freeze_timestamp
+
+ @method
+ def _z_freeze(self):
+ self._z__freezing_data = Data()
+ event.notify(interfaces.ObjectFrozenEvent(self))
+
+KEY = "zc.freeze._z_freeze_timestamp"
+
+class FreezingAdapter(object):
+ interface.implements(interfaces.IFreezing)
+ 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_frozen(self):
+ return self.annotations.get(KEY) is not None
+
+ @property
+ def _z_freeze_timestamp(self):
+ res = self.annotations.get(KEY)
+ if res is not None:
+ return res._z_freeze_timestamp
+
+ @method
+ def _z_freeze(self):
+ self.annotations[KEY] = Data()
+ event.notify(interfaces.ObjectFrozenEvent(self.context))
+
Copied: zc.freeze/trunk/src/zc/freeze/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.freeze/trunk/src/zc/freeze/configure.zcml 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,17 @@
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ i18n_domain="zc.freeze">
+
+ <class class=".FreezingAdapter">
+ <require permission="zope.View"
+ attributes="_z_frozen _z_freeze_timestamp" />
+ <require permission="zope.ManageContent" attributes="_z_freeze" />
+ </class>
+
+ <adapter factory=".FreezingAdapter" trusted="1" />
+
+ <adapter factory=".copier.data_copyfactory" />
+
+ <subscriber handler=".subscribers.freezer" />
+
+</configure>
Copied: zc.freeze/trunk/src/zc/freeze/copier.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.freeze/trunk/src/zc/freeze/copier.py 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,14 @@
+
+import zope.component
+import zope.interface
+
+import zc.copy.interfaces
+import zc.freeze.interfaces
+
+# this can be used to rip off old versioning data and put new values in place
+ at zope.component.adapter(zc.freeze.interfaces.IData)
+ at zope.interface.implementer(zc.copy.interfaces.ICopyHook)
+def data_copyfactory(obj):
+ def factory(location, register):
+ return None
+ return factory
Copied: zc.freeze/trunk/src/zc/freeze/copier.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.freeze/trunk/src/zc/freeze/copier.txt 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,40 @@
+The copier module uses zc.copy. See the README.txt for that module for
+explanations of how this hook fits in to the larger picture.
+
+Our use case is 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 data_copyfactory function. It
+adapts zc.freeze.interfaces.IData and implements
+zc.copy.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.freeze
+ >>> original = zc.freeze.Freezing()
+ >>> original._z_freeze()
+ >>> original._z_frozen
+ True
+ >>> import zc.copy
+ >>> copy = zc.copy.copy(original)
+ >>> copy is original
+ False
+ >>> copy._z_frozen
+ True
+
+Again, in the common case, we don't want copies to be versioned. Let's
+register the adapter and try that again.
+
+ >>> import zope.component
+ >>> import zc.freeze.copier
+ >>> zope.component.provideAdapter(zc.freeze.copier.data_copyfactory)
+ >>> copy2 = zc.copy.copy(original)
+ >>> copy2 is original
+ False
+ >>> copy2._z_frozen
+ False
+
+Much better.
Copied: zc.freeze/trunk/src/zc/freeze/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.freeze/trunk/src/zc/freeze/interfaces.py 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,43 @@
+from zope import interface
+import zope.component.interfaces
+
+class FrozenError(Exception):
+ """The object is already frozen and cannot be changed."""
+
+class FreezeError(Exception):
+ """The object is unable to be frozen at this time."""
+
+class IObjectFrozenEvent(zope.component.interfaces.IObjectEvent):
+ """The object is being frozen"""
+
+class ObjectFrozenEvent(zope.component.interfaces.ObjectEvent):
+ """Object was frozen"""
+
+ interface.implements(IObjectFrozenEvent)
+
+class IFreezable(interface.Interface):
+ """Marker interface specifying that it is desirable to adapt the object to
+ IFreezing"""
+
+class IFreezing(IFreezable):
+ _z_frozen = interface.Attribute(
+ """Boolean, whether the object is frozen. Readonly""")
+
+ _z_freeze_timestamp = interface.Attribute(
+ "datetime.datetime in pytz.utc of when frozen, or None. Readonly.")
+
+ def _z_freeze():
+ """sets _z_frozen to True, sets _z_freeze_timestamp, and fires
+ ObjectFrozenEvent. Raises FrozenError if _z_frozen is already True."""
+
+class ITokenEnforced(interface.Interface):
+ """A marker interface indicating that the instance wants to have its
+ freezing enforced by zope.locking tokens (see the subscribers module).
+ """
+
+class IData(interface.Interface):
+ """An object used to store freezing data for another object. Useful for
+ the copy hook. Only of internal interest."""
+
+ _z_freeze_timestamp = interface.Attribute(
+ "datetime.datetime in pytz.utc of when frozen, or None.")
Copied: zc.freeze/trunk/src/zc/freeze/rwproperty.py (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/rwproperty.py)
Copied: zc.freeze/trunk/src/zc/freeze/subscribers.py (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/subscribers.py)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/subscribers.py 2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.freeze/trunk/src/zc/freeze/subscribers.py 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,16 @@
+from zope import component
+import zope.locking.interfaces
+import zope.locking.tokens
+
+from zc.freeze import interfaces
+
+ at component.adapter(interfaces.ITokenEnforced, interfaces.IObjectFrozenEvent)
+def freezer(obj, ev):
+ util = component.getUtility(zope.locking.interfaces.ITokenUtility)
+ 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))
Copied: zc.freeze/trunk/src/zc/freeze/subscribers.txt (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/subscribers.txt)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/subscribers.txt 2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.freeze/trunk/src/zc/freeze/subscribers.txt 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,87 @@
+The subscribers module contains a subscriber for "enforcing" freezing
+using zope.locking tokens. This, of course, assumes that zope.locking
+tokens are configured to themselves be enforced somehow, using
+approaches such as those in zc.tokenpolicy. By default, this `freezer`
+subscriber is registered only for objects that provide
+zc.freeze.interfaces.ITokenEnforced. Imagine a demo object that
+implements ITokenEnforced, with a token utility already in place [#setup]_.
+
+ >>> import zc.freeze.subscribers
+ >>> import zope.component
+ >>> zope.component.provideHandler(zc.freeze.subscribers.freezer)
+ >>> demo = Demo()
+ >>> import zc.freeze.interfaces
+ >>> import zope.interface
+ >>> zope.interface.directlyProvides(
+ ... demo, zc.freeze.interfaces.ITokenEnforced)
+ >>> zc.freeze.interfaces.ITokenEnforced.providedBy(demo)
+ True
+ >>> util.get(demo) is None
+ True
+ >>> demo._z_frozen
+ False
+ >>> demo._z_freeze()
+ >>> demo._z_frozen
+ True
+ >>> import zope.locking.interfaces
+ >>> zope.locking.interfaces.IFreeze.providedBy(util.get(demo))
+ True
+
+If an object does not provide ITokenEnforced, the subscriber will not
+fire, given a default registration.
+
+ >>> demo2 = Demo()
+ >>> zc.freeze.interfaces.ITokenEnforced.providedBy(demo2)
+ False
+ >>> demo2._z_freeze()
+ >>> demo2._z_frozen
+ True
+ >>> util.get(demo2) is None
+ True
+
+=========
+Footnotes
+=========
+
+.. [#setup] This sets up a zope.locking token utility as a global,
+ non-persistent utility. This is completely useless and even dangerous
+ in real use, but is fine for this test. This code also creates a Demo
+ class that provides IVersioning and can be used in our non-persistent
+ setup.
+
+ >>> from zope import interface, component
+ >>> from zope.locking import utility, interfaces
+ >>> util = utility.TokenUtility()
+ >>> component.provideUtility(util, provides=interfaces.ITokenUtility)
+ >>> import zope.app.keyreference.interfaces
+ >>> class IDemo(interface.Interface):
+ ... """a demonstration interface for a demonstration class"""
+ ...
+ >>> import zc.freeze
+ >>> class Demo(zc.freeze.Freezing):
+ ... interface.implements(IDemo)
+ ...
+ >>> class DemoKeyReference(object):
+ ... component.adapts(IDemo)
+ ... _class_counter = 0
+ ... interface.implements(
+ ... zope.app.keyreference.interfaces.IKeyReference)
+ ... 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.freeze.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)
Copied: zc.freeze/trunk/src/zc/freeze/tests.py (from rev 69734, zc.copyversion/trunk/src/zc/copyversion/tests.py)
===================================================================
--- zc.copyversion/trunk/src/zc/copyversion/tests.py 2006-08-23 19:03:56 UTC (rev 69734)
+++ zc.freeze/trunk/src/zc/freeze/tests.py 2006-08-23 21:51:35 UTC (rev 69739)
@@ -0,0 +1,57 @@
+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 subscribersSetUp(test):
+ placelesssetup.setUp(test)
+ zope.testing.module.setUp(test, 'zc.freeze.subscribers_txt')
+
+def subscribersTearDown(test):
+ zope.testing.module.tearDown(test)
+ placelesssetup.tearDown(test)
+
+def copierSetUp(test):
+ zope.testing.module.setUp(test, 'zc.freeze.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(
+ 'subscribers.txt',
+ setUp=subscribersSetUp, tearDown=subscribersTearDown,
+ optionflags=doctest.INTERPRET_FOOTNOTES),
+ doctest.DocFileSuite(
+ 'copier.txt',
+ setUp=copierSetUp,
+ tearDown=copierTearDown),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
More information about the Checkins
mailing list