Ticket #6447: ticket_6447_warnings.diff

File ticket_6447_warnings.diff, 13.5 KB (added by Carl Meyer, 14 years ago)

updated patch using warnings

  • django/core/cache/__init__.py

    diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py
    a b  
    1818from cgi import parse_qsl
    1919from django.conf import settings
    2020from django.core import signals
    21 from django.core.cache.backends.base import InvalidCacheBackendError
     21from django.core.cache.backends.base import InvalidCacheBackendError, CacheKeyWarning
    2222from django.utils import importlib
    2323
    2424# Name for use in settings file --> name of module in "backends" directory.
  • django/core/cache/backends/base.py

    diff --git a/django/core/cache/backends/base.py b/django/core/cache/backends/base.py
    a b  
    11"Base Cache class."
    22
     3import warnings
     4
    35from django.core.exceptions import ImproperlyConfigured
     6from django.core.warnings import DjangoRuntimeWarning
    47
    58class InvalidCacheBackendError(ImproperlyConfigured):
    69    pass
    710
     11class CacheKeyWarning(DjangoRuntimeWarning):
     12    pass
     13
     14# memcached does not accept keys longer than 250 characters
     15MAX_KEY_LENGTH = 250
     16
    817class BaseCache(object):
    918    def __init__(self, params):
    1019        timeout = params.get('timeout', 300)
     
    116125    def clear(self):
    117126        """Remove *all* values from the cache at once."""
    118127        raise NotImplementedError
     128
     129    def validate_key(self, key):
     130        """
     131        Warn about keys that would not be portable to the memcached
     132        backend. This encourages (but does not force) writing backend-portable
     133        cache code.
     134
     135        """
     136        if len(key) > MAX_KEY_LENGTH:
     137            warnings.warn('Cache key is too long for memcached, and '
     138                          'will cause errors if used with memcached backend.'
     139                          ' (Max length %s)' % MAX_KEY_LENGTH,
     140                          CacheKeyWarning)
     141        for char in key:
     142            if ord(char) < 33 or ord(char) == 127:
     143                warnings.warn('Cache key contains control character, and '
     144                              'will cause errors if used with memcached backend.',
     145                              CacheKeyWarning)
     146
  • django/core/cache/backends/db.py

    diff --git a/django/core/cache/backends/db.py b/django/core/cache/backends/db.py
    a b  
    4646            self._cull_frequency = 3
    4747
    4848    def get(self, key, default=None):
     49        self.validate_key(key)
    4950        db = router.db_for_read(self.cache_model_class)
    5051        table = connections[db].ops.quote_name(self._table)
    5152        cursor = connections[db].cursor()
     
    6566        return pickle.loads(base64.decodestring(value))
    6667
    6768    def set(self, key, value, timeout=None):
     69        self.validate_key(key)
    6870        self._base_set('set', key, value, timeout)
    6971
    7072    def add(self, key, value, timeout=None):
     73        self.validate_key(key)
    7174        return self._base_set('add', key, value, timeout)
    7275
    7376    def _base_set(self, mode, key, value, timeout=None):
     
    103106            return True
    104107
    105108    def delete(self, key):
     109        self.validate_key(key)
    106110        db = router.db_for_write(self.cache_model_class)
    107111        table = connections[db].ops.quote_name(self._table)
    108112        cursor = connections[db].cursor()
     
    111115        transaction.commit_unless_managed(using=db)
    112116
    113117    def has_key(self, key):
     118        self.validate_key(key)
    114119        db = router.db_for_read(self.cache_model_class)
    115120        table = connections[db].ops.quote_name(self._table)
    116121        cursor = connections[db].cursor()
  • django/core/cache/backends/dummy.py

    diff --git a/django/core/cache/backends/dummy.py b/django/core/cache/backends/dummy.py
    a b  
    66    def __init__(self, *args, **kwargs):
    77        pass
    88
    9     def add(self, *args, **kwargs):
     9    def add(self, key, *args, **kwargs):
     10        self.validate_key(key)
    1011        return True
    1112
    1213    def get(self, key, default=None):
     14        self.validate_key(key)
    1315        return default
    1416
    15     def set(self, *args, **kwargs):
    16         pass
     17    def set(self, key, *args, **kwargs):
     18        self.validate_key(key)
    1719
    18     def delete(self, *args, **kwargs):
    19         pass
     20    def delete(self, key, *args, **kwargs):
     21        self.validate_key(key)
    2022
    2123    def get_many(self, *args, **kwargs):
    2224        return {}
    2325
    24     def has_key(self, *args, **kwargs):
     26    def has_key(self, key, *args, **kwargs):
     27        self.validate_key(key)
    2528        return False
    2629
    2730    def set_many(self, *args, **kwargs):
  • django/core/cache/backends/filebased.py

    diff --git a/django/core/cache/backends/filebased.py b/django/core/cache/backends/filebased.py
    a b  
    3232            self._createdir()
    3333
    3434    def add(self, key, value, timeout=None):
     35        self.validate_key(key)
    3536        if self.has_key(key):
    3637            return False
    3738
     
    3940        return True
    4041
    4142    def get(self, key, default=None):
     43        self.validate_key(key)
    4244        fname = self._key_to_file(key)
    4345        try:
    4446            f = open(fname, 'rb')
     
    5658        return default
    5759
    5860    def set(self, key, value, timeout=None):
     61        self.validate_key(key)
    5962        fname = self._key_to_file(key)
    6063        dirname = os.path.dirname(fname)
    6164
     
    7982            pass
    8083
    8184    def delete(self, key):
     85        self.validate_key(key)
    8286        try:
    8387            self._delete(self._key_to_file(key))
    8488        except (IOError, OSError):
     
    9599            pass
    96100
    97101    def has_key(self, key):
     102        self.validate_key(key)
    98103        fname = self._key_to_file(key)
    99104        try:
    100105            f = open(fname, 'rb')
  • django/core/cache/backends/locmem.py

    diff --git a/django/core/cache/backends/locmem.py b/django/core/cache/backends/locmem.py
    a b  
    3030        self._lock = RWLock()
    3131
    3232    def add(self, key, value, timeout=None):
     33        self.validate_key(key)
    3334        self._lock.writer_enters()
    3435        try:
    3536            exp = self._expire_info.get(key)
     
    4445            self._lock.writer_leaves()
    4546
    4647    def get(self, key, default=None):
     48        self.validate_key(key)
    4749        self._lock.reader_enters()
    4850        try:
    4951            exp = self._expire_info.get(key)
     
    7678        self._expire_info[key] = time.time() + timeout
    7779
    7880    def set(self, key, value, timeout=None):
     81        self.validate_key(key)
    7982        self._lock.writer_enters()
    8083        # Python 2.4 doesn't allow combined try-except-finally blocks.
    8184        try:
     
    8790            self._lock.writer_leaves()
    8891
    8992    def has_key(self, key):
     93        self.validate_key(key)
    9094        self._lock.reader_enters()
    9195        try:
    9296            exp = self._expire_info.get(key)
     
    127131            pass
    128132
    129133    def delete(self, key):
     134        self.validate_key(key)
    130135        self._lock.writer_enters()
    131136        try:
    132137            self._delete(key)
  • new file django/core/warnings.py

    diff --git a/django/core/warnings.py b/django/core/warnings.py
    new file mode 100644
    - +  
     1"Global Django warnings"
     2
     3class DjangoRuntimeWarning(RuntimeWarning):
     4    pass
  • docs/topics/cache.txt

    diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt
    a b  
    641641    However, if the backend doesn't natively provide an increment/decrement
    642642    operation, it will be implemented using a two-step retrieve/update.
    643643
     644Cache key warnings
     645------------------
     646
     647.. versionadded:: 1.3
     648
     649Memcached, the most commonly-used production cache backend, does not allow
     650cache keys longer than 250 characters or containing whitespace or control
     651characters, and using such keys will cause an exception. To encourage
     652cache-portable code and minimize unpleasant surprises, the other built-in cache
     653backends issue a warning (``django.core.cache.backends.base.CacheKeyWarning``)
     654if a key is used that would cause an error on memcached.
     655
     656If you are using a production backend that can accept a wider range of keys (a
     657custom backend, or one of the non-memcached built-in backends), and want to use
     658this wider range without warnings, you can silence ``CacheKeyWarning`` with
     659this code in the ``management`` module of one of your
     660:setting:`INSTALLED_APPS`::
     661
     662     import warnings
     663
     664     from django.core.cache import CacheKeyWarning
     665
     666     warnings.simplefilter("ignore", CacheKeyWarning)
     667
     668If you want to instead provide custom key validation logic for one of the
     669built-in backends, you can subclass it, override just the ``validate_key``
     670method, and follow the instructions for `using a custom cache backend`_. For
     671instance, to do this for the ``locmem`` backend, put this code in a module::
     672
     673    from django.core.cache.backends.locmem import CacheClass as LocMemCacheClass
     674
     675    class CacheClass(LiberalKeyValidationMixin, LocMemCacheClass):
     676        def validate_key(self, key):
     677            # do custom validation, raising an exception or a warning,
     678            # depending on your preference
     679
     680And use the dotted Python path to this module as the scheme portion of your
     681:setting:`CACHE_BACKEND`.
     682
    644683Upstream caches
    645684===============
    646685
  • new file tests/regressiontests/cache/liberal_backend.py

    diff --git a/tests/regressiontests/cache/liberal_backend.py b/tests/regressiontests/cache/liberal_backend.py
    new file mode 100644
    - +  
     1from django.core.cache.backends.locmem import CacheClass as LocMemCacheClass
     2
     3class LiberalKeyValidationMixin(object):
     4    def validate_key(self, key):
     5        pass
     6
     7class CacheClass(LiberalKeyValidationMixin, LocMemCacheClass):
     8    pass
     9
  • tests/regressiontests/cache/tests.py

    diff --git a/tests/regressiontests/cache/tests.py b/tests/regressiontests/cache/tests.py
    a b  
    88import tempfile
    99import time
    1010import unittest
     11import warnings
    1112
    1213from django.conf import settings
    1314from django.core import management
    1415from django.core.cache import get_cache
    15 from django.core.cache.backends.base import InvalidCacheBackendError
     16from django.core.cache.backends.base import InvalidCacheBackendError, CacheKeyWarning
    1617from django.http import HttpResponse, HttpRequest
    1718from django.middleware.cache import FetchFromCacheMiddleware, UpdateCacheMiddleware
    1819from django.utils import translation
     
    366367                count = count + 1
    367368        self.assertEqual(count, final_count)
    368369
     370    def test_invalid_keys(self):
     371        """
     372        All the builtin backends (except memcached, see below) should warn on
     373        keys that would be refused by memcached. This encourages portable
     374        caching code without making it too difficult to use production backends
     375        with more liberal key rules. Refs #6447.
     376
     377        """
     378        # On Python 2.6+ we could use the catch_warnings context
     379        # manager to test this warning nicely. Since we can't do that
     380        # yet, the cleanest option is to temporarily ask for
     381        # CacheKeyWarning to be raised as an exception.
     382        warnings.simplefilter("error", CacheKeyWarning)
     383
     384        # memcached does not allow whitespace or control characters in keys
     385        self.assertRaises(CacheKeyWarning, self.cache.set, 'key with spaces', 'value')
     386        # memcached limits key length to 250
     387        self.assertRaises(CacheKeyWarning, self.cache.set, 'a' * 251, 'value')
     388
     389        # The warnings module has no public API for getting the
     390        # current list of warning filters, so we can't save that off
     391        # and reset to the previous value, we have to globally reset
     392        # it. The effect will be the same, as long as the Django test
     393        # runner doesn't add any global warning filters (it currently
     394        # does not).
     395        warnings.resetwarnings()
     396
    369397class DBCacheTests(unittest.TestCase, BaseCacheTests):
    370398    def setUp(self):
    371399        # Spaces are used in the table name to ensure quoting/escaping is working
     
    397425        def setUp(self):
    398426            self.cache = get_cache(settings.CACHE_BACKEND)
    399427
     428        def test_invalid_keys(self):
     429            """
     430            On memcached, we don't introduce a duplicate key validation
     431            step (for speed reasons), we just let the memcached API
     432            library raise its own exception on bad keys. Refs #6447.
     433
     434            In order to be memcached-API-library agnostic, we only assert
     435            that a generic exception of some kind is raised.
     436
     437            """
     438            # memcached does not allow whitespace or control characters in keys
     439            self.assertRaises(Exception, self.cache.set, 'key with spaces', 'value')
     440            # memcached limits key length to 250
     441            self.assertRaises(Exception, self.cache.set, 'a' * 251, 'value')
     442
     443
    400444class FileBasedCacheTests(unittest.TestCase, BaseCacheTests):
    401445    """
    402446    Specific test cases for the file-based cache.
     
    429473    def test_cull(self):
    430474        self.perform_cull_test(50, 28)
    431475
     476class CustomCacheKeyValidationTests(unittest.TestCase):
     477    """
     478    Tests for the ability to mixin a custom ``validate_key`` method to
     479    a custom cache backend that otherwise inherits from a builtin
     480    backend, and override the default key validation. Refs #6447.
     481
     482    """
     483    def test_custom_key_validation(self):
     484        cache = get_cache('regressiontests.cache.liberal_backend://')
     485
     486        # this key is both longer than 250 characters, and has spaces
     487        key = 'some key with spaces' * 15
     488        val = 'a value'
     489        cache.set(key, val)
     490        self.assertEqual(cache.get(key), val)
     491
    432492class CacheUtils(unittest.TestCase):
    433493    """TestCase for django.utils.cache functions."""
    434494
Back to Top