[Zope-CVS] CVS: Products/Transience - Transience.py:1.9 TransienceInterfaces.py:1.8 __init__.py:1.4

Chris McDonough chrism@zope.com
Wed, 7 Nov 2001 01:47:07 -0500


Update of /cvs-repository/Products/Transience
In directory cvs.zope.org:/tmp/cvs-serv27930

Modified Files:
	Transience.py TransienceInterfaces.py __init__.py 
Log Message:
- Permission name changes.

- Removed advanced (import/export) view - superfluous.

- new() and new_or_existing() methods now accept a wrap_with argument.
  If wrap_with is non-None, the data object returned will be wrapped
  in (via __of__) with the object passed into the wrap_with argument.
  This will facilitate the wrapping of data objects in the calling
  session data manager object.

- Transient objects no longer keep a reference to their container.
  Instead, containers use the isValid method of objects to determine
  whether an object is valid.  An object no longer deletes itself from
  its container and relies solely on this mechanism.

- Callback handling now logs on failure.

- Various deletions of commented material.

- getToken of data objects returns the key with which they were entered
  into their container, while getId returns a uniform unique id.  getName
  is now aliased to getId.

- data objects now no longer aq-unwrap things stored in them.  This
  is a security change.

- repr method of data object now returns simpler structure.

- removed stx workarounds from interfaces file (we should just fix the
  help system instead of doing stuff like  _, etc.)

- extended some descriptions of interfaces.

- extended and edited other docs.

TODO:

- fix tests to run under testrunner.

- "ring" must change to become more conflict resistant.
  (new data structure and algorithm needs to be put in)

-  need new icon for data container.

- need out of memory protection in container.


=== Products/Transience/Transience.py 1.8 => 1.9 ===
 from AccessControl import ClassSecurityInfo
 from BTrees import OOBTree
+from zLOG import LOG, WARNING
 import os.path
 import math
 import time
@@ -113,15 +114,13 @@
 time = time.time
 
 # permissions
-ADD_DATAMGR_PERM = 'Add Transient Object Container'
-CHANGE_DATAMGR_PERM = 'Change Transient Object Containers'
+ADD_CONTAINER_PERM = 'Add Transient Object Container'
 MGMT_SCREEN_PERM = 'View management screens'
 ACCESS_CONTENTS_PERM = 'Access contents information'
 CREATE_TRANSIENTS_PERM = 'Create Transient Objects'
-ACCESS_SESSIONDATA_PERM = 'Access Transient Objects'
+ACCESS_TRANSIENTS_PERM = 'Access Transient Objects'
 MANAGE_CONTAINER_PERM = 'Manage Transient Object Container'
 
-
 constructTransientObjectContainerForm = HTMLFile(
     'dtml/addTransientObjectContainer', globals())
 
@@ -159,9 +158,6 @@
             'action':   'manage_access'
         },
 
-        {   'label':    'Advanced',
-            'action':   'manage_advanced'
-        }
     )
 
     security = ClassSecurityInfo()
@@ -172,7 +168,7 @@
                                 ['Manager',])
     security.setPermissionDefault(ACCESS_CONTENTS_PERM,
                                 ['Manager','Anonymous'])
-    security.setPermissionDefault(ACCESS_SESSIONDATA_PERM,
+    security.setPermissionDefault(ACCESS_TRANSIENTS_PERM,
                                 ['Manager','Anonymous'])
     security.setPermissionDefault(CREATE_TRANSIENTS_PERM,
                                 ['Manager',])
@@ -181,9 +177,6 @@
     manage_container = HTMLFile('dtml/manageTransientObjectContainer',
         globals())
 
-    security.declareProtected(MGMT_SCREEN_PERM, 'manage_advanced')
-    manage_advanced = HTMLFile('dtml/manageImpExpTransientObjects', globals())
-
     security.setDefaultAccess('deny')
 
     #
@@ -222,30 +215,32 @@
     #
 
     security.declareProtected(CREATE_TRANSIENTS_PERM, 'new')
-    def new(self, key):
-
+    def new(self, key, wrap_with=None):
         if type(key) is not type(''):
             raise TypeError, (key, "key is not a string type")
-    
         if self.has_key(key):
             raise KeyError, key         # Not allowed to dup keys
         
-        item = TransientObject(key, parent=self)
-
+        item = TransientObject(key)
         self[key] = item
-
         self.notifyAdd(item)
-
-        return item
-        
+        if not wrap_with:
+            return item.__of__(self)
+        else:
+            return item.__of__(wrap_with)
 
     security.declareProtected(CREATE_TRANSIENTS_PERM, 'new_or_existing')
-    def new_or_existing(self, key):
-
+    def new_or_existing(self, key, wrap_with=None):
         item  = self.get(key,_notfound)
-        if item is not _notfound: return item
-
-        return self.new(key)
+        if item is _notfound:
+            return self.new(key, wrap_with)
+        if not item.isValid():
+            del self[key]
+            return self.new(key, wrap_with)
+        if not wrap_with:
+            return item.__of__(self)
+        else:
+            return item.__of__(wrap_with)
 
     # -----------------------------------------------------------------
     # TransientItemContainer 
@@ -265,8 +260,7 @@
 
     security.declareProtected(MGMT_SCREEN_PERM, 'getAddNotificationTarget')
     def getAddNotificationTarget(self):
-        # What might we do here to help through the web stuff?
-        return self._addCallback
+        return self._addCallback or ''
 
     security.declareProtected(MANAGE_CONTAINER_PERM,
         'setAddNotificationTarget')
@@ -277,8 +271,7 @@
 
     security.declareProtected(MGMT_SCREEN_PERM, 'getDelNotificationTarget')
     def getDelNotificationTarget(self):
-        # What might we do here to help through the web stuff?
-        return self._delCallback
+        return self._delCallback or ''
 
     security.declareProtected(MANAGE_CONTAINER_PERM,
         'setDelNotificationTarget')
@@ -292,36 +285,62 @@
     # Supporting methods (not part of the interface)
     #
 
-
     def notifyAdd(self, item):
-
-        callback = self._addCallback
-
-        if type(callback) is type(''):
-            callback = self.aq_parent.unrestrictedTraverse(callback)
-
-        if callable(callback):
-            try:
-                callback(item, self)            # Use self as context
-            except: pass                        # Eat all errors 
+        if self._addCallback:
+            self._notify(item, 'add')
 
     def notifyDestruct(self, item):
+        if self._delCallback:
+            self._notify(item, 'destruct')
 
-        callback = self._delCallback
+    def _notify(self, item, kind):
+        if kind =='add':
+            name = 'notifyAdd'
+            callback = self._addCallback
+        else:
+            name = 'notifyDestruct'
+            callback = self._delCallback
 
         if type(callback) is type(''):
-            callback = self.aq_parent.unrestrictedTraverse(callback)
+            try:
+                method = self.unrestrictedTraverse(callback)
+            except (KeyError, AttributeError):
+                path = self.getPhysicalPath()
+                err = 'No such method %s in %s %s'
+                LOG('Transience',
+                    WARNING,
+                    err % (callback, '/'.join(path), name),
+                    error=sys.exc_info()
+                    )
+                return
+        else:
+            method = callback
 
-        if callable(callback):
+        if callable(method):
             try:
-                callback(item, self)            # Use self as context
-            except: pass                        # Eat all errors 
+                method(item, self)
+            except:
+                # dont raise, just log
+                path = self.getPhysicalPath()
+                LOG('Transience',
+                    WARNING,
+                    '%s failed when calling %s in %s' % (name, callback,
+                                                    '/'.join(path)),
+                    error=sys.exc_info()
+                    )
+        else:
+            err = '%s in %s attempted to call non-callable %s'
+            path = self.getPhysicalPath()
+            LOG('Transience',
+                WARNING,
+                err % (name, '/'.join(path), callback),
+                error=sys.exc_info()
+                )
 
     # -----------------------------------------------------------------
     # Management item support (non API)
     #
 
-
     security.declareProtected(MANAGE_CONTAINER_PERM,
         'manage_changeTransientObjectContainer')
     def manage_changeTransientObjectContainer(self, title='',
@@ -334,52 +353,16 @@
 
         self.title = title
         self.setTimeoutMinutes(timeout_mins)
+        if not addNotification:
+            addNotification = None
+        if not delNotification:
+            delNotification = None
         self.setAddNotificationTarget(addNotification)
         self.setDelNotificationTarget(delNotification)
 
         if REQUEST is not None:
             return self.manage_container(self, REQUEST)
 
-
-    security.declareProtected(MANAGE_CONTAINER_PERM,
-        'manage_exportTransientObjects')
-    def manage_exportTransientObjects(self, REQUEST=None):
-    
-        """
-        Export the transient objects to a named file in the var directory.
-        """
-
-        f = os.path.join(Globals.data_dir, "transientobjects.zexp")
-        self.c = PersistentMapping()
-        for k, v in self.items():
-            self.c[k] = v
-
-        get_transaction().commit()
-        self.c._p_jar.exportFile(self.c._p_oid, f)
-        del self.c
-        if REQUEST is not None:
-            return MessageDialog(
-                title="Transient objects exported",
-                message="Transient objects exported to %s" % f,
-                action="manage_container")
-        
-    security.declareProtected(MANAGE_CONTAINER_PERM,
-        'manage_importTransientObjects')
-    def manage_importTransientObjects(self, REQUEST=None):
-        """
-        Import the transient objects from a zexp file.
-        """
-        f = os.path.join(Globals.data_dir, "transientobjects.zexp")
-        conn = self._p_jar
-        ob = conn.importFile(f)
-        for k,v in ob.items():
-            self[k] = v
-        if REQUEST is not None:
-            return MessageDialog(
-                title="Transient objects imported",
-                message="Transient objects imported from %s" % f,
-                action="manage_container")
-
     def _setTimeout(self, timeout_mins):
         if type(timeout_mins) is not type(1):
             raise TypeError, (timeout_mins, "Must be integer")
@@ -400,7 +383,6 @@
         index = self._ctype()
         self._ring = Ring(l, index)
 
-
     def _getCurrentBucket(self, get_dump=0):
         # no timeout always returns last bucket
         if not self._timeout_secs:
@@ -430,32 +412,12 @@
             return b
 
     def _clean(self, b, index):
-
-
-        # What is all this?
-        #for ob in b.values():
-        #    d = last = None
-        #    f = getattr(ob, self._onend, None)
-        #    #
-        #    # HUH?
-        #    #
-        #    getDataMgr = getattr(ob, 'getDataMgr', None)
-        #    if getDataMgr is not None:
-        #        if callable(getDataMgr):
-        #            d = getDataMgr()
-        #        if d != last:
-        #            mgr = self.aq_parent.unrestrictedTraverse(d)
-        #            last = d
-        #
-        #    if callable(f): f(mgr)
-
         for k, v in list(index.items()):
             if v is b:
                 self.notifyDestruct(index[k][k])
                 del index[k]
         b.clear()
 
-
     def _show(self):
         """ debug method """
         b,dump,now = self._getCurrentBucket(1)
@@ -470,7 +432,6 @@
         for x in t:
             print x
 
-
     def __setitem__(self, k, v):
         current = self._getCurrentBucket()
         index = self._ring._index
@@ -501,14 +462,14 @@
             del b[k] # delete the item from the old bucket.
         return v
 
-    security.declareProtected(ACCESS_SESSIONDATA_PERM, 'get')
+    security.declareProtected(ACCESS_TRANSIENTS_PERM, 'get')
     def set(self, k, v):
         """ """
         if type(k) is not type(''):
             raise TypeError, "Transient Object Container keys must be strings"
         self[k] = v
 
-    security.declareProtected(ACCESS_SESSIONDATA_PERM, 'get')
+    security.declareProtected(ACCESS_TRANSIENTS_PERM, 'get')
     # Uses a different marker than _notfound
     def get(self, k, default=_marker):
         try: v = self[k]
@@ -527,12 +488,12 @@
         del index[k]
         del b[k]
 
-    security.declareProtected(ACCESS_SESSIONDATA_PERM, '__len__')
+    security.declareProtected(ACCESS_TRANSIENTS_PERM, '__len__')
     def __len__(self):
         self._getCurrentBucket()
         return len(self._ring._index)
 
-    security.declareProtected(ACCESS_SESSIONDATA_PERM, 'has_key')
+    security.declareProtected(ACCESS_TRANSIENTS_PERM, 'has_key')
     def has_key(self, k):
         self._getCurrentBucket()
         index = self._ring._index
@@ -558,7 +519,7 @@
     def copy(self):
         raise NotImplementedError
 
-    security.declareProtected(ACCESS_SESSIONDATA_PERM, 'getLen')
+    security.declareProtected(ACCESS_TRANSIENTS_PERM, 'getLen')
     getLen = __len__
     
 class Ring(Persistent):
@@ -587,8 +548,6 @@
     def _p_independent(self):
         return 1
 
-
-
 class TransientObject(Persistent, Implicit):
     """ akin to Session Data Object """
     __implements__ = (ItemWithId, # randomly generate an id
@@ -603,18 +562,16 @@
     security.declareObjectPublic()
 
     #
-    # Initialzer
+    # Initializer
     #
 
-    def __init__(self, id, parent=None):
-        self.name = id
+    def __init__(self, id):
+        self.token = id
         self.id = self._generateUniqueId()
-        self._parent = parent
         self._container = {}
         self._created = self._last_accessed = time()
         self._timergranularity = WRITEGRANULARITY # timer granularity
 
-
     # -----------------------------------------------------------------
     # ItemWithId
     #
@@ -627,10 +584,11 @@
     #
 
     def invalidate(self):
-        parent = self._parent
-        if parent: parent.notifyDestruct(self)
         self._invalid = None
 
+    def isValid(self):
+        return not hasattr(self, '_invalid')
+
     def getLastAccessed(self):
         return self._last_accessed
 
@@ -651,7 +609,6 @@
     # DictionaryLike
     #
 
-
     def keys(self):
         return self._container.keys()
 
@@ -692,9 +649,6 @@
         if hasattr(k, '_p_jar') and k._p_jar is None:
             k._p_jar = self._p_jar
             k._p_changed = 1
-        # unwrap this thing if it's wrapped
-        k = aq_base(k)
-        v = aq_base(v)
         self._container[k] = v
         self._p_changed = 1
 
@@ -726,19 +680,20 @@
         # other objects (eliminates read conflicts).
         return 1
 
-    def getName(self):
-        return self.name
+    getName = getId
 
+    def getToken(self):
+        return self.token
+    
     def _generateUniqueId(self):
-        return str(time())+str(random.randint(0,sys.maxint-1))
-
-    def __str__(self):
-        result = "<table>\n"
-        for (key, value) in self.items():
-            result = result + "<tr><th>%s</th><td>%s</td></tr>\n" % (key, value)
+        t = str(int(time()))
+        d = "%010d" % random.randint(0, sys.maxint-1)
+        return "%s%s" % (t, d)
 
-        result= result + "</table>"
-        return result
+    def __repr__(self):
+        return "id: %s, token: %s, contents: %s" % (
+            self.id, self.token, `self.items()`
+            )
 
 Globals.InitializeClass(TransientObjectContainer)
 Globals.InitializeClass(TransientObject)


=== Products/Transience/TransienceInterfaces.py 1.7 => 1.8 ===
 Transient Objects
 
-  TransientObjectContainers implement:
+  TransientObjectContainers are objects which contain zero or more
+  TransientObjects.  They implement the following interfaces:
 
     - ItemWithId
 
@@ -85,16 +86,24 @@
 
     - TransientItemContainer
 
-  In particular, one uses the 'new_&nbsp;_or_&nbsp;_existing' method on
-  TransientObjectContainers to retrieve or create a TransientObject based
-  on a given string key.  
-
-  If add or delete notifications are registered with the container, they
-  will be called back when items in the container are added or deleted,
-  with the item and the container as arguments.  The callbacks may be
-  registered either as bound methods, functions, or named paths in Zope.
-
-  TransientObjects implement:
+  In particular, one uses the 'new_or_existing' method on
+  TransientObjectContainers to retrieve or create a TransientObject
+  based on a given string key.
+
+  If add or delete notifications are registered with the container,
+  they will be called back when items in the container are added or
+  deleted, with the item and the container as arguments.  The
+  callbacks may be registered either as bound methods, functions, or
+  physical paths to Zope Script (Python Script or External Method)
+  objects (e.g. '/some/resolvable/script/name').  In any of these
+  cases, the delete and add notifications will be called with
+  arguments allowing the callbacks to operate on data representing the
+  state of the transient object at the moment of addition or deletion
+  (see setAddNotificationTarget and setDelNotificationTarget below).
+
+  TransientObjects are containerish items held within
+  TransientObjectContainers and they implement the following
+  interfaces:
 
     - ItemWithId
 
@@ -106,7 +115,28 @@
 
     - ImmutablyValuedMappingOfPickleableObjects
 
-"""
+  Of particular importance is the idea that TransientObjects do not
+  offer the contract of "normal" ZODB container objects; mutations
+  made to items which are contained within a TransientObject cannot be
+  expected to persist.  Developers need explicitly resave the state of
+  a subobject of a TransientObject by placing it back into the
+  TransientObject via the TransientObject.__setitem__ or .set methods.
+  This requirement is due to the desire to allow people to create
+  alternate TransientObject implementations that are *not* based on
+  the ZODB.  Practically, this means that when working with a
+  TransientObject which contains mutable subobjects (even if they
+  inherit from Persistence.Persistent), you *must* resave them back
+  into the TransientObject.  For example::
+
+    class Foo(Persistence.Persistent):
+        pass
+        
+    transient_object = transient_data_container.new('t')
+    foo = transient_object['foo'] = Foo()
+    foo.bar = 1
+    # the following is *necessary* to repersist the data
+    transient_object['foo'] = foo
+  """
 
 import Interface
 
@@ -119,6 +149,13 @@
         related to this object to be called as a side effect.
         """
 
+    def isValid(self):
+        """
+        Return true if transient object is still valid, false if not.
+        A transient object is valid if its invalidate method has not been
+        called.
+        """
+
     def getLastAccessed(self):
         """
         Return the time the transient object was last accessed in
@@ -178,23 +215,24 @@
 class ItemWithId(Interface.Base):
     def getId(self):
         """
-        Returns a meaningful unique id for the object.
+        Returns a meaningful unique id for the object.  Note that this id
+        need not the key under which the object is stored in its container.
         """
 
 class TTWDictionary(DictionaryLike, ItemWithId):
     def set(self, k, v):
         """
-        Call _&nbsp;&nbsp;_setitem_&nbsp;&nbsp;_ with key k, value v.
+        Call __setitem__ with key k, value v.
         """
 
     def delete(self, k):
         """
-        Call _&nbsp;&nbsp;_delitem_&nbsp;&nbsp;_ with key k.
+        Call __delitem__ with key k.
         """
 
     def __guarded_setitem__(self, k, v):
         """
-        Call _&nbsp;&nbsp;_setitem_&nbsp;&nbsp;_ with key k, value v.
+        Call __setitem__ with key k, value v.
         """
 
 class ImmutablyValuedMappingOfPickleableObjects(Interface.Base):
@@ -209,11 +247,10 @@
         Returns the value associated with key k.
 
         Note that no guarantee is made to persist changes made to mutable
-        objects obtained via _&nbsp;&nbsp;_getitem_&nbsp;&nbsp;_, even if
-        they support the ZODB Persistence interface.  In order to ensure
-        that changes to mutable values are persisted, you need to explicitly
-        put the value back in to the mapping via the
-        _&nbsp;&nbsp;_setitem_&nbsp;&nbsp;_.
+        objects obtained via __getitem__, even if they support the ZODB
+        Persistence interface.  In order to ensure that changes to mutable
+        values are persisted, you need to explicitly put the value back in
+        to the mapping via __setitem__.
         """
 
     def __delitem__(self, k):
@@ -228,16 +265,10 @@
      2.  Is responsible for the creation of its subobjects.
      3.  Allows for the access of a subobject by key.
     """
-    def getSubobjectInterface(self):
-        """
-        Returns the interface object which must be supported by items added
-        to or created by this container.
-        """
-
     def get(self, k, default=None):
         """
-        Return value associated with key k.  If value associated with k does
-        not exist, return default.
+        Return value associated with key k via __getitem__.  If value
+        associated with k does not exist, return default.
         """
 
     def has_key(self, k):
@@ -252,7 +283,7 @@
         """
 
 class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer):
-    def new(self, k):
+    def new(self, k, wrap_with=None):
         """
         Creates a new subobject of the type supported by this container
         with key "k" and returns it.
@@ -260,17 +291,25 @@
         If an object already exists in the container with key "k", a
         KeyError is raised.
 
+        If wrap_with is non-None, the subobject is returned in the
+        acquisition context of wrap_in, else it is returned in
+        the acquisition context of this transient object container.
+
         "k" must be a string, else a TypeError is raised.
         """
 
-    def new_or_existing(self, k):
+    def new_or_existing(self, k, wrap_with=None):
         """
         If an object already exists in the container with key "k", it
         is returned.
 
-        Otherwiser, create a new subobject of the type supported by this
+        Otherwise, create a new subobject of the type supported by this
         container with key "k" and return it.
 
+        If wrap_with is non-None, the subobject is returned in the
+        acquisition context of wrap_in, else it is returned in
+        the acquisition context of this transient object container.
+
         "k" must be a string, else a TypeError is raised.
         """
     
@@ -289,32 +328,45 @@
 
     def getAddNotificationTarget(self):
         """
-        Returns the current 'after add' function, or None.
+        Returns the currently registered 'add notification' value, or None.
         """
 
     def setAddNotificationTarget(self, f):
         """
-        Cause the 'after add' function to be 'f'.
+        Cause the 'add notification' function to be 'f'.
 
-        If 'f' is not callable and is a string, treat it as a Zope path to
-        a callable function.
+        If 'f' is not callable and is a string, treat it as a physical
+        path to a Zope Script object (Python Script, External Method,
+        et. al).
+
+        'add notify' functions need accept two arguments: 'item',
+        which is the transient object being destroyed, and 'container',
+        which is the transient object container which is performing
+        the destruction.  For example::
 
-        'after add' functions need accept a single argument: 'item', which
-        is the item being added to the container.
+          def addNotify(item, container):
+              print "id of 'item' arg was %s" % item.getId()
         """
 
     def getDelNotificationTarget(self):
         """
-        Returns the current 'before destruction' function, or None.
+        Returns the currently registered 'delete notification' value, or
+        None.
         """
 
     def setDelNotificationTarget(self, f):
         """
-        Cause the 'before destruction' function to be 'f'.
+        Cause the 'delete notification' function to be 'f'.
 
-        If 'f' is not callable and is a string, treat it as a Zope path to
-        a callable function.
+        If 'f' is not callable and is a string, treat it as a physical
+        path to a Zope Script object (Python Script, External Method,
+        et. al).
+
+        'Before destruction' functions need accept two arguments: 'item',
+        which is the transient object being destroyed, and 'container',
+        which is the transient object container which is performing
+        the destruction.  For example::
 
-        'before destruction' functions need accept a single argument: 'item',
-        which is the item being destroyed.
+          def delNotify(item, container):
+              print "id of 'item' arg was %s" % item.getId()
         """


=== Products/Transience/__init__.py 1.3 => 1.4 ===
     context.registerClass(
         Transience.TransientObjectContainer,
-        permission=Transience.ADD_DATAMGR_PERM,
+        permission=Transience.ADD_CONTAINER_PERM,
         icon='www/datacontainer.gif',
         constructors=(Transience.constructTransientObjectContainerForm,
                       Transience.constructTransientObjectContainer)