[Zope3-checkins] SVN: Zope3/branches/stub-session/src/zope/app/session/ Refactoring session machinery

Stuart Bishop zen at shangri-la.dropbear.id.au
Thu May 27 23:01:49 EDT 2004


Log message for revision 25075:
Refactoring session machinery

I'm relying on adapters more now that we can do so. RAMSessionDataContainer
appears to be working fine, although as I wanted to behave identically
to the PersistentSessionDataContainer it is fully transactional so
it will give conflict errors if you abuse it. Hopefully I'm not (ab)using
MappingStorage in an unsupported way ;-)

Using sessions from TALES should all fall into place once the
adapter syntax has been nutted out and implemented.




-=-
Modified: Zope3/branches/stub-session/src/zope/app/session/__init__.py
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/__init__.py	2004-05-28 02:50:29 UTC (rev 25074)
+++ Zope3/branches/stub-session/src/zope/app/session/__init__.py	2004-05-28 03:01:48 UTC (rev 25075)
@@ -30,9 +30,9 @@
 from zope.app.utility.interfaces import ILocalUtility
 from zope.app.annotation.interfaces import IAttributeAnnotatable
 
-from interfaces import IBrowserIdManager, IBrowserId, ICookieBrowserIdManager, \
-                       ISessionDataContainer, ISession
-from zope.app.container.interfaces import IContained
+from interfaces import \
+        IBrowserIdManager, IBrowserId, ICookieBrowserIdManager, \
+        ISessionDataContainer, ISession, ISessionProductData, ISessionData
 
 import ZODB
 import ZODB.MappingStorage
@@ -45,10 +45,15 @@
 
 
 class BrowserId(str):
-    """A browser id"""
+    """See zope.app.interfaces.utilities.session.IBrowserId"""
     implements(IBrowserId)
 
+    def __new__(cls, request):
+        return str.__new__(
+                cls, zapi.getUtility(IBrowserIdManager).getBrowserId(request)
+                )
 
+
 class CookieBrowserIdManager(Persistent):
     """Session service implemented using cookies."""
 
@@ -71,11 +76,11 @@
         # we store a HMAC of the random value together with it, which makes
         # our session ids unforgeable.
         mac = hmac.new(s, self.secret, digestmod=sha).digest()
-        return BrowserId(s + digestEncode(mac))
+        return s + digestEncode(mac)
 
     def getRequestId(self, request):
-        """Return the IBrowserId encoded in request or None if it's
-        non-existent."""
+        """Return the browser id encoded in request as a string, 
+        or None if it's non-existent."""
         # If there is an id set on the response, use that but don't trust it.
         # We need to check the response in case there has already been a new
         # session created during the course of this request.
@@ -91,7 +96,7 @@
             != mac):
             return None
         else:
-            return BrowserId(sid)
+            return sid
 
     def setRequestId(self, request, id):
         """Set cookie with id on request."""
@@ -120,7 +125,7 @@
                     )
 
     def getBrowserId(self, request):
-        ''' See zope.app.interfaces.utilities.session.IBrowserIdManager '''
+        """See zope.app.session.interfaces.IBrowserIdManager"""
         sid = self.getRequestId(request)
         if sid is None:
             sid = self.generateUniqueId()
@@ -138,20 +143,23 @@
         self.data = OOBTree()
         self.sweepInterval = 5*60
 
-    def __getitem__(self, key):
-        rv = IterableUserDict.__getitem__(self, key)
+    def __getitem__(self, product_id):
+        rv = IterableUserDict.__getitem__(self, product_id)
         now = time.time()
         # Only update lastAccessTime once every few minutes, rather than
-        # every hit, to avoid ZODB bloat since this is being stored 
-        # persistently
+        # every hit, to avoid ZODB bloat and conflicts
         if rv.lastAccessTime + self.sweepInterval < now:
-            rv.lastAccessTime = now
+            rv.lastAccessTime = int(now)
             # XXX: When scheduler exists, this method should just schedule
             # a sweep later since we are currently busy handling a request
             # and may end up doing simultaneous sweeps
             self.sweep()
         return rv
 
+    def __setitem__(self, product_id, session_data):
+        session_data.lastAccessTime = int(time.time())
+        return IterableUserDict.__setitem__(self, product_id, session_data)
+
     def sweep(self):
         ''' Clean out stale data '''
         expire_time = time.time() - self.sweepInterval
@@ -164,7 +172,6 @@
             else:
                 return
 
-_ram_session_storages = {}
 
 class RAMSessionDataContainer(PersistentSessionDataContainer):
     ''' A SessionDataContainer that stores data in RAM. Currently session
@@ -176,6 +183,7 @@
         self.key = sha.new(str(time.time() + random.random())).hexdigest()
 
     _ram_storage = ZODB.MappingStorage.MappingStorage()
+    _ram_db = ZODB.DB(_ram_storage)
     _conns = {}
 
     def _getData(self):
@@ -183,74 +191,70 @@
         # Open a connection to _ram_storage per thread
         tid = thread.get_ident()
         if not self._conns.has_key(tid):
-            db = ZODB.DB(self._ram_storage)
-            self._conns[tid] = db.open()
+            self._conns[tid] = self._ram_db.open()
 
         root = self._conns[tid].root()
         if not root.has_key(self.key):
             root[self.key] = OOBTree()
         return root[self.key]
 
-        logger = logging.getLogger('zope.app.session')
-        logger.error('Oops %r' % (_ram_session_storages.keys(),))
-        
     data = property(_getData, None)
 
+    def sweep(self):
+        super(RAMSessionDataContainer, self).sweep()
+        self._ram_db.pack(time.time())
 
-class SessionData(Persistent, IterableUserDict):
-    ''' Mapping nodes in the ISessionDataContainer tree '''
-    implements(IMapping)
 
-    def __init__(self):
-        self.data = OOBTree()
-        self.lastAccessTime = time.time()
-
-
-class Session(IterableUserDict):
+class Session:
+    """See zope.app.session.interfaces.ISession"""
     implements(ISession)
-    def __init__(self, data_manager, browser_id, product_id):
-        ''' See zope.app.interfaces.utilities.session.ISession '''
-        browser_id = str(browser_id)
-        product_id = str(product_id)
-        try:
-            data = data_manager[browser_id]
-        except KeyError:
-            data_manager[browser_id] = SessionData()
-            data_manager[browser_id][product_id] = SessionData()
-            self.data = data_manager[browser_id][product_id]
-        else:
-            try:
-                self.data = data[product_id]
-            except KeyError:
-                data[product_id] = SessionData()
-                self.data = data[product_id]
+    __slots__ = ('browser_id',)
+    def __init__(self, request):
+        self.browser_id = str(IBrowserId(request))
 
-# XXX: remove context arg
-def getSession(context, request, product_id, session_data_container=None):
-    ''' Retrieve an ISession. session_data_container defaults to 
-        an ISessionDataContainer utility registered with the name product_id
+    def __getitem__(self, product_id):
+        """See zope.app.session.interfaces.ISession"""
 
-        XXX: This method will probably be changed when we have an
-            Interaction or other object that combines context & request
-            into a single object.
-    '''
-    if session_data_container is None:
+        # First locate the ISessionDataContainer by looking up
+        # the named Utility, and falling back to the unnamed one.
         try:
-            dc = zapi.getUtility(ISessionDataContainer, product_id)
+            sdc = zapi.getUtility(ISessionDataContainer, product_id)
         except ComponentLookupError:
+            # XXX: Do we want this?
             warnings.warn(
                     'Unable to find ISessionDataContainer named %s. '
                     'Using default' % repr(product_id),
                     RuntimeWarning
                     )
-            dc = zapi.getUtility(ISessionDataContainer)
-    elif ISessionDataContainer.providedBy(session_data_container):
-        dc = session_data_container
-    else:
-        dc = zapi.getUtility(ISessionDataContainer, session_data_container)
+            sdc = zapi.getUtility(ISessionDataContainer)
 
-    bim = zapi.getUtility(IBrowserIdManager)
-    browser_id = bim.getBrowserId(request)
-    return Session(dc, browser_id, product_id)
+        # The ISessionDataContainer contains two levels:
+        # ISessionDataContainer[product_id] == ISessionProductData
+        # ISessionDataContainer[product_id][browser_id] == ISessionData
+        try:
+            spd = sdc[product_id]
+        except KeyError:
+            sdc[product_id] = SessionProductData()
+            spd = sdc[product_id]
 
+        try:
+            return spd[self.browser_id]
+        except KeyError:
+            spd[self.browser_id] = SessionData()
+            return spd[self.browser_id]
 
+
+class SessionProductData(Persistent, IterableUserDict):
+    """See zope.app.session.interfaces.ISessionProductData"""
+    implements(ISessionProductData)
+    lastAccessTime = 0
+    def __init__(self):
+        self.data = OOBTree()
+
+
+class SessionData(Persistent, IterableUserDict):
+    """See zope.app.session.interfaces.ISessionData"""
+    implements(ISessionData)
+    def __init__(self):
+        self.data = OOBTree()
+

Modified: Zope3/branches/stub-session/src/zope/app/session/configure.zcml
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/configure.zcml	2004-05-28 02:50:29 UTC (rev 25074)
+++ Zope3/branches/stub-session/src/zope/app/session/configure.zcml	2004-05-28 03:01:48 UTC (rev 25075)
@@ -2,8 +2,20 @@
     xmlns="http://namespaces.zope.org/zope"
     xmlns:browser="http://namespaces.zope.org/browser">
 
-  <!-- Session machinery -->
+  <adapter
+      factory=".BrowserId"
+      provides=".interfaces.IBrowserId"
+      for="zope.publisher.interfaces.IRequest"
+      permission="zope.Public" 
+      />
 
+  <adapter
+      factory=".Session"
+      provides=".interfaces.ISession"
+      for="zope.publisher.interfaces.IRequest"
+      permission="zope.Public"
+      />
+
   <content class=".CookieBrowserIdManager">
     <require
         interface=".interfaces.ICookieBrowserIdManager"
@@ -13,10 +25,6 @@
         permission="zope.ManageContent" />
   </content>
 
-  <content class=".SessionData">
-    <allow interface="zope.interface.common.mapping.IMapping" />
-  </content>
-
   <content class=".PersistentSessionDataContainer">
     <implements
         interface=".interfaces.ISessionDataContainer"/>
@@ -39,6 +47,14 @@
         permission="zope.ManageContent" />
   </content>
 
+  <content class=".SessionData">
+    <allow interface="zope.interface.common.mapping.IMapping" />
+  </content>
+
+  <content class=".Session">
+    <allow interface=".interfaces.ISession" />
+  </content>
+
   <include file="browser.zcml" />
 
 </configure>

Modified: Zope3/branches/stub-session/src/zope/app/session/interfaces.py
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/interfaces.py	2004-05-28 02:50:29 UTC (rev 25074)
+++ Zope3/branches/stub-session/src/zope/app/session/interfaces.py	2004-05-28 03:01:48 UTC (rev 25075)
@@ -27,7 +27,7 @@
     """Manages sessions - fake state over multiple browser requests."""
 
     def getBrowserId(request):
-        """Return the IBrowserId for the given request.
+        """Return the browser id for the given request as a string.
         
         If the request doesn't have an attached sessionId a new one will be
         generated.
@@ -83,15 +83,15 @@
         """As a unique ASCII string"""
 
 
-class ISessionDataContainer(IReadMapping, IWriteMapping):
+class ISessionDataContainer(IMapping):
     """Stores data objects for sessions.
 
     The object implementing this interface is responsible for expiring data as
     it feels appropriate.
 
-    Used like::
+    Usage::
 
-      session_data_container[browser_id][product_id][key] = value
+      session_data_container[product_id][browser_id][key] = value
 
     Attempting to access a key that does not exist will raise a KeyError.
     """
@@ -116,17 +116,47 @@
             min=1,
             )
 
+    def __getitem__(self, product_id):
+        """Return an ISessionProductData"""
 
-class ISession(IMapping):
-    """A session object that keeps the state of the user.
+    def __setitem__(self, product_id, value):
+        """Store an ISessionProductData"""
 
-    To access bits of data within an ISessionDataContainer, we
-    need to know the browser_id, the product_id, and the actual key.
-    An ISession is a wrapper around an ISessionDataContainer that
-    simplifies this by storing the browser_id and product_id enabling
-    access using just the key.
+
+class ISession(Interface):
+    """This object allows retrieval of the correct ISessionData
+    for a particular product id
+    
+    >>> session = ISession(request)[product_id]
+    >>> session['color'] = 'red'
     """
 
-    def __init__(session_data_container, browser_id, product_id):
-        """Construct an ISession"""
+    def __getitem__(product_id):
+        """Locate the correct ISessionDataContainer for the given product id
+        and return that product id's ISessionData"""
 
+
+class ISessionProductData(IReadMapping, IWriteMapping):
+    """Storage for a particular product id's session data, containing
+    0 or more ISessionData instances"""
+
+    lastAccessTime = schema.Int(
+            title=_("Last Access Time"),
+            description=_(
+                "Approximate epoch time this ISessionData was last retrieved "
+                "from its ISessionDataContainer"
+                ),
+            default=0,
+            required=True,
+            )
+
+    def __getitem__(self, browser_id):
+        """Return an ISessionData"""
+
+    def __setitem__(self, browser_id, session_data):
+        """Store an ISessionData"""
+
+class ISessionData(IMapping):
+    """Storage for a particular product id and browser id's session data"""
+
+

Modified: Zope3/branches/stub-session/src/zope/app/session/session.stx
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/session.stx	2004-05-28 02:50:29 UTC (rev 25074)
+++ Zope3/branches/stub-session/src/zope/app/session/session.stx	2004-05-28 03:01:48 UTC (rev 25075)
@@ -9,27 +9,40 @@
 for propagating this id so that future requests from the browser get
 the same id (eg. by setting an HTTP cookie)
 
-ISessionDataContainer Utilities provide a mapping interface to store
-session data. The ISessionDataContainer is responsible for expiring
-data.
+ISessionDataContainer Utilities store session data. The ISessionDataContainer
+is responsible for expiring data.
 
+ISessionDataContainer[product_id] returns ISessionProductData
+ISessionDataContainer[product_id][browser_id] returns ISessionData
 
+ISession(request)[product_id] returns ISessionData
+
+An ISession determines what ISessionDataContainer to use by looking
+up an ISessionDataContainer using the product_id as the name, and
+falling back to the unnamed ISessionDataContainer utility. This allows
+site administrators to select which ISessionDataContainer a particular
+product stores its session data in by registering the utility with
+the relevant name(s).
+
 Python example::
 
-    >>> browser_id = getAdapter(request, IBrowserId))
+    >>> browser_id = IBrowserId(request)
 
-    >>> explicit_dm = getUtility(context, ISessionDataContainer, 
-    ...     'zopeproducts.fooprod')
-    >>> session = Session(explicit_dm, browser_id, 'zopeproducts.foorprod')
-    >>> session['color'] = 'red'
+    >>> session_data = ISession(request)['zopeproducts.fooprod']
+    >>> session_data['color'] = 'red'
 
-    or....
+    or for the adventurous....
 
-    >>> session = zapi.getSession(context, request, 'zopeproducts.fooprod')
-    >>> session['color'] = 'red'
+    >>> explicit_dc = getUtility(ISessionDataContainer, 'zopeproducts.fooprod')
+    >>> session_data = explicit_dc['zopeproducts.fooprod'][str(browser_id)]
+    >>> session_data = Session(explicit_dc, browser_id)['zopeproducts.fooprod']
+    >>> session_data['color'] = 'red'
 
+
 Page Template example::
 
+    XXX: Needs update when TALES adapter syntax decided
+
     <tal:x condition="exists:session/zopeproducts.fooprod/count">
        <tal:x condition="python:
         session['zopeproducts.fooprod']['count'] += 1" />
@@ -40,8 +53,3 @@
     </tal:x>
     <span content="session/zopeproducts.fooprod/count">6</span>
 
-TODO
-----
-Do we want to provide one or more 'default' ISessionDataContainer's out of the
-box (eg. 'persistant' and 'transient')?
-

Modified: Zope3/branches/stub-session/src/zope/app/session/tests.py
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/tests.py	2004-05-28 02:50:29 UTC (rev 25074)
+++ Zope3/branches/stub-session/src/zope/app/session/tests.py	2004-05-28 03:01:48 UTC (rev 25075)
@@ -26,15 +26,46 @@
 from zope.app.annotation.interfaces import IAttributeAnnotatable
 
 from zope.app.session.interfaces import \
-        IBrowserId, IBrowserIdManager, ISession, ISessionDataContainer
+        IBrowserId, IBrowserIdManager, \
+        ISession, ISessionDataContainer, ISessionData, ISessionProductData
 
 from zope.app.session import \
-        CookieBrowserIdManager, Session, SessionData, getSession, \
-        PersistentSessionDataContainer, RAMSessionDataContainer
+        BrowserId, CookieBrowserIdManager, \
+        PersistentSessionDataContainer, RAMSessionDataContainer, \
+        Session, SessionData, SessionProductData
 
+from zope.publisher.interfaces import IRequest
 from zope.publisher.interfaces.http import IHTTPRequest
 from zope.publisher.http import HTTPRequest
 
+def setUp(session_data_container_class):
+
+    # Placeful setup
+    root = setup.placefulSetUp(site=True)
+    setup.createStandardServices(root)
+    sm = setup.createServiceManager(root, True)
+    setup.addService(sm, Utilities, LocalUtilityService())
+
+    # Add a CookieBrowserIdManager Utility
+    setup.addUtility(sm, '', IBrowserIdManager, CookieBrowserIdManager())
+
+    # Add an ISessionDataContainer, registered under a number of names
+    sdc = session_data_container_class()
+    for product_id in ('', 'products.foo', 'products.bar', 'products.baz'):
+        setup.addUtility(sm, product_id, ISessionDataContainer, sdc)
+
+    # Register our adapters
+    ztapi.provideAdapter(IRequest, IBrowserId, BrowserId)
+    ztapi.provideAdapter(IRequest, ISession, Session)
+
+    # Return a request
+    request = HTTPRequest(None, None, {}, None)
+    return request
+
+def tearDown():
+    setup.placefulTearDown()
+
+
 def test_CookieBrowserIdManager():
     """
     CookieBrowserIdManager.generateUniqueId should generate a unique
@@ -45,12 +76,8 @@
     >>> id2 = bim.generateUniqueId()
     >>> id1 != id2
     True
-    >>> IBrowserId.providedBy(id1)
-    True
-    >>> IBrowserId.providedBy(id2)
-    True
 
-    CookieBrowserIdManager.getRequestId pulls the IBrowserId from an
+    CookieBrowserIdManager.getRequestId pulls the browser id from an
     IHTTPRequest, or returns None if there isn't one stored in it.
     Because cookies cannnot be trusted, we confirm that they are not forged,
     returning None if we have a corrupt or forged browser id.
@@ -74,16 +101,18 @@
     >>> bim.getRequestId(request) == bim.getRequestId(request2)
     True
 
-    CookieBrowserIdManager.getBrowserId pulls the IBrowserId from an
+    CookieBrowserIdManager.getBrowserId pulls the browser id from an
     IHTTPRequest, or generates a new one and returns it after storing
     it in the request.
 
     >>> id3 = bim.getBrowserId(request)
     >>> id4 = bim.getBrowserId(request)
-    >>> str(id3) == str(id4)
+    >>> id3 == id4
     True
     >>> id3 == id4
     True
+    >>> bool(id3)
+    True
 
     Confirm the path of the cookie is correct. The value being tested
     for here will eventually change - it should be the URL to the
@@ -117,6 +146,20 @@
     True
     """
 
+
+def test_BrowserId():
+    """
+    >>> request = setUp(PersistentSessionDataContainer)
+
+    >>> id1 = BrowserId(request)
+    >>> id2 = BrowserId(request)
+    >>> id1 == id2
+    True
+
+    >>> tearDown()
+    """
+
+
 def test_PersistentSessionDataContainer():
     """
     Ensure mapping interface is working as expected
@@ -157,8 +200,22 @@
     >>> ignore = sdc[1]
     >>> sdc.get(2, 'stale')
     'stale'
+
+    Ensure lastAccessTime on the ISessionData is being updated 
+    occasionally. The ISessionDataContainer maintains this whenever
+    the ISessionData is retrieved.
+
+    >>> sd = SessionData()
+    >>> sdc['product_id'] = sd
+    >>> sd.lastAccessTime > 0
+    True
+    >>> last1 = sd.lastAccessTime - 62
+    >>> sd.lastAccessTime = last1 # Wind back the clock
+    >>> last1 < sdc['product_id'].lastAccessTime
+    True
     """
 
+
 def test_RAMSessionDataContainer(self):
     pass
 test_RAMSessionDataContainer.__doc__ = \
@@ -166,10 +223,26 @@
             'PersistentSessionDataContainer', 'RAMSessionDataContainer'
             )
 
-def test_Session():
+
+def test_SessionProductData():
     """
-    >>> data_container = PersistentSessionDataContainer()
-    >>> session = Session(data_container, 'browser id', 'zopeproducts.foo')
+    >>> session = SessionProductData()
+    >>> ISessionProductData.providedBy(session)
+    True
+    """
+
+
+def test_SessionData():
+    """
+    >>> session = SessionData()
+
+    Is the interface defined?
+
+    >>> ISessionData.providedBy(session)
+    True
+
+    Make sure it actually works
+
     >>> session['color']
     Traceback (most recent call last):
     File "<stdin>", line 1, in ?
@@ -180,15 +253,6 @@
     >>> session['color']
     'red'
 
-    And make sure no namespace conflicts...
-
-    >>> session2 = Session(data_container, 'browser id', 'zopeproducts.bar')
-    >>> session2['color'] = 'blue'
-    >>> session['color']
-    'red'
-    >>> session2['color']
-    'blue'
-
     Test the rest of the dictionary interface...
 
     >>> 'foo' in session
@@ -214,67 +278,48 @@
     True
     """
 
-
-def test_localutilities():
+def test_Session():
     """
-    Setup a placeful environment with a IBrowserIdManager
-    and ISessionDataContainer
+    >>> request = setUp(PersistentSessionDataContainer)
+    >>> request2 = HTTPRequest(None, None, {}, None)
+  
+    >>> ISession.providedBy(Session(request))
+    True
 
-    >>> root = setup.placefulSetUp(site=True)
-    >>> setup.createStandardServices(root)
-    >>> sm = setup.createServiceManager(root, True)
-    >>> us = setup.addService(sm, Utilities, LocalUtilityService())
-    >>> idmanager = CookieBrowserIdManager()
-    >>> zope.interface.directlyProvides(idmanager,
-    ...                                 IAttributeAnnotatable, ILocalUtility)
-    >>> bim = setup.addUtility(
-    ...     sm, '', IBrowserIdManager, idmanager, 'test')
-    >>> pdc = PersistentSessionDataContainer()
-    >>> zope.interface.directlyProvides(pdc,
-    ...                                 IAttributeAnnotatable, ILocalUtility)
-    >>> sdc = setup.addUtility(sm, 'persistent', ISessionDataContainer, pdc)
-    >>> sdc = setup.addUtility(sm, 'products.foo',ISessionDataContainer, pdc)
-    >>> sdc = setup.addUtility(sm, 'products.bar', ISessionDataContainer, pdc)
-    >>> request = HTTPRequest(None, None, {}, None)
-   
-    Make sure we can access utilities
+    >>> session1 = Session(request)['products.foo']
+    >>> session2 = Session(request)['products.bar']
+    >>> session3 = Session(request)['products.bar']  # dupe
+    >>> session4 = Session(request2)['products.bar'] # not dupe
 
-    >>> sdc = zapi.getUtility(ISessionDataContainer, 'persistent',
-    ...                       context=root)
-    >>> bim = zapi.getUtility(IBrowserIdManager, context=root)
-
-    Make sure getSession works
-
-    >>> session1 = getSession(root, request, 'products.foo')
-    >>> session2 = getSession(root, request, 'products.bar', 'persistent')
-    >>> session3 = getSession(root, request, 'products.baz', pdc)
-    >>> session4 = getSession(root, request, 'products.foo')
-
     Make sure it returned sane values
 
-    >>> ISession.providedBy(session1)
+    >>> ISessionData.providedBy(session1)
     True
-    >>> ISession.providedBy(session2)
+    >>> ISessionData.providedBy(session2)
     True
-    >>> ISession.providedBy(session3)
+    >>> session2 == session3
     True
+    >>> ISessionData.providedBy(session4)
+    True
 
     Make sure that product_ids don't share a namespace, except when they should
 
     >>> session1['color'] = 'red'
     >>> session2['color'] = 'blue'
+    >>> session4['color'] = 'vomit'
     >>> session1['color']
     'red'
     >>> session2['color']
     'blue'
+    >>> session3['color']
+    'blue'
     >>> session4['color']
-    'red'
+    'vomit'
 
-    >>> setup.placefulTearDown()
-    >>> 'Thats all folks!'
-    'Thats all folks!'
+    >>> tearDown()
     """
 
+
 def test_suite():
     return unittest.TestSuite((
         doctest.DocTestSuite(),




More information about the Zope3-Checkins mailing list