[Repoze-checkins] r1117 - in repoze.accelerator/trunk: . repoze/accelerator

Chris McDonough chrism at agendaless.com
Sun Jun 22 19:19:25 EDT 2008


Author: Chris McDonough <chrism at agendaless.com>
Date: Sun Jun 22 19:19:25 2008
New Revision: 1117

Log:
Work towards "real" policy; always require zope.interface.


Added:
   repoze.accelerator/trunk/repoze/accelerator/policy.py   (contents, props changed)
   repoze.accelerator/trunk/repoze/accelerator/storage.py   (contents, props changed)
Modified:
   repoze.accelerator/trunk/repoze/accelerator/cache_headers.txt
   repoze.accelerator/trunk/repoze/accelerator/interfaces.py
   repoze.accelerator/trunk/repoze/accelerator/middleware.py
   repoze.accelerator/trunk/repoze/accelerator/tests.py
   repoze.accelerator/trunk/setup.py

Modified: repoze.accelerator/trunk/repoze/accelerator/cache_headers.txt
==============================================================================
--- repoze.accelerator/trunk/repoze/accelerator/cache_headers.txt	(original)
+++ repoze.accelerator/trunk/repoze/accelerator/cache_headers.txt	Sun Jun 22 19:19:25 2008
@@ -26,6 +26,9 @@
 accelerator may be configured to ignore some other requirements of the
 RFC (such as setting 'Age' and 'Warning' headers).
 
+An overview of caching in HTTP in the RFC is here:
+
+http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
 
 Header: 'Age'
 -------------
@@ -205,6 +208,17 @@
 
 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.44
 
+A server SHOULD use the Vary header field to inform a cache of what
+request-header fields were used to select among multiple
+representations of a cacheable response subject to server-driven
+negotiation. The set of header fields named by the Vary field value is
+known as the "selecting" request-headers.  When the cache receives a
+subsequent request whose Request-URI specifies one or more cache
+entries including a Vary header field, the cache MUST NOT use such a
+cache entry to construct a response to the new request unless all of
+the selecting request-headers present in the new request match the
+corresponding stored request-headers in the original request.
+
 Values referred to in Vary response headers might be used to find a
 resource in the cache that would otherwise need to be regenerated on
 the origin server.
@@ -214,6 +228,10 @@
 would be consulted during fetch, and one (or none) would be returned
 by comparing each entity against request environment data.
 
+A Vary header field-value of "*" always fails to match and subsequent
+requests on that resource can only be properly interpreted by the
+origin server.
+
 Header: 'Last-Modified'
 -----------------------
 
@@ -255,3 +273,18 @@
 
 We should probably cache anything with a 2XX response code, and ignore
 stuff with non-2XX responses. Except 206 (partial response).
+
+Not sure what to do about Set-Cookie headers.  See
+http://www.squid-cache.org/mail-archive/squid-dev/200101/0446.html .
+
+Simplifying assumptions
+-----------------------
+
+- We never store responses or fetch cached entities for Range
+  requests.
+
+- We always pass conditional requests (requests with
+  If-Modified-Since, If-Match, If-None-Match, etc.) through to the
+  application without getting involved in revalidation ourselves.  We
+  aren't interested in playing in any bandwidth-conservation schemes.
+

Modified: repoze.accelerator/trunk/repoze/accelerator/interfaces.py
==============================================================================
--- repoze.accelerator/trunk/repoze/accelerator/interfaces.py	(original)
+++ repoze.accelerator/trunk/repoze/accelerator/interfaces.py	Sun Jun 22 19:19:25 2008
@@ -1,21 +1,4 @@
-
-try:
-    from zope.interface import Interface
-    from zope.interface import implements
-    from zope.interface import provides
-    from zope.interface.verify import verifyClass
-    from zope.interface.verify import verifyObject
-except ImportError:
-    class Interface:
-        pass
-    def implements(iface):
-        pass
-    def provides(iface):
-        pass
-    def verifyClass(iface, klass):
-        return True
-    def verifyObject(iface, object):
-        return True
+from zope.interface import Interface
 
 class IChunkHandler(Interface):
     """ API of the helper object returned from a call to 'IStorage.store'.
@@ -68,16 +51,21 @@
     """ Reqired API of plugins which manage the cache's backing store.
     """
     def fetch(url):
-        """ Return a response from the backing store for the given 'url'.
+        """ Return a sequence of entries from the backing store for
+        the given 'url'.  An entry is in the form (status, headers, body_iter,
+        req_discrims, env_discrims).
 
         o Return None on a miss.
         """
 
-    def store(self, url, status, headers):
+    def store(url, status, headers, req_discrims, env_discrims):
         """ Prepare to cache a response to a backing store.
 
-        o 'url' is the key for the response.
-
+        o 'url' is the key for the response used during fetch.
+          Two stored entries should "hash" to the same value
+          if the tuple (url, req_discrims, env_discrims) is equal
+          for both.
+        
         o 'status' and 'headers' should be saved as well.
 
         o Return an object implementing IChunkHandler, which will

Modified: repoze.accelerator/trunk/repoze/accelerator/middleware.py
==============================================================================
--- repoze.accelerator/trunk/repoze/accelerator/middleware.py	(original)
+++ repoze.accelerator/trunk/repoze/accelerator/middleware.py	Sun Jun 22 19:19:25 2008
@@ -1,17 +1,4 @@
 import itertools
-import threading
-
-from paste.request import construct_url
-from paste.request import parse_headers
-from paste.response import header_value
-
-from repoze.accelerator.interfaces import IChunkHandler
-from repoze.accelerator.interfaces import IPolicy
-from repoze.accelerator.interfaces import IPolicyFactory
-from repoze.accelerator.interfaces import IStorage
-from repoze.accelerator.interfaces import IStorageFactory
-from repoze.accelerator.interfaces import implements
-from repoze.accelerator.interfaces import provides
 
 class Accelerator:
     def __init__(self, app, policy):
@@ -58,105 +45,14 @@
 
         raise StopIteration
 
-
-
-class NullPolicy:
-    """ Pass-through, caches nothing.
-    """
-    implements(IPolicy)
-    provides(IPolicyFactory)
-
-    def __init__(self, storage, config=None):
-        pass
-
-    def fetch(self, environ):
-        return None
-
-    def store(self, status, headers, environ):
-        pass
-
-class NaivePolicy:
-    implements(IPolicy)
-    provides(IPolicyFactory)
-
-    def __init__(self, storage, config=None):
-        self.storage = storage
-        if config is None:
-            config = {}
-        allowed_methods = config.get('policy.allowed_methods', 'GET')
-        self.allowed_methods = filter(None, allowed_methods.split())
-
-    def _minimalCacheOK(self, headers, environ):
-        if environ.get('REQUEST_METHOD', 'GET') not in self.allowed_methods:
-            return False
-        for nocache in ('Pragma', 'Cache-Control'):
-            value = header_value(headers, nocache)
-            if value and 'no-cache' in value.lower():
-                return False
-        return True
-
-    def store(self, status, headers, environ):
-        if not self._minimalCacheOK(headers, environ):
-            return
-
-        if not status.startswith('200') or status.startswith('203'):
-            return
-
-        outheaders = []
-        for key, val in headers:
-            if key.lower() not in ('status', 'content-encoding',
-                                   'transfer-encoding'):
-                outheaders.append((key, val))
-
-        url = construct_url(environ)
-
-        return self.storage.store(url, status, outheaders)
-    
-    def fetch(self, environ):
-        headers = parse_headers(environ)
-
-        if not self._minimalCacheOK(headers, environ):
-            return
-
-        url = construct_url(environ)
-
-        return self.storage.fetch(url)
-
-class RAMStorage:
-    implements(IStorage)
-    provides(IStorageFactory)
-
-    def __init__(self, lock=threading.Lock(), config=None):
-        self.data = {}
-        self.lock = lock
-
-    def store(self, url, status, headers):
-        result = []
-        storage = self
-
-        class SimpleHandler:
-            implements(IChunkHandler)
-            def write(self, chunk):
-                result.append(chunk)
-
-            def close(self):
-                storage.lock.acquire()
-                try:
-                    storage.data[url] = (status, headers, result)
-                finally:
-                    storage.lock.release()
-
-        return SimpleHandler()
-                
-    def fetch(self, url):
-        return self.data.get(url)
-
 def main(app, global_conf, **local_conf):
+    from repoze.accelerator.storage import make_memory_storage
+    from repoze.accelerator.policy import make_accelerator_policy
 
-    storage_factory = local_conf.get('storage', RAMStorage)
+    storage_factory = local_conf.get('storage', make_memory_storage)
     storage = storage_factory(config=local_conf)
 
-    policy_factory = local_conf.get('policy', NaivePolicy)
+    policy_factory = local_conf.get('policy', make_accelerator_policy)
     policy = policy_factory(storage, config=local_conf)
 
     return Accelerator(app, policy)

Added: repoze.accelerator/trunk/repoze/accelerator/policy.py
==============================================================================
--- (empty file)
+++ repoze.accelerator/trunk/repoze/accelerator/policy.py	Sun Jun 22 19:19:25 2008
@@ -0,0 +1,354 @@
+import calendar
+from email.Utils import parsedate_tz
+import time
+
+from paste.request import construct_url
+from paste.request import parse_headers
+from paste.response import header_value
+
+from zope.interface import implements
+from zope.interface import directlyProvides
+
+from repoze.accelerator.interfaces import IPolicy
+from repoze.accelerator.interfaces import IPolicyFactory
+
+class NullPolicy:
+    """ Pass-through, caches nothing.
+    """
+    implements(IPolicy)
+
+    def __init__(self, storage):
+        pass
+
+    def fetch(self, environ):
+        return None
+
+    def store(self, status, headers, environ):
+        pass
+
+def make_null_policy(storage, config=None):
+    return NullPolicy(storage)
+directlyProvides(make_null_policy, IPolicyFactory)
+
+class AcceleratorPolicy:
+    """ Simple accelerating cache policy.
+
+    - Allow configuration of "vary" policies for both request headers
+      and request environment values.
+
+    - Allow specification of "allowed methods"; requests which don't use
+      one of these methods won't be served from cache.
+
+    - Allow specification of "honor_shift_reload": if Pragma: no-cache or
+      Cache-Control: no-cache exists in the request, and this value is true,
+      the response will not be served from cache even if it otherwise might
+      have been.
+
+    - Allow specification of "store_https_responses".  If this is true,
+      we will store https responses and some information provided by
+      requests emitted via HTTPS.
+
+    When deciding whether we can fetch from our storage or not:
+
+    - If we honor shift-reload, and the request has a Pragma: no-cache
+      or Cache-Control: no-cache associated with it, don't try to
+      retrieve it from storage.
+
+    - If the request method doesn't match one of our allowed_methods,
+      don't try to retrieve it from storage.
+
+    - If the request has a Range, header, don't try to retrieve it
+      from storage.
+
+    - If the request is conditional (e.g. If-Modified-Since,
+      If-None-Match), don't try to retrieve it from storage.
+
+    - Otherwise, attempt to retrieve it from storage.
+
+    When deciding whether a value returned from storage should be
+    checked for freshness:
+
+    - If one representation stored matches the request headers and
+      environment, check it for freshness.
+
+    - Otherwise, abort.
+
+    To decide whether an item is fresh or not:
+
+    - If the entry in the cache is stale, don't serve from cache.
+      Staleness is defined as having a CC: max-age < (now -
+      entitydate) or an expires header whereby (expires - entitydate)
+      < (now - entitydate).  Entities which don't have a Date header
+      are also considered stale.
+
+    When deciding whether we can store response data in our storage:
+
+    - If the request method doesn't match one of our allowed_methods,
+      don't store.
+
+    - If the response status is not 200 (OK) or 203 (Non-Authoritative
+      Information), don't store.
+
+    - If the response has a Cache-Control header or a Pragma header,
+      and either has 'no-cache' in its value, don't store.
+
+    - If the response has a Cache-Control header, and it has a max-age
+      of 0 or a max-age we don't understand, don't store.
+
+    - If the request is an https request, and "store_https_responses" is false,
+      don't store.
+
+    - If the response does not have a Date header, don't store.
+
+    When storing data to storage:
+
+    - Store the status, the end-to-end headers in the response, and
+      information about request header and environment variance.
+      
+    """
+    implements(IPolicy)
+
+    def __init__(self,
+                 storage,
+                 allowed_methods=('GET',),
+                 always_vary_on_headers=(),
+                 always_vary_on_environ=('REQUEST_METHOD',),
+                 honor_shift_reload=True,
+                 store_https_responses=False,
+                 ):
+        self.storage = storage
+        self.allowed_methods = allowed_methods
+        self.always_vary_on_headers = always_vary_on_headers
+        self.always_vary_on_environ = always_vary_on_environ
+        self.honor_shift_reload = honor_shift_reload
+        self.store_https_responses = store_https_responses
+        
+    def fetch(self, environ):
+        if environ.get('REQUEST_METHOD', 'GET') not in self.allowed_methods:
+            return
+
+        request_headers = parse_headers(environ)
+
+        # if a Cache-Control/Pragma: no-cache header is in the request,
+        # and if honor_shift_reload is true, we don't serve it from cache
+        if self.honor_shift_reload:
+            if self._check_no_cache(request_headers, environ):
+                return
+        # we don't try to serve range requests up from the cache
+        if header_value(request_headers, 'Range'):
+            return
+        # we don't try to serve conditional requests up from cache
+        for conditional in ('If-Modified-Since', 'If-None-Match',
+                            'If-Match'):  # XXX other conditionals?
+            if header_value(request_headers, conditional):
+                return
+
+        url = construct_url(environ)
+        entries = self.storage.fetch(url)
+
+        if entries:
+            matching = self._discriminate(entries, request_headers, environ)
+            if not matching:
+                return
+        
+            status, response_headers, body = matching
+
+            if self._isfresh(response_headers):
+                return status, response_headers, body
+
+            # XXX purge?
+
+    def store(self, status, response_headers, environ):
+        request_headers = parse_headers(environ)
+
+        # abort if we shouldn't store this response
+        request_method = environ.get('REQUEST_METHOD', 'GET')
+        if request_method not in self.allowed_methods:
+            return
+        if not (status.startswith('200') or status.startswith('203')):
+            return
+        if self._check_no_cache(response_headers, environ):
+            return
+        cc_header = header_value(response_headers, 'Cache-Control')
+        if cc_header:
+            cc_parts = parse_cache_control_header(cc_header)
+            try:
+                if int(cc_parts.get('max-age')) == 0:
+                    return
+            except ValueError:
+                return
+        if environ['wsgi.url_scheme'] == 'https':
+            if not self.store_https_responses:
+                return
+        date = header_value(response_headers, 'Date')
+        if not date:
+            return
+
+        # if we didn't abort due to any condition above, store the response
+        vary_header_names = []
+        vary = header_value(response_headers, 'Vary')
+        if vary is not None:
+            vary_header_names.extend(
+                [ x.strip().lower() for x in vary.split(',') ])
+        if self.always_vary_on_headers:
+            vary_header_names.extend(list(self.always_vary_on))
+
+        if '*' in vary_header_names:
+            return
+
+        req_discrims = []
+        for header_name in vary_header_names:
+            value = header_value(request_headers, header_name)
+            if value is not None:
+                req_discrims.append((header_name, value))
+
+        env_discrims = []
+        for varname in self.always_vary_on_environ:
+            value = environ.get(varname)
+            if value is not None:
+                env_discrims.append((varname, value))
+
+        env_discrims.sort()
+        req_discrims.sort()
+        
+        headers = endtoend(response_headers)
+        url = construct_url(environ)
+
+        return self.storage.store(
+            url,
+            status,
+            headers,
+            req_discrims,
+            env_discrims,
+            )
+
+    def _discriminate(self, entries, request_headers, environ):
+
+        def req_getter(name):
+            return header_value(request_headers, name)
+
+        matching_entries = entries[:]
+
+        for entry in entries:
+            status, headers, body, req_discrims, env_discrims = entry
+            discrims = [ (environ.get, x) for x in env_discrims ]
+            discrims.extend([ (req_getter, x) for x in req_discrims ])
+
+            for (getter, discrim) in discrims:
+                stored_name, stored_value = discrim
+                strval = getter(stored_name)
+                if strval is None or strval.lower() != stored_value:
+                    matching_entries.remove(entry)
+                    break
+
+        if matching_entries:
+            match = matching_entries[0]
+            status, headers, body = match[:3]
+            return status, headers, body
+
+    def _check_no_cache(self, headers, environ):
+        for nocache in ('Pragma', 'Cache-Control'):
+            value = header_value(headers, nocache)
+            if value and 'no-cache' in value.lower():
+                return True
+        return False
+
+    def _isfresh(self, response_headers):
+        date = header_value(response_headers, 'Date')
+        if not date:
+            return False
+        date = parsedate_tz(date)
+        if not date:
+            return False
+        date = calendar.timegm(date)
+        now = time.time()
+        current_age = max(0, now - date)
+        cc_header = header_value(response_headers, 'Cache-Control')
+        expires_header = header_value(response_headers, 'Expires')
+
+        # freshness logic stolen from httplib2
+        lifetime = 0
+
+        if cc_header is not None:
+            header_parts = parse_cache_control_header(cc_header)
+            if 'max-age' in header_parts:
+                try:
+                    lifetime = int(header_parts['max-age'])
+                    if lifetime == 0:
+                        return False
+                except ValueError:
+                    lifetime = 0
+
+        if not lifetime:
+            if expires_header is not None:
+                expires = parsedate_tz(expires_header)
+                if expires is None:
+                    lifetime = 0
+                else:
+                    lifetime = max(0, calendar.timegm(expires) - date)
+
+        if lifetime > current_age:
+            return True
+
+        return False
+
+def make_accelerator_policy(storage, config=None):
+    if config is None:
+        config = {}
+    allowed_methods = config.get('policy.allowed_methods', 'GET')
+    allowed_methods = [x.upper() for x in
+                       filter(None, allowed_methods.split()) ]
+    honor_shift_reload = config.get('policy.honor_shift_reload', False)
+    honor_shift_reload = asbool(honor_shift_reload)
+    store_https_responses = config.get('policy.store_https_responses',False)
+    store_https_responses = asbool(store_https_responses)
+    always_vary_on_headers = config.get('policy.always_vary_on_headers', '')
+    always_vary_on_headers = filter(None, always_vary_on_headers.split())
+    always_vary_on_environ = config.get('policy.always_vary_on_environ',
+                                        'REQUEST_METHOD')
+    always_vary_on_environ = filter(None, always_vary_on_environ.split())
+    return AcceleratorPolicy(
+        storage,
+        allowed_methods,
+        always_vary_on_headers,
+        always_vary_on_environ,
+        honor_shift_reload,
+        store_https_responses,
+        )
+directlyProvides(make_accelerator_policy, IPolicyFactory)
+
+HOP_BY_HOP = ['connection',
+              'keep-alive',
+              'proxy-authenticate',
+              'proxy-authorization',
+              'te',
+              'trailers',
+              'transfer-encoding',
+              'upgrade']
+
+def endtoend(headers):
+    connection_header = header_value(headers, 'Connection') or ''
+    hop_by_hop = [x.strip().lower() for x in connection_header.split(',')]
+    hop_by_hop.extend(HOP_BY_HOP)
+    header_names = [ header[0] for header in headers ]
+    return [(name, header_value(headers, name)) for name
+             in header_names if name.lower() not in hop_by_hop]
+
+def parse_cache_control_header(header):
+    cc_parts = {}
+    if header is not None:
+        parts = [ x.strip() for x in header.split(',') ]
+        for part in parts:
+            if '=' in part:
+                key, val = [ x.strip() for x in part.split('=', 1) ]
+                cc_parts[key] = val
+            else:
+                cc_parts[key] = None
+    return cc_parts
+
+def asbool(val):
+    val= str(val)
+    if val.lower() in ('y', 'yes', 'true', 't'):
+        return True
+    return False
+

Added: repoze.accelerator/trunk/repoze/accelerator/storage.py
==============================================================================
--- (empty file)
+++ repoze.accelerator/trunk/repoze/accelerator/storage.py	Sun Jun 22 19:19:25 2008
@@ -0,0 +1,50 @@
+import threading
+
+from zope.interface import implements
+from zope.interface import directlyProvides
+
+from repoze.accelerator.interfaces import IChunkHandler
+from repoze.accelerator.interfaces import IStorage
+from repoze.accelerator.interfaces import IStorageFactory
+
+class MemoryStorage:
+    implements(IStorage)
+
+    def __init__(self, lock=threading.Lock()):
+        self.data = {}
+        self.lock = lock
+
+    def store(self, url, status, headers, req_discrims, env_discrims):
+        body = []
+        storage = self
+        req_discrims = tuple(req_discrims)
+        env_discrims = tuple(env_discrims)
+
+        class SimpleHandler:
+            implements(IChunkHandler)
+            def write(self, chunk):
+                body.append(chunk)
+
+            def close(self):
+                storage.lock.acquire()
+                try:
+                    discrims = storage.data.setdefault(url, {})
+                    discrims[(req_discrims, env_discrims)]=status, headers, body
+                finally:
+                    storage.lock.release()
+
+        return SimpleHandler()
+                
+    def fetch(self, url):
+        discrims = self.data.get(url)
+        if discrims is None:
+            return None
+        L = []
+        for (req_d, env_d), (status, headers, body) in discrims.items():
+            L.append((status, headers, body, req_d, env_d))
+        return L
+
+def make_memory_storage(config=None):
+    return MemoryStorage()
+directlyProvides(make_memory_storage, IStorageFactory)
+    

Modified: repoze.accelerator/trunk/repoze/accelerator/tests.py
==============================================================================
--- repoze.accelerator/trunk/repoze/accelerator/tests.py	(original)
+++ repoze.accelerator/trunk/repoze/accelerator/tests.py	Sun Jun 22 19:19:25 2008
@@ -2,62 +2,60 @@
 
 _MARKER = object()
 
-class TestRAMStorage(unittest.TestCase):
+class TestMemoryStorage(unittest.TestCase):
     def _getTargetClass(self):
-        from repoze.accelerator.middleware import RAMStorage
-        return RAMStorage
+        from repoze.accelerator.storage import MemoryStorage
+        return MemoryStorage
 
-    def _makeOne(self, lock, config=_MARKER):
+    def _makeOne(self, lock):
         klass = self._getTargetClass()
-        if config is _MARKER:
-            return klass(lock)
-        return klass(lock, config=config)
+        return klass(lock)
 
     def test_class_conforms_to_IStorage(self):
-        from repoze.accelerator.interfaces import verifyClass
+        from zope.interface.verify import verifyClass
         from repoze.accelerator.interfaces import IStorage
         verifyClass(IStorage, self._getTargetClass())
 
-    def test_class_provides_IStorageFactory(self):
-        from repoze.accelerator.interfaces import verifyObject
-        from repoze.accelerator.interfaces import IStorageFactory
-        verifyObject(IStorageFactory, self._getTargetClass())
-
     def test_instance_conforms_to_IStorage(self):
-        from repoze.accelerator.interfaces import verifyObject
+        from zope.interface.verify import verifyObject
         from repoze.accelerator.interfaces import IStorage
         verifyObject(IStorage, self._makeOne(DummyLock()))
 
-    def test_ctor_accepts_config(self):
-        lock = DummyLock()
-        storage = self._makeOne(lock, config={})
+    def test_factory_provides_IStorageFactory(self):
+        from zope.interface.verify import verifyObject
+        from repoze.accelerator.interfaces import IStorageFactory
+        from repoze.accelerator.storage import make_memory_storage
+        verifyObject(IStorageFactory, make_memory_storage)
 
     def test_store_nonexistent(self):
         lock = DummyLock()
         storage = self._makeOne(lock)
         headers = [('Header1', 'value1')]
-        handler = storage.store('url', 'status', headers)
+        handler = storage.store('url', 'status', headers, [], [])
         self.failIf(handler is None)
         chunks = ['chunk1', 'chunk2']
         for chunk in ('chunk1', 'chunk2'):
             handler.write(chunk)
         handler.close()
-        self.assertEqual(storage.data['url'], ('status', headers, chunks))
+        self.assertEqual(storage.data['url'][(), ()],
+                         ('status', headers, chunks))
         self.assertEqual(lock.acquired, 1)
         self.assertEqual(lock.released, 1)
 
     def test_store_existing(self):
         lock = DummyLock()
         storage = self._makeOne(lock)
-        storage.data['url'] = ('otherstatus', (), ())
+        storage.data['url'] = {}
+        storage.data['url'][(), ()] = ('otherstatus', (), ())
         headers = [('Header1', 'value1')]
-        handler = storage.store('url', 'status', headers)
+        handler = storage.store('url', 'status', headers, [], [])
         self.failIf(handler is None)
         chunks = ['chunk1', 'chunk2']
         for chunk in ('chunk1', 'chunk2'):
             handler.write(chunk)
         handler.close()
-        self.assertEqual(storage.data['url'], ('status', headers, chunks))
+        self.assertEqual(storage.data['url'][(), ()],
+                         ('status', headers, chunks))
         self.assertEqual(lock.acquired, 1)
         self.assertEqual(lock.released, 1)
 
@@ -69,19 +67,25 @@
     def test_fetch_existing(self):
         lock = DummyLock()
         storage = self._makeOne(lock)
-        storage.data['url'] = 1
-        self.assertEqual(storage.fetch('url'), 1)
+        storage.data['url'] = {
+            (1, 2):(200, [], []),
+            (3, 4):(203, [], [])
+            }
+        result = storage.fetch('url')
+        result.sort()
+        self.assertEqual(len(result), 2)
+        self.assertEqual(result[0], (200, [], [], 1, 2))
+        self.assertEqual(result[1], (203, [], [], 3, 4))
+
 
-class TestNaivePolicy(unittest.TestCase):
+class TestAcceleratorPolicy(unittest.TestCase):
     def _getTargetClass(self):
-        from repoze.accelerator.middleware import NaivePolicy
-        return NaivePolicy
+        from repoze.accelerator.policy import AcceleratorPolicy
+        return AcceleratorPolicy
 
-    def _makeOne(self, storage, config=_MARKER):
+    def _makeOne(self, storage):
         klass = self._getTargetClass()
-        if config is _MARKER:
-            return klass(storage)
-        return klass(storage, config=config)
+        return klass(storage)
 
     def _makeEnviron(self):
         return {
@@ -92,23 +96,20 @@
             }
 
     def test_class_conforms_to_IPolicy(self):
-        from repoze.accelerator.interfaces import verifyClass
+        from zope.interface.verify import verifyClass
         from repoze.accelerator.interfaces import IPolicy
         verifyClass(IPolicy, self._getTargetClass())
 
-    def test_class_provides_IPolicyFactory(self):
-        from repoze.accelerator.interfaces import verifyObject
-        from repoze.accelerator.interfaces import IPolicyFactory
-        verifyObject(IPolicyFactory, self._getTargetClass())
-
     def test_instance_conforms_to_IPolicy(self):
-        from repoze.accelerator.interfaces import verifyObject
+        from zope.interface.verify import verifyObject
         from repoze.accelerator.interfaces import IPolicy
         verifyObject(IPolicy, self._makeOne(DummyStorage()))
 
-    def test_ctor_accepts_config(self):
-        storage = DummyStorage()
-        policy = self._makeOne(storage, config={})
+    def test_factory_provides_IPolicyFactory(self):
+        from zope.interface.verify import verifyObject
+        from repoze.accelerator.interfaces import IPolicyFactory
+        from repoze.accelerator.policy import make_accelerator_policy
+        verifyObject(IPolicyFactory, make_accelerator_policy)
 
     def test_store_not_cacheable_post_request_method(self):
         storage = DummyStorage()
@@ -151,36 +152,42 @@
 
     def test_store_allowed_request_method_cacheable(self):
         storage = DummyStorage()
-        policy = self._makeOne(storage,
-                               config={'policy.allowed_methods': 'GET FOO'})
+        policy = self._makeOne(storage)
+        policy.allowed_methods = ('FOO',)
         environ = self._makeEnviron()
         environ['REQUEST_METHOD'] = 'FOO'
-        result = policy.store('200 OK', [('header1', 'value1')], environ)
-        self.assertEqual(result, False)
+        from email.Utils import formatdate
+        now = formatdate()
+        result = policy.store('200 OK', [('Date', now)], environ)
+        self.assertEqual(result, None)
         self.assertEqual(storage.url, 'http://example.com')
         self.assertEqual(storage.status, '200 OK')
-        self.assertEqual(storage.outheaders, [('header1', 'value1')])
+        self.assertEqual(storage.outheaders, [('Date', now)])
 
     def test_store_no_request_method_cacheable(self):
         storage = DummyStorage()
         policy = self._makeOne(storage)
         environ = self._makeEnviron()
         del environ['REQUEST_METHOD']
-        result = policy.store('200 OK', [('header1', 'value1')], environ)
-        self.assertEqual(result, False)
+        from email.Utils import formatdate
+        now = formatdate()
+        result = policy.store('200 OK', [('Date', now)], environ)
+        self.assertEqual(result, None)
         self.assertEqual(storage.url, 'http://example.com')
         self.assertEqual(storage.status, '200 OK')
-        self.assertEqual(storage.outheaders, [('header1', 'value1')])
+        self.assertEqual(storage.outheaders, [('Date', now)])
 
     def test_store_get_request_method_cacheable(self):
         storage = DummyStorage()
         policy = self._makeOne(storage)
         environ = self._makeEnviron()
-        result = policy.store('200 OK', [('header1', 'value1')], environ)
-        self.assertEqual(result, False)
+        from email.Utils import formatdate
+        now = formatdate()
+        result = policy.store('200 OK', [('Date', now)], environ)
+        self.assertEqual(result, None)
         self.assertEqual(storage.url, 'http://example.com')
         self.assertEqual(storage.status, '200 OK')
-        self.assertEqual(storage.outheaders, [('header1', 'value1')])
+        self.assertEqual(storage.outheaders, [('Date', now)])
 
     def test_fetch_fails_post_request_method(self):
         storage = DummyStorage(result=123)
@@ -207,19 +214,27 @@
         self.failIfEqual(result, 123)
 
     def test_fetch_succeeds_no_request_method(self):
-        storage = DummyStorage(result=123)
+        from email.Utils import formatdate
+        now = formatdate()
+        cc = 'max-age=4000'
+        expected = (200, [('Date', now), ('Cache-Control', cc)], [], [], [])
+        storage = DummyStorage(result=[expected])
         policy = self._makeOne(storage)
         environ = self._makeEnviron()
         del environ['REQUEST_METHOD']
         result = policy.fetch(environ)
-        self.assertEqual(result, 123)
+        self.assertEqual(result, expected[:3])
 
     def test_fetch_succeeds_get_request_method(self):
-        storage = DummyStorage(result=123)
+        from email.Utils import formatdate
+        now = formatdate()
+        cc = 'max-age=4000'
+        expected = (200, [('Date', now), ('Cache-Control', cc)], [], [], [])
+        storage = DummyStorage(result=[expected])
         policy = self._makeOne(storage)
         environ = self._makeEnviron()
         result = policy.fetch(environ)
-        self.assertEqual(result, 123)
+        self.assertEqual(result, expected[:3])
 
 class TestAcceleratorMiddleware(unittest.TestCase):
     def _getTargetClass(self):
@@ -324,14 +339,16 @@
         return self.handler
 
 class DummyStorage:
-    def __init__(self, result=None, writer=False):
+    def __init__(self, result=None, writer=None):
         self.result = result
         self.writer = writer
 
-    def store(self, url, status, outheaders):
+    def store(self, url, status, outheaders, req_discrims, env_discrims):
         self.url = url
         self.status = status
         self.outheaders = outheaders
+        self.req_discrims = req_discrims
+        self.env_discrims = env_discrims
         return self.writer
 
     def fetch(self, url):

Modified: repoze.accelerator/trunk/setup.py
==============================================================================
--- repoze.accelerator/trunk/setup.py	(original)
+++ repoze.accelerator/trunk/setup.py	Sun Jun 22 19:19:25 2008
@@ -47,8 +47,8 @@
       include_package_data=True,
       namespace_packages=['repoze'],
       zip_safe=False,
-      tests_require = ['Paste'],
-      install_requires=['Paste'],
+      tests_require = ['Paste', 'zope.interface'],
+      install_requires=['Paste', 'zope.interface'],
       test_suite="repoze.accelerator.tests",
       entry_points = """\
         [paste.filter_app_factory]


More information about the Repoze-checkins mailing list