[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