Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow defining a cache as "shared" #167

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion cachecontrol/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

class BaseCache(object):

def __init__(self, shared=False):
self.shared = shared

def get(self, key):
raise NotImplemented()

Expand All @@ -22,7 +25,8 @@ def close(self):

class DictCache(BaseCache):

def __init__(self, init_dict=None):
def __init__(self, init_dict=None, shared=False):
super(DictCache, self).__init__(shared)
self.lock = Lock()
self.data = init_dict or {}

Expand Down
5 changes: 4 additions & 1 deletion cachecontrol/caches/file_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ def _secure_open_write(filename, fmode):

class FileCache(BaseCache):
def __init__(self, directory, forever=False, filemode=0o0600,
dirmode=0o0700, use_dir_lock=None, lock_class=None):
dirmode=0o0700, use_dir_lock=None, lock_class=None,
shared=False):

super(FileCache, self).__init__(shared)

if use_dir_lock is not None and lock_class is not None:
raise ValueError("Cannot use use_dir_lock and lock_class together")
Expand Down
3 changes: 2 additions & 1 deletion cachecontrol/caches/redis_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def total_seconds(td):

class RedisCache(BaseCache):

def __init__(self, conn):
def __init__(self, conn, shared=False):
super(RedisCache, self).__init__(shared)
self.conn = conn

def get(self, key):
Expand Down
13 changes: 13 additions & 0 deletions cachecontrol/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,10 +284,23 @@ def cache_response(self, request, response, body=None,
if 'no-store' in cc_req:
no_store = True
logger.debug('Request header has "no-store"')

if no_store and self.cache.get(cache_url):
logger.debug('Purging existing cache entry to honor "no-store"')
self.cache.delete(cache_url)


# Check to see if the cache shared or not
is_shared_cache = False
try:
is_shared_cache = self.cache.shared
except AttributeError:
pass

if 'private' in cc and is_shared_cache is True:
logger.debug('Request header has "private" and the cache is shared')
return

# If we've been given an etag, then keep the response
if self.cache_etags and 'etag' in response_headers:
logger.debug('Caching due to etag')
Expand Down
40 changes: 39 additions & 1 deletion tests/test_cache_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Unit tests that verify our caching methods work correctly.
"""
import pytest
from mock import ANY, Mock
from mock import ANY, Mock, patch
import time

from cachecontrol import CacheController
Expand Down Expand Up @@ -115,6 +115,44 @@ def test_cache_response_no_store(self):
cc.cache_response(self.req(), resp)
assert not cc.cache.get(cache_url)

def test_cache_response_private_with_shared_cache(self):
'''
When a cache store is shared, a private directive should turn off caching.

In this example, the etag is set, which should trigger a
cache, but since the private directive is set and the cache is
considered shared, we should not cache.
'''
resp = Mock()
cache = DictCache(shared=True)
cc = CacheController(cache)

cache_url = cc.cache_url(self.url)

resp = self.resp({'cache-control': 'max-age=3600, private'})

cc.cache_response(self.req(), resp)
assert not cc.cache.get(cache_url)

def test_cache_response_private_with_legacy_cache(self):
# Not all cache objects will have the "shared" attribute.
resp = Mock()
cache = Mock()
cache.shared.side_effect = AttributeError
cc = CacheController(cache)
cc.serializer = Mock()

cache_url = cc.cache_url(self.url)

now = time.strftime(TIME_FMT, time.gmtime())
resp = self.resp({
'cache-control': 'max-age=3600, private',
'date': now,
})

cc.cache_response(self.req(), resp)
assert cc.cache.set.called

def test_update_cached_response_with_valid_headers(self):
cached_resp = Mock(headers={'ETag': 'jfd9094r808', 'Content-Length': 100})

Expand Down