[Zope3-checkins] SVN: Zope3/branches/jim-session/src/zope/app/session/ Begin rearranging session api and tests

Jim Fulton jim at zope.com
Sun Jun 6 11:49:42 EDT 2004


Log message for revision 25278:
Begin rearranging session api and tests


-=-
Added: Zope3/branches/jim-session/src/zope/app/session/README.txt
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/README.txt	2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/README.txt	2004-06-06 15:49:41 UTC (rev 25278)
@@ -0,0 +1,148 @@
+Sessions
+========
+
+Sessions provide a way to temporarily associate information with a
+client without requiring the authentication of a principal.  We
+associate an identifier with a particular client. Whenever we get a
+request from that client, we compute the identifier and use the
+identifier to look up associated information, which is stored on the
+server. 
+
+A major disadvantage of sessions is that they require management of
+information on the server. This can have major implications for
+scalability.  It is possible for a framework to make use of session
+data very easy for the developer.  This is great if scalability is not
+an issue, otherwise, it is a booby trap.
+
+Design Issues
+-------------
+
+Sessions introduce a number of issues to be considered:
+
+- Clients have to be identified. A number of approaches are possible,
+  including: 
+
+  o Using HTTP cookies. The application assigns a client identifier,
+    which is stored in a cookie.  This technique is the most
+    straightforward, but can be defeated if the client does not
+    support HTTP cookies (usually because the feature has been
+    disabled).
+
+  o Using URLs.  The application assigns a client identifier, which is
+    stored in the URL.  This makes URLs a bit uglier and requires some
+    care. If people copy URLs and send them to others, then you could
+    end up with multiple clients with the same session
+    identifier. There are a number of ways to reduce the risk of
+    accidental reuse of session identifiers:
+
+    - Embed the client IP address in the identifier
+
+    - Expire the identifier
+
+  o Use hidden form variables.  This complicates applications. It
+    requires all requests to be POST requests and requires the
+    maintenance of the hidden variables.
+
+  o Use the client IP address
+
+    This doesn't work very well, because an IP address may be shared by
+    many clients.
+
+- Data storage
+
+  Data can be simply stored in the object database. This provides lots
+  of flexibility. You can store pretty much anything you want as long
+  as it is persistent. You get the full benefit of the object database,
+  such as transactions, transparency, clustering, and so on.  Using
+  the object database is especially useful when:
+
+  - Writes are infrequent
+
+  - Data are complex
+
+  If writes are frequent, then the object database introduces
+  scalability problems.  Really, any transactional database is likely
+  to introduce problems with frequent writes. If you are tempted to
+  update session data on every request, think very hard about it.  You
+  are creating a scalability problem.
+
+  If you know that scalability is not (and never will be) an issue,
+  you can just use the object database.
+ 
+  If you have client data that needs to be updated often (as in every
+  request), consider storing the data on the client.  (Like all data
+  received from a client, it may be tainted and, in most instances,
+  should not be trusted. Sensitive information that the user should
+  not see should likewise not be stored on the client, unless
+  encrypted with a key the client has no access to.)  If you can't
+  store it on the client, then consider some other storage mechanism,
+  like a fast database, possibly without transaction support.
+
+  You may be tempted to store session data in memory for speed.  This
+  doesn't turn out to work very well.  If you need scalability, then
+  you need to be able to use an application-server cluster and storage
+  of session data in memory defeats that.  You can use
+  "server-affinity" to assure that requests from a client always go
+  back to the same server, but not all load balancers support server
+  affinity, and, for those that do, enabling server affinity tends to
+  defeat load balancing.
+
+- Session expiration
+
+  You may wish to ensure that sessions terminate after some period of
+  time. This may be for security reasons, or to avoid accidental
+  sharing of a session among multiple clients.  The policy might be
+  expressed in terms of total session time, or maximum inactive time,
+  or some combination.
+
+  There are a number of ways to approach this.  You can expire client
+  ids. You can expire session data.
+
+- Data expiration
+
+  Because HTTP is a stateless protocol, you can't tell whether a user
+  is thinking about a task or has simply stopped working on it.  Some
+  means is needed to free server session storage that is no-longer needed.
+
+  The simplest strategy is to never remove data. This strategy has
+  some obvious disadvantages.  Other strategies can be viewed as
+  optimizations of the basic strategy.  It is important to realize that
+  a data expiration strategy can be informed by, but need not be
+  constrained by a session-expiration strategy.
+
+Application programming interface
+---------------------------------
+
+Application code will merely adapt request objects to a session data
+interface.  Initially, we will define the session data interface
+`IPersistentSessionData'. `IPersistentSessionData` provides a mapping
+interface. Keys in the mapping are application identifiers. Values are
+persistent mapping objects.
+
+Application code that wants to get object session data for a request
+adapts the request to `IPersistentSessionData`::
+
+  data = IPersistentSessionData(request)[appkey]
+
+where `appkey` is a dotted name that identifies the application, such
+as ``zope.app.actionplan``.  Given the session data, the application
+can then store data in it::
+
+  data['original'] = original_actions
+  data['new'] = new_actions
+
+From ZPT, you can access session data using adapter syntax::
+
+  request*PersistentSession
+
+So, for example, to access the `old` key for the session data for the
+sample key above:
+
+  request*PersistentSession/zope.app.actionplan/old
+
+In this example, the data for an aplication key was a mapping object.
+The semantics of a session data for a particular application key are
+determined by the session data type interface.
+`IPersistentSessionData` defines the application data to be a mapping
+object by efault.  Other data interfaces could specify different
+bahavior.  


Property changes on: Zope3/branches/jim-session/src/zope/app/session/README.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/branches/jim-session/src/zope/app/session/__init__.py
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/__init__.py	2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/__init__.py	2004-06-06 15:49:41 UTC (rev 25278)
@@ -11,249 +11,7 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-"""
-Session implementation using cookies
+"""Session support
 
 $Id$
 """
-import sha, time, string, random, hmac, warnings, thread
-from UserDict import IterableUserDict
-from heapq import heapify, heappop
-
-from persistent import Persistent
-from zope.server.http.http_date import build_http_date
-from zope.interface import implements
-from zope.component import ComponentLookupError
-from zope.app.zapi import getUtility
-from BTrees.OOBTree import OOBTree
-from zope.app.utility.interfaces import ILocalUtility
-from zope.app.annotation.interfaces import IAttributeAnnotatable
-
-from interfaces import \
-        IBrowserIdManager, IBrowserId, ICookieBrowserIdManager, \
-        ISessionDataContainer, ISession, ISessionProductData, ISessionData
-
-import ZODB
-import ZODB.MappingStorage
-
-cookieSafeTrans = string.maketrans("+/", "-.")
-
-def digestEncode(s):
-    """Encode SHA digest for cookie."""
-    return s.encode("base64")[:-2].translate(cookieSafeTrans)
-
-
-class BrowserId(str):
-    """See zope.app.interfaces.utilities.session.IBrowserId"""
-    implements(IBrowserId)
-
-    def __new__(cls, request):
-        return str.__new__(
-                cls, getUtility(IBrowserIdManager).getBrowserId(request)
-                )
-
-
-class CookieBrowserIdManager(Persistent):
-    """Session service implemented using cookies."""
-
-    implements(IBrowserIdManager, ICookieBrowserIdManager,
-               ILocalUtility, IAttributeAnnotatable,
-               )
-
-    __parent__ = __name__ = None
-
-    def __init__(self):
-        self.namespace = "zope3_cs_%x" % (int(time.time()) - 1000000000)
-        self.secret = "%.20f" % random.random()
-        self.cookieLifetime = None
-
-    def generateUniqueId(self):
-        """Generate a new, random, unique id."""
-        data = "%.20f%.20f%.20f" % (random.random(), time.time(), time.clock())
-        digest = sha.sha(data).digest()
-        s = digestEncode(digest)
-        # 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 s + digestEncode(mac)
-
-    def getRequestId(self, request):
-        """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.
-        response_cookie = request.response.getCookie(self.namespace)
-        if response_cookie:
-            sid = response_cookie['value']
-        else:
-            sid = request.cookies.get(self.namespace)
-        if sid is None or len(sid) != 54:
-            return None
-        s, mac = sid[:27], sid[27:]
-        if (digestEncode(hmac.new(s, self.secret, digestmod=sha).digest())
-            != mac):
-            return None
-        else:
-            return sid
-
-    def setRequestId(self, request, id):
-        """Set cookie with id on request."""
-        # XXX Currently, the path is the ApplicationURL. This is reasonable,
-        #     and will be adequate for most purposes.
-        #     A better path to use would be that of the folder that contains
-        #     the service-manager this service is registered within. However,
-        #     that would be expensive to look up on each request, and would
-        #     have to be altered to take virtual hosting into account.
-        #     Seeing as this service instance has a unique namespace for its
-        #     cookie, using ApplicationURL shouldn't be a problem.
-
-        if self.cookieLifetime is not None:
-            if self.cookieLifetime:
-                expires = build_http_date(time.time() + self.cookieLifetime)
-            else:
-                expires = 'Tue, 19 Jan 2038 00:00:00 GMT'
-            request.response.setCookie(
-                    self.namespace, id, expires=expires,
-                    path=request.getApplicationURL(path_only=True)
-                    )
-        else:
-            request.response.setCookie(
-                    self.namespace, id,
-                    path=request.getApplicationURL(path_only=True)
-                    )
-
-    def getBrowserId(self, request):
-        """See zope.app.session.interfaces.IBrowserIdManager"""
-        sid = self.getRequestId(request)
-        if sid is None:
-            sid = self.generateUniqueId()
-        self.setRequestId(request, sid)
-        return sid
-
-
-class PersistentSessionDataContainer(Persistent, IterableUserDict):
-    ''' A SessionDataContainer that stores data in the ZODB '''
-    __parent__ = __name__ = None
-
-    implements(ISessionDataContainer, ILocalUtility, IAttributeAnnotatable)
-
-    def __init__(self):
-        self.data = OOBTree()
-        self.sweepInterval = 5*60
-
-    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 and conflicts
-        if rv.lastAccessTime + self.sweepInterval < 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
-        heap = [(v.lastAccessTime, k) for k,v in self.data.items()]
-        heapify(heap)
-        while heap:
-            lastAccessTime, key = heappop(heap)
-            if lastAccessTime < expire_time:
-                del self.data[key]
-            else:
-                return
-
-
-class RAMSessionDataContainer(PersistentSessionDataContainer):
-    ''' A SessionDataContainer that stores data in RAM. Currently session
-        data is not shared between Zope clients, so server affinity will
-        need to be maintained to use this in a ZEO cluster.
-    '''
-    def __init__(self):
-        self.sweepInterval = 5*60
-        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):
-
-        # Open a connection to _ram_storage per thread
-        tid = thread.get_ident()
-        if not self._conns.has_key(tid):
-            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]
-
-    data = property(_getData, None)
-
-    def sweep(self):
-        super(RAMSessionDataContainer, self).sweep()
-        self._ram_db.pack(time.time())
-
-
-class Session:
-    """See zope.app.session.interfaces.ISession"""
-    implements(ISession)
-    __slots__ = ('browser_id',)
-    def __init__(self, request):
-        self.browser_id = str(IBrowserId(request))
-
-    def __getitem__(self, product_id):
-        """See zope.app.session.interfaces.ISession"""
-
-        # First locate the ISessionDataContainer by looking up
-        # the named Utility, and falling back to the unnamed one.
-        try:
-            sdc = 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
-                    )
-            sdc = getUtility(ISessionDataContainer)
-
-        # 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()
-

Copied: Zope3/branches/jim-session/src/zope/app/session/http.py (from rev 25125, Zope3/trunk/src/zope/app/session/__init__.py)
===================================================================
--- Zope3/trunk/src/zope/app/session/__init__.py	2004-05-29 13:20:46 UTC (rev 25125)
+++ Zope3/branches/jim-session/src/zope/app/session/http.py	2004-06-06 15:49:41 UTC (rev 25278)
@@ -0,0 +1,266 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Session implementation using cookies
+
+$Id$
+"""
+
+from persistent import Persistent
+from zope.app.annotation.interfaces import IAttributeAnnotatable
+from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.app import zapi
+from zope.app.session.interfaces import IClientIdManager 
+from zope.app.utility.interfaces import ILocalUtility
+from zope import schema
+from zope.interface import implements
+from zope.server.http.http_date import build_http_date
+import hmac
+import random
+import re
+import sha
+import string
+import time
+
+cookieSafeTrans = string.maketrans("+/", "-.")
+
+def digestEncode(s):
+    """Encode SHA digest for cookie."""
+    return s.encode("base64")[:-2].translate(cookieSafeTrans)
+
+class ICookieClientIdManager(IClientIdManager):
+    """Manages sessions using a cookie"""
+
+    namespace = schema.TextLine(
+            title=_('Cookie Name'),
+            description=_(
+                "Name of cookie used to maintain state. "
+                "Must be unique to the site domain name, and only contain "
+                "ASCII letters, digits and '_'"
+                ),
+            required=True,
+            min_length=1,
+            max_length=30,
+            constraint=re.compile("^[\d\w_]+$").search,
+            )
+
+    cookieLifetime = schema.Int(
+            title=_('Cookie Lifetime'),
+            description=_(
+                "Number of seconds until the browser expires the cookie. "
+                "Leave blank expire the cookie when the browser is quit. "
+                "Set to 0 to never expire. "
+                ),
+            min=0,
+            required=False,
+            default=None,
+            missing_value=None,
+            )
+
+class CookieBrowserIdManager(Persistent):
+    """Session service implemented using cookies."""
+
+    implements(IClientIdManager, ICookieClientIdManager,
+               ILocalUtility, IAttributeAnnotatable,
+               )
+
+    __parent__ = __name__ = None
+
+    def __init__(self):
+        self.namespace = "zope3_cs_%x" % (int(time.time()) - 1000000000)
+        self.secret = "%.20f" % random.random()
+        self.cookieLifetime = None
+
+    def generateUniqueId(self):
+        """Generate a new, random, unique id.
+
+            >>> bim = CookieBrowserIdManager()
+            >>> id1 = bim.generateUniqueId()
+            >>> id2 = bim.generateUniqueId()
+            >>> id1 != id2
+            True
+
+           """
+        data = "%.20f%.20f%.20f" % (random.random(), time.time(), time.clock())
+        digest = sha.sha(data).digest()
+        s = digestEncode(digest)
+        # 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 s + digestEncode(mac)
+
+    def getRequestId(self, request):
+        """Return the browser id encoded in request as a string
+        
+           Return None if an id is not set.
+
+           For example:
+
+             >>> from zope.publisher.http import HTTPRequest
+             >>> request = HTTPRequest(None, None, {}, None)
+             >>> bim = CookieBrowserIdManager()
+
+           Because no cookie has been set, we get no id:
+
+             >>> bim.getRequestId(request) is None
+             True
+             >>> id1 = bim.generateUniqueId()
+
+           We can set an id:
+
+             >>> bim.setRequestId(request, id1)
+
+           And get it back:
+
+             >>> bim.getRequestId(request) == id1
+             True
+
+           When we set the request id, we also set a response cookie.  We
+           can simulate getting this cookie back in a subsequent request:
+
+             >>> request2 = HTTPRequest(None, None, {}, None)
+             >>> request2._cookies = dict(
+             ...   [(name, cookie['value'])
+             ...    for (name, cookie) in request.response._cookies.items()
+             ...   ])
+
+           And we get the same id back from the new request:
+
+             >>> bim.getRequestId(request) == bim.getRequestId(request2)
+             True
+
+           """
+
+        # 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.
+        response_cookie = request.response.getCookie(self.namespace)
+        if response_cookie:
+            sid = response_cookie['value']
+        else:
+            sid = request.cookies.get(self.namespace)
+        if sid is None or len(sid) != 54:
+            return None
+        s, mac = sid[:27], sid[27:]
+        if (digestEncode(hmac.new(s, self.secret, digestmod=sha).digest())
+            != mac):
+            return None
+        else:
+            return sid
+
+    def setRequestId(self, request, id):
+        """Set cookie with id on request.
+
+           This sets the response cookie:
+
+           See the examples in getRequestId.
+
+           Note that the id is checkec for validity. Setting an
+           invalid value is silently ignored:
+
+             >>> from zope.publisher.http import HTTPRequest
+             >>> request = HTTPRequest(None, None, {}, None)
+             >>> bim = CookieBrowserIdManager()
+             >>> bim.getRequestId(request)
+             >>> bim.setRequestId(request, 'invalid id')
+             >>> bim.getRequestId(request)
+
+           For now, the cookie path is the application URL:
+
+             >>> cookie = request.response.getCookie(bim.namespace)
+             >>> cookie['path'] == request.getApplicationURL(path_only=True)
+             True
+
+           In the future, it should be the site containing the
+           CookieBrowserIdManager
+
+           By default, session cookies don't expire:
+
+             >>> cookie.has_key('expires')
+             False
+
+           Expiry time of 0 means never (well - close enough)
+
+             >>> bim.cookieLifetime = 0
+             >>> request = HTTPRequest(None, None, {}, None)
+             >>> bid = bim.getClientId(request)
+             >>> cookie = request.response.getCookie(bim.namespace)
+             >>> cookie['expires']
+             'Tue, 19 Jan 2038 00:00:00 GMT'
+
+           A non-zero value means to expire after than number of seconds:
+
+             >>> bim.cookieLifetime = 3600
+             >>> request = HTTPRequest(None, None, {}, None)
+             >>> bid = bim.getClientId(request)
+             >>> cookie = request.response.getCookie(bim.namespace)
+             >>> import rfc822
+             >>> expires = time.mktime(rfc822.parsedate(cookie['expires']))
+             >>> expires > time.mktime(time.gmtime()) + 55*60
+             True
+           """
+        # XXX Currently, the path is the ApplicationURL. This is reasonable,
+        #     and will be adequate for most purposes.
+        #     A better path to use would be that of the folder that contains
+        #     the service-manager this service is registered within. However,
+        #     that would be expensive to look up on each request, and would
+        #     have to be altered to take virtual hosting into account.
+        #     Seeing as this service instance has a unique namespace for its
+        #     cookie, using ApplicationURL shouldn't be a problem.
+
+        if self.cookieLifetime is not None:
+            if self.cookieLifetime:
+                expires = build_http_date(time.time() + self.cookieLifetime)
+            else:
+                expires = 'Tue, 19 Jan 2038 00:00:00 GMT'
+            request.response.setCookie(
+                    self.namespace, id, expires=expires,
+                    path=request.getApplicationURL(path_only=True)
+                    )
+        else:
+            request.response.setCookie(
+                    self.namespace, id,
+                    path=request.getApplicationURL(path_only=True)
+                    )
+
+    def getClientId(self, request):
+        """Get the client id
+
+        This creates one if necessary:
+
+          >>> from zope.publisher.http import HTTPRequest
+          >>> request = HTTPRequest(None, None, {}, None)
+          >>> bim = CookieBrowserIdManager()
+          >>> id = bim.getClientId(request)
+          >>> id == bim.getClientId(request)
+          True
+
+        The id is retained accross requests:
+        
+          >>> request2 = HTTPRequest(None, None, {}, None)
+          >>> request2._cookies = dict(
+          ...   [(name, cookie['value'])
+          ...    for (name, cookie) in request.response._cookies.items()
+          ...   ])
+          >>> id == bim.getClientId(request2)
+          True
+          >>> bool(id)
+          True
+
+        """
+        sid = self.getRequestId(request)
+        if sid is None:
+            sid = self.generateUniqueId()
+        self.setRequestId(request, sid)
+        return sid

Modified: Zope3/branches/jim-session/src/zope/app/session/interfaces.py
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/interfaces.py	2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/interfaces.py	2004-06-06 15:49:41 UTC (rev 25278)
@@ -17,17 +17,14 @@
 """
 import re
 from zope.interface import Interface
-from zope.interface.common.mapping import IMapping, IReadMapping, IWriteMapping
-from zope import schema
-from zope.app.container.interfaces import IContainer
-from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.interface.common.mapping import IMapping
 
 
-class IBrowserIdManager(Interface):
-    """Manages sessions - fake state over multiple browser requests."""
+class IClientIdManager(Interface):
+    """Manages sessions - fake state over multiple client requests."""
 
-    def getBrowserId(request):
-        """Return the browser id for the given request as a string.
+    def getClientId(request):
+        """Return the client id for the given request as a string.
         
         If the request doesn't have an attached sessionId a new one will be
         generated.
@@ -40,123 +37,37 @@
 
 
     """ XXX: Want this
-    def invalidate(browser_id):
-        ''' Expire the browser_id, and remove any matching ISessionData data 
+    def invalidate(client_id):
+        ''' Expire the client_id, and remove any matching ISessionData data 
         '''
     """
 
-
-class ICookieBrowserIdManager(IBrowserIdManager):
-    """Manages sessions using a cookie"""
-
-    namespace = schema.TextLine(
-            title=_('Cookie Name'),
-            description=_(
-                "Name of cookie used to maintain state. "
-                "Must be unique to the site domain name, and only contain "
-                "ASCII letters, digits and '_'"
-                ),
-            required=True,
-            min_length=1,
-            max_length=30,
-            constraint=re.compile("^[\d\w_]+$").search,
-            )
-
-    cookieLifetime = schema.Int(
-            title=_('Cookie Lifetime'),
-            description=_(
-                "Number of seconds until the browser expires the cookie. "
-                "Leave blank expire the cookie when the browser is quit. "
-                "Set to 0 to never expire. "
-                ),
-            min=0,
-            required=False,
-            default=None,
-            missing_value=None,
-            )
-
-
-class IBrowserId(Interface):
-    """A unique ID representing a session"""
-
-    def __str__():
-        """As a unique ASCII string"""
-
-
-class ISessionDataContainer(IMapping):
-    """Stores data objects for sessions.
-
-    The object implementing this interface is responsible for expiring data as
-    it feels appropriate.
-
-    Usage::
-
-      session_data_container[product_id][browser_id][key] = value
-
-    Attempting to access a key that does not exist will raise a KeyError.
+class IPersistentSessionDataManager(IMapping):
+    """Manage IPersistentSessionData objects by client id
     """
 
-    timeout = schema.Int(
-            title=_(u"Timeout"),
-            description=_(
-                "Number of seconds before data becomes stale and may "
-                "be removed"),
-            default=3600,
-            required=True,
-            min=1,
-            )
-    sweepInterval = schema.Int(
-            title=_(u"Purge Interval"),
-            description=_(
-                "How often stale data is purged in seconds. "
-                "Higer values improve performance."
-                ),
-            default=5*60,
-            required=True,
-            min=1,
-            )
+    def __getitem__(self, client_id):
+        """Returns data for a client id
 
-    def __getitem__(self, product_id):
-        """Return an ISessionProductData"""
+        If data hasn't been stored previously, a new IPersistentSessionData
+        will be created and returned.
+        
+        """
+    
+class IPersistentSessionData(IMapping):
+    """Provide persistent session storage
 
-    def __setitem__(self, product_id, value):
-        """Store an ISessionProductData"""
+    Data are stored persistently and transactionally. Data stored must
+    be persistent or picklable.
 
-
-class ISession(Interface):
-    """This object allows retrieval of the correct ISessionData
-    for a particular product id
-    
-    >>> session = ISession(request)[product_id]
-    >>> session['color'] = 'red'
+    Note that this type of sessionstoreage should not be written to
+    very frequently.
     """
 
-    def __getitem__(product_id):
-        """Locate the correct ISessionDataContainer for the given product id
-        and return that product id's ISessionData"""
+    def __getitem__(self, application_id):
+        """Returns data for an application id
 
-
-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"""
-
-
+        If data hasn't been stored previously, a new mapping object
+        will be created and returned.
+        
+        """

Copied: Zope3/branches/jim-session/src/zope/app/session/persist.py (from rev 25125, Zope3/trunk/src/zope/app/session/__init__.py)
===================================================================
--- Zope3/trunk/src/zope/app/session/__init__.py	2004-05-29 13:20:46 UTC (rev 25125)
+++ Zope3/branches/jim-session/src/zope/app/session/persist.py	2004-06-06 15:49:41 UTC (rev 25278)
@@ -0,0 +1,254 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Session implementation using cookies
+
+$Id$
+"""
+        
+from BTrees.OOBTree import OOBTree
+from heapq import heapify, heappop
+from persistent import Persistent
+from UserDict import DictMixin
+from zope.app.annotation.interfaces import IAttributeAnnotatable
+from zope.app.container.contained import Contained
+from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.app import zapi
+from zope.app.session.interfaces import IClientIdManager 
+from zope.app.session.interfaces import IPersistentSessionData
+from zope.app.session.interfaces import IPersistentSessionDataManager
+from zope.app.utility.interfaces import ILocalUtility
+from zope import schema
+import random
+import sha
+import thread
+import time
+import ZODB.MappingStorage
+import zope.cachedescriptors.property
+import zope.interface
+        
+
+class ISessionDataConfig(zope.interface.Interface):
+    """Configure session data storage"""
+
+    sweepInterval = schema.Int(
+            title=_(u"Purge Interval"),
+            description=_(
+                "How often stale data is purged in seconds. "
+                "Higer values improve performance."
+                ),
+            default=5*60,
+            required=True,
+            min=1,
+            )
+
+class PersistentSessionDataContainer(Persistent, Contained):
+    ''' A SessionDataContainer that stores data in the ZODB '''
+
+    zope.interface.implements(
+        IPersistentSessionDataManager,
+        ISessionDataConfig,
+        ILocalUtility,
+        IAttributeAnnotatable,
+        )
+
+    def __init__(self):
+        self.data = OOBTree()
+        self.sweepInterval = 5*60
+
+    _v_data = zope.cachedescriptors.property.Volatile('_v_data', dict)
+
+    def __getitem__(self, client_id):
+        """Return data for a client id
+
+           If no data has been stored for the client id, then return
+           a new PersistentSessionData object, but don't modify the
+           PersistentSessionDataContainer.
+
+           Let's look at an example:
+
+             >>> container = PersistentSessionDataContainer()
+             >>> session = container['id']
+             >>> session['appid1'] = 42
+
+           Of course, if we ask for the session data again. we'll get
+           the same object:
+
+             >>> container['id'] is session
+             True
+
+           This applies to new data, even when the container hasn't
+           changed:
+
+             >>> newsession = container['newid']
+             >>> newsession is session
+             False
+             >>> newsession is container['newid']
+             True
+
+           The container has a sweep interval, in seconds.
+           Data older than that interval is removed. To see how this
+           works, we'll "age" our session object by setting it's last
+           access time to a time in the distant past:
+
+             >>> from time import time
+             >>> session.lastAccessTime = (
+             ...    int(time()) - container.sweepInterval * 2)
+
+           Now, if we sk for a session, we'll get a different one:
+
+             >>> container['id'] is session
+             False
+
+           And the data we stored before will be gone:
+
+             >>> 'appid1' in container['id']
+             False
+           """
+        now = time.time()
+        
+        rv = self.data.get(client_id)
+        if rv is None:
+            rv = self._v_data.get(client_id)
+            if rv is None:
+                rv = PersistentSessionData(self, client_id)
+                rv.lastAccessTime = int(now)
+                self._v_data[client_id] = rv
+            
+        # Only update lastAccessTime once every few minutes, rather than
+        # every hit, to avoid ZODB bloat and conflicts
+        if rv.lastAccessTime + self.sweepInterval < 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()
+            rv = PersistentSessionData(self, client_id)
+            rv.lastAccessTime = int(now)
+            self._v_data[client_id] = rv
+            
+        return rv
+
+    def _setitem(self, client_id, session_data):
+        # This method is for use by PersistentSessionData only
+        if client_id in self._v_data:
+            del self._v_data[client_id]
+        self.data[client_id] = session_data
+
+    def sweep(self):
+        ''' Clean out stale data '''
+        expire_time = time.time() - self.sweepInterval
+        heap = [(v.lastAccessTime, k) for k,v in self.data.items()]
+        heapify(heap)
+        while heap:
+            lastAccessTime, key = heappop(heap)
+            if lastAccessTime < expire_time:
+                del self.data[key]
+            else:
+                return
+
+
+class RAMSessionDataContainer(PersistentSessionDataContainer):
+    ''' A SessionDataContainer that stores data in RAM. Currently session
+        data is not shared between Zope clients, so server affinity will
+        need to be maintained to use this in a ZEO cluster.
+    '''
+    def __init__(self):
+        self.sweepInterval = 5*60
+        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):
+
+        # Open a connection to _ram_storage per thread
+        tid = thread.get_ident()
+        if not self._conns.has_key(tid):
+            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]
+
+    data = property(_getData, None)
+
+    def sweep(self):
+        super(RAMSessionDataContainer, self).sweep()
+        self._ram_db.pack(time.time())
+
+
+class PersistentSessionData(Persistent, DictMixin):
+
+    zope.interface.implements(IPersistentSessionData)
+
+    def __init__(self, manager, client_id):
+        self.manager, self.client_id = manager, client_id
+        self.data = {}
+
+    _v_data = zope.cachedescriptors.property.Volatile('_v_data', dict)
+
+    def __getitem__(self, application_key):
+        r = self.data.get(application_key)
+        if r is None:
+            r = self._v_data.get(application_key)
+            if r is None:
+                r = PersistentApplicationData(self, application_key)
+        return r
+
+    def __setitem__(self, application_key, application_data):
+        if self.manager is not None:
+            self.manager._setitem(self.client_id, self)
+            self.manager = None
+        if application_key in self._v_data:
+            del self._v_data[application_key]
+        self.data[application_key] = application_data
+        self._p_changed = 1
+
+    def __delitem__(self, application_key):
+        if application_key in self.data:
+            del self.data[application_key]
+        if application_key in self._v_data:
+            del self._v_data[application_key]
+        self._p_changed = 1
+
+    def keys(self):
+        return self.data.keys
+            
+
+class PersistentApplicationData(DictMixin):
+    def __init__(self, manager, application_key):
+        self.manager = manager
+        self.application_key = application_key
+        self.data = {}
+
+    def __getitem__(self, key):
+        return self.data[key]
+        
+    def __setitem__(self, key, value):
+        self.manager[self.application_key] = self
+        self.data[key] = value
+
+    def __delitem__(self, key):
+        del self.data[key]
+
+    def keys(self):
+        return self.data.keys()
+
+def accessPersistentSessionData(request):
+    ids = zapi.getUtility(IClientIdManager)
+    client_id = ids.getClientId(request)
+    manager = zapi.getUtility(IPersistentSessionDataManager)
+    return manager[client_id]

Deleted: Zope3/branches/jim-session/src/zope/app/session/session.stx
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/session.stx	2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/session.stx	2004-06-06 15:49:41 UTC (rev 25278)
@@ -1,55 +0,0 @@
-Session Support
----------------
-
-Sessions allow us to fake state over a stateless protocol - HTTP. We do this
-by having a unique identifier stored across multiple HTTP requests, be it
-a cookie or some id mangled into the URL.
-
-The IBrowserIdManager Utility provides this unique id. It is responsible
-for propagating this id so that future requests from the browser get
-the same id (eg. by setting an HTTP cookie)
-
-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 = IBrowserId(request)
-
-    >>> session_data = ISession(request)['zopeproducts.fooprod']
-    >>> session_data['color'] = 'red'
-
-    or for the adventurous....
-
-    >>> 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" />
-    </tal:x>
-    <tal:x condition="not:exists:session/zopeprodicts.fooprod/count">
-        <tal:x condition="python:
-            session['zopeproducts.fooprod']['count'] = 1 />
-    </tal:x>
-    <span content="session/zopeproducts.fooprod/count">6</span>
-

Modified: Zope3/branches/jim-session/src/zope/app/session/tests.py
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/tests.py	2004-06-06 15:27:08 UTC (rev 25277)
+++ Zope3/branches/jim-session/src/zope/app/session/tests.py	2004-06-06 15:49:41 UTC (rev 25278)
@@ -1,4 +1,3 @@
-# -*- coding: ascii -*-
 ##############################################################################
 #
 # Copyright (c) 2004 Zope Corporation and Contributors.
@@ -12,313 +11,14 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-'''
+"""XXX short summary goes here.
+
 $Id$
-'''
-import unittest, doctest, time, rfc822
-from zope.app import zapi
-from zope.app.tests import ztapi
-from zope.app.tests import setup
-import zope.interface
-from zope.app.utility.interfaces import ILocalUtility
-from zope.app.utility import LocalUtilityService
-from zope.app.servicenames import Utilities
-from zope.app.annotation.interfaces import IAttributeAnnotatable
+"""
+import unittest
+from zope.testing.doctestunit import DocTestSuite
 
-from zope.app.session.interfaces import \
-        IBrowserId, IBrowserIdManager, \
-        ISession, ISessionDataContainer, ISessionData, ISessionProductData
 
-from zope.app.session import \
-        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
-    IBrowserId each time it is called
-
-    >>> bim = CookieBrowserIdManager()
-    >>> id1 = bim.generateUniqueId()
-    >>> id2 = bim.generateUniqueId()
-    >>> id1 != id2
-    True
-
-    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.
-    
-    >>> request = HTTPRequest(None, None, {}, None)
-    >>> bim.getRequestId(request) is None
-    True
-    >>> bim.setRequestId(request, id1)
-    >>> bim.getRequestId(request) == id1
-    True
-    >>> bim.setRequestId(request, 'invalid_id')
-    >>> bim.getRequestId(request) is None
-    True
-
-    Make sure that the same browser id is extracted from a cookie in
-    request (sent from the browser) and a cookie in request.response
-    (set during this transaction)
-
-    >>> request2 = HTTPRequest(None, None, {}, None)
-    >>> request2._cookies = request.response._cookies
-    >>> bim.getRequestId(request) == bim.getRequestId(request2)
-    True
-
-    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)
-    >>> 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
-    site containing the CookieBrowserIdManager
-
-    >>> cookie = request.response.getCookie(bim.namespace)
-    >>> cookie['path'] == request.getApplicationURL(path_only=True)
-    True
-
-    Confirm the expiry time of the cookie is correct.
-    Default is no expires.
-
-    >>> cookie.has_key('expires')
-    False
-
-    Expiry time of 0 means never (well - close enough)
-
-    >>> bim.cookieLifetime = 0
-    >>> request = HTTPRequest(None, None, {}, None)
-    >>> bid = bim.getBrowserId(request)
-    >>> cookie = request.response.getCookie(bim.namespace)
-    >>> cookie['expires']
-    'Tue, 19 Jan 2038 00:00:00 GMT'
-
-    >>> bim.cookieLifetime = 3600
-    >>> request = HTTPRequest(None, None, {}, None)
-    >>> bid = bim.getBrowserId(request)
-    >>> cookie = request.response.getCookie(bim.namespace)
-    >>> expires = time.mktime(rfc822.parsedate(cookie['expires']))
-    >>> expires > time.mktime(time.gmtime()) + 55*60
-    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
-
-    >>> sdc = PersistentSessionDataContainer()
-    >>> sdc['a']
-    Traceback (most recent call last):
-    File "<stdin>", line 1, in ?
-    File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 19, in __getitem__
-        def __getitem__(self, key): return self.data[key]
-    KeyError: 'a'
-    >>> sdc['a'] = SessionData()
-    >>> pdict = SessionData()
-    >>> sdc['a'] = pdict
-    >>> id(pdict) == id(sdc['a'])
-    True
-    >>> del sdc['a']
-    >>> sdc['a']
-    Traceback (most recent call last):
-    File "<stdin>", line 1, in ?
-    File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 19, in __getitem__
-        def __getitem__(self, key): return self.data[key]
-    KeyError: 'a'
-    >>> del sdc['a']
-    Traceback (most recent call last):
-    File "<stdin>", line 1, in ?
-    File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 21, in __delitem__
-        def __delitem__(self, key): del self.data[key]
-    KeyError: 'a'
-
-    Make sure stale data is removed
-
-    >>> sdc.sweepInterval = 60
-    >>> sdc[1], sdc[2] = sd1, sd2 = SessionData(), SessionData()
-    >>> ignore = sdc[1], sdc[2]
-    >>> sd1.lastAccessTime = sd1.lastAccessTime - 62
-    >>> sd2.lastAccessTime = sd2.lastAccessTime - 62
-    >>> 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__ = \
-        test_PersistentSessionDataContainer.__doc__.replace(
-            'PersistentSessionDataContainer', 'RAMSessionDataContainer'
-            )
-
-
-def test_SessionProductData():
-    """
-    >>> 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 ?
-    File "zope/app/utilities/session.py", line 157, in __getitem__
-        return self._data[key]
-    KeyError: 'color'
-    >>> session['color'] = 'red'
-    >>> session['color']
-    'red'
-
-    Test the rest of the dictionary interface...
-
-    >>> 'foo' in session
-    False
-    >>> 'color' in session
-    True
-    >>> session.get('size', 'missing')
-    'missing'
-    >>> session.get('color', 'missing')
-    'red'
-    >>> list(session.keys())
-    ['color']
-    >>> list(session.values())
-    ['red']
-    >>> list(session.items())
-    [('color', 'red')]
-    >>> len(session)
-    1
-    >>> [x for x in session]
-    ['color']
-    >>> del session['color']
-    >>> session.get('color') is None
-    True
-    """
-
-def test_Session():
-    """
-    >>> request = setUp(PersistentSessionDataContainer)
-    >>> request2 = HTTPRequest(None, None, {}, None)
-  
-    >>> ISession.providedBy(Session(request))
-    True
-
-    >>> session1 = Session(request)['products.foo']
-    >>> session2 = Session(request)['products.bar']
-    >>> session3 = Session(request)['products.bar']  # dupe
-    >>> session4 = Session(request2)['products.bar'] # not dupe
-
-    Make sure it returned sane values
-
-    >>> ISessionData.providedBy(session1)
-    True
-    >>> ISessionData.providedBy(session2)
-    True
-    >>> 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']
-    'vomit'
-
-    >>> tearDown()
-    """
-
 from zope.app.appsetup.tests import TestBootstrapSubscriberBase, EventStub
 class TestBootstrapInstance(TestBootstrapSubscriberBase):
 
@@ -345,13 +45,11 @@
 
 def test_suite():
     return unittest.TestSuite((
-        doctest.DocTestSuite(),
+        DocTestSuite('zope.app.session.http'),
+        DocTestSuite('zope.app.session.persist'),
         unittest.makeSuite(TestBootstrapInstance),
         ))
 
 if __name__ == '__main__':
-    unittest.main()
+    unittest.main(defaultTest='test_suite')
 
-# vim: set filetype=python ts=4 sw=4 et si
-
-




More information about the Zope3-Checkins mailing list