Refs #32193 -- Removed MemcachedCache per deprecation timeline.

This commit is contained in:
Mariusz Felisiak 2021-09-16 09:37:03 +02:00
parent e2be307b3a
commit 05f3a6186e
5 changed files with 12 additions and 157 deletions

View File

@ -1,14 +1,11 @@
"Memcached cache backend" "Memcached cache backend"
import pickle
import re import re
import time import time
import warnings
from django.core.cache.backends.base import ( from django.core.cache.backends.base import (
DEFAULT_TIMEOUT, BaseCache, InvalidCacheKey, memcache_key_warnings, DEFAULT_TIMEOUT, BaseCache, InvalidCacheKey, memcache_key_warnings,
) )
from django.utils.deprecation import RemovedInDjango41Warning
from django.utils.functional import cached_property from django.utils.functional import cached_property
@ -136,43 +133,6 @@ class BaseMemcachedCache(BaseCache):
raise InvalidCacheKey(warning) raise InvalidCacheKey(warning)
class MemcachedCache(BaseMemcachedCache):
"An implementation of a cache binding using python-memcached"
# python-memcached doesn't support default values in get().
# https://github.com/linsomniac/python-memcached/issues/159
_missing_key = None
def __init__(self, server, params):
warnings.warn(
'MemcachedCache is deprecated in favor of PyMemcacheCache and '
'PyLibMCCache.',
RemovedInDjango41Warning, stacklevel=2,
)
# python-memcached ≥ 1.45 returns None for a nonexistent key in
# incr/decr(), python-memcached < 1.45 raises ValueError.
import memcache
super().__init__(server, params, library=memcache, value_not_found_exception=ValueError)
self._options = {'pickleProtocol': pickle.HIGHEST_PROTOCOL, **self._options}
def get(self, key, default=None, version=None):
key = self.make_and_validate_key(key, version=version)
val = self._cache.get(key)
# python-memcached doesn't support default values in get().
# https://github.com/linsomniac/python-memcached/issues/159
# Remove this method if that issue is fixed.
if val is None:
return default
return val
def delete(self, key, version=None):
# python-memcached's delete() returns True when key doesn't exist.
# https://github.com/linsomniac/python-memcached/issues/170
# Call _deletetouch() without the NOT_FOUND in expected results.
key = self.make_and_validate_key(key, version=version)
return bool(self._cache._deletetouch([b'DELETED'], 'delete', key))
class PyLibMCCache(BaseMemcachedCache): class PyLibMCCache(BaseMemcachedCache):
"An implementation of a cache binding using pylibmc" "An implementation of a cache binding using pylibmc"
def __init__(self, server, params): def __init__(self, server, params):

View File

@ -260,3 +260,6 @@ to remove usage of these features.
* ``TransactionTestCase.assertQuerysetEqual()`` no longer calls ``repr()`` on a * ``TransactionTestCase.assertQuerysetEqual()`` no longer calls ``repr()`` on a
queryset when compared to string values. queryset when compared to string values.
* The ``django.core.cache.backends.memcached.MemcachedCache`` backend is
removed.

View File

@ -158,12 +158,6 @@ permanent storage -- they're all intended to be solutions for caching, not
storage -- but we point this out here because memory-based caching is storage -- but we point this out here because memory-based caching is
particularly temporary. particularly temporary.
.. deprecated:: 3.2
The ``MemcachedCache`` backend is deprecated as ``python-memcached`` has
some problems and seems to be unmaintained. Use ``PyMemcacheCache`` or
``PyLibMCCache`` instead.
.. _redis: .. _redis:
Redis Redis
@ -931,12 +925,6 @@ stored a literal value ``None``, use a sentinel object as the default::
>>> cache.get('my_key', sentinel) is sentinel >>> cache.get('my_key', sentinel) is sentinel
True True
.. admonition:: ``MemcachedCache``
Due to a ``python-memcached`` limitation, it's not possible to distinguish
between stored ``None`` value and a cache miss signified by a return value
of ``None`` on the deprecated ``MemcachedCache`` backend.
``cache.get()`` can take a ``default`` argument. This specifies which value to ``cache.get()`` can take a ``default`` argument. This specifies which value to
return if the object doesn't exist in the cache:: return if the object doesn't exist in the cache::

108
tests/cache/tests.py vendored
View File

@ -11,7 +11,6 @@ import tempfile
import threading import threading
import time import time
import unittest import unittest
import warnings
from pathlib import Path from pathlib import Path
from unittest import mock, skipIf from unittest import mock, skipIf
@ -38,7 +37,7 @@ from django.template.context_processors import csrf
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import ( from django.test import (
RequestFactory, SimpleTestCase, TestCase, TransactionTestCase, RequestFactory, SimpleTestCase, TestCase, TransactionTestCase,
ignore_warnings, override_settings, override_settings,
) )
from django.test.signals import setting_changed from django.test.signals import setting_changed
from django.test.utils import CaptureQueriesContext from django.test.utils import CaptureQueriesContext
@ -46,7 +45,6 @@ from django.utils import timezone, translation
from django.utils.cache import ( from django.utils.cache import (
get_cache_key, learn_cache_key, patch_cache_control, patch_vary_headers, get_cache_key, learn_cache_key, patch_cache_control, patch_vary_headers,
) )
from django.utils.deprecation import RemovedInDjango41Warning
from django.views.decorators.cache import cache_control, cache_page from django.views.decorators.cache import cache_control, cache_page
from .models import Poll, expensive_calculation from .models import Poll, expensive_calculation
@ -285,10 +283,6 @@ class BaseCacheTests:
# A common set of tests to apply to all cache backends # A common set of tests to apply to all cache backends
factory = RequestFactory() factory = RequestFactory()
# RemovedInDjango41Warning: python-memcached doesn't support .get() with
# default.
supports_get_with_default = True
# Some clients raise custom exceptions when .incr() or .decr() are called # Some clients raise custom exceptions when .incr() or .decr() are called
# with a non-integer value. # with a non-integer value.
incr_decr_type_error = TypeError incr_decr_type_error = TypeError
@ -357,10 +351,7 @@ class BaseCacheTests:
cache.set("no_expiry", "here", None) cache.set("no_expiry", "here", None)
self.assertIs(cache.has_key("no_expiry"), True) self.assertIs(cache.has_key("no_expiry"), True)
cache.set('null', None) cache.set('null', None)
self.assertIs( self.assertIs(cache.has_key('null'), True)
cache.has_key('null'),
True if self.supports_get_with_default else False,
)
def test_in(self): def test_in(self):
# The in operator can be used to inspect cache contents # The in operator can be used to inspect cache contents
@ -368,10 +359,7 @@ class BaseCacheTests:
self.assertIn("hello2", cache) self.assertIn("hello2", cache)
self.assertNotIn("goodbye2", cache) self.assertNotIn("goodbye2", cache)
cache.set('null', None) cache.set('null', None)
if self.supports_get_with_default: self.assertIn('null', cache)
self.assertIn('null', cache)
else:
self.assertNotIn('null', cache)
def test_incr(self): def test_incr(self):
# Cache values can be incremented # Cache values can be incremented
@ -965,11 +953,7 @@ class BaseCacheTests:
cache.incr_version('does_not_exist') cache.incr_version('does_not_exist')
cache.set('null', None) cache.set('null', None)
if self.supports_get_with_default: self.assertEqual(cache.incr_version('null'), 2)
self.assertEqual(cache.incr_version('null'), 2)
else:
with self.assertRaises(self.incr_decr_type_error):
cache.incr_version('null')
def test_decr_version(self): def test_decr_version(self):
cache.set('answer', 42, version=2) cache.set('answer', 42, version=2)
@ -996,11 +980,7 @@ class BaseCacheTests:
cache.decr_version('does_not_exist', version=2) cache.decr_version('does_not_exist', version=2)
cache.set('null', None, version=2) cache.set('null', None, version=2)
if self.supports_get_with_default: self.assertEqual(cache.decr_version('null', version=2), 1)
self.assertEqual(cache.decr_version('null', version=2), 1)
else:
with self.assertRaises(self.incr_decr_type_error):
cache.decr_version('null', version=2)
def test_custom_key_func(self): def test_custom_key_func(self):
# Two caches with different key functions aren't visible to each other # Two caches with different key functions aren't visible to each other
@ -1059,11 +1039,8 @@ class BaseCacheTests:
self.assertEqual(cache.get_or_set('projector', 42), 42) self.assertEqual(cache.get_or_set('projector', 42), 42)
self.assertEqual(cache.get('projector'), 42) self.assertEqual(cache.get('projector'), 42)
self.assertIsNone(cache.get_or_set('null', None)) self.assertIsNone(cache.get_or_set('null', None))
if self.supports_get_with_default: # Previous get_or_set() stores None in the cache.
# Previous get_or_set() stores None in the cache. self.assertIsNone(cache.get('null', 'default'))
self.assertIsNone(cache.get('null', 'default'))
else:
self.assertEqual(cache.get('null', 'default'), 'default')
def test_get_or_set_callable(self): def test_get_or_set_callable(self):
def my_callable(): def my_callable():
@ -1073,11 +1050,8 @@ class BaseCacheTests:
self.assertEqual(cache.get_or_set('mykey', my_callable()), 'value') self.assertEqual(cache.get_or_set('mykey', my_callable()), 'value')
self.assertIsNone(cache.get_or_set('null', lambda: None)) self.assertIsNone(cache.get_or_set('null', lambda: None))
if self.supports_get_with_default: # Previous get_or_set() stores None in the cache.
# Previous get_or_set() stores None in the cache. self.assertIsNone(cache.get('null', 'default'))
self.assertIsNone(cache.get('null', 'default'))
else:
self.assertEqual(cache.get('null', 'default'), 'default')
def test_get_or_set_version(self): def test_get_or_set_version(self):
msg = "get_or_set() missing 1 required positional argument: 'default'" msg = "get_or_set() missing 1 required positional argument: 'default'"
@ -1504,70 +1478,6 @@ class BaseMemcachedTests(BaseCacheTests):
self.assertEqual(failing_keys, ['key']) self.assertEqual(failing_keys, ['key'])
# RemovedInDjango41Warning.
MemcachedCache_params = configured_caches.get('django.core.cache.backends.memcached.MemcachedCache')
@ignore_warnings(category=RemovedInDjango41Warning)
@unittest.skipUnless(MemcachedCache_params, "MemcachedCache backend not configured")
@override_settings(CACHES=caches_setting_for_tests(
base=MemcachedCache_params,
exclude=memcached_excluded_caches,
))
class MemcachedCacheTests(BaseMemcachedTests, TestCase):
base_params = MemcachedCache_params
supports_get_with_default = False
incr_decr_type_error = ValueError
def test_memcached_uses_highest_pickle_version(self):
# Regression test for #19810
for cache_key in settings.CACHES:
with self.subTest(cache_key=cache_key):
self.assertEqual(caches[cache_key]._cache.pickleProtocol, pickle.HIGHEST_PROTOCOL)
@override_settings(CACHES=caches_setting_for_tests(
base=MemcachedCache_params,
exclude=memcached_excluded_caches,
OPTIONS={'server_max_value_length': 9999},
))
def test_memcached_options(self):
self.assertEqual(cache._cache.server_max_value_length, 9999)
def test_default_used_when_none_is_set(self):
"""
python-memcached doesn't support default in get() so this test
overrides the one in BaseCacheTests.
"""
cache.set('key_default_none', None)
self.assertEqual(cache.get('key_default_none', default='default'), 'default')
class MemcachedCacheDeprecationTests(SimpleTestCase):
def test_warning(self):
from django.core.cache.backends.memcached import MemcachedCache
# Remove warnings filter on MemcachedCache deprecation warning, added
# in runtests.py.
warnings.filterwarnings(
'error',
'MemcachedCache is deprecated',
category=RemovedInDjango41Warning,
)
try:
msg = (
'MemcachedCache is deprecated in favor of PyMemcacheCache and '
'PyLibMCCache.'
)
with self.assertRaisesMessage(RemovedInDjango41Warning, msg):
MemcachedCache('127.0.0.1:11211', {})
finally:
warnings.filterwarnings(
'ignore',
'MemcachedCache is deprecated',
category=RemovedInDjango41Warning,
)
@unittest.skipUnless(PyLibMCCache_params, "PyLibMCCache backend not configured") @unittest.skipUnless(PyLibMCCache_params, "PyLibMCCache backend not configured")
@override_settings(CACHES=caches_setting_for_tests( @override_settings(CACHES=caches_setting_for_tests(
base=PyLibMCCache_params, base=PyLibMCCache_params,

View File

@ -48,12 +48,6 @@ warnings.simplefilter("error", ResourceWarning)
warnings.simplefilter("error", RuntimeWarning) warnings.simplefilter("error", RuntimeWarning)
# Ignore known warnings in test dependencies. # Ignore known warnings in test dependencies.
warnings.filterwarnings("ignore", "'U' mode is deprecated", DeprecationWarning, module='docutils.io') warnings.filterwarnings("ignore", "'U' mode is deprecated", DeprecationWarning, module='docutils.io')
# RemovedInDjango41Warning: Ignore MemcachedCache deprecation warning.
warnings.filterwarnings(
'ignore',
'MemcachedCache is deprecated',
category=RemovedInDjango41Warning,
)
# Reduce garbage collection frequency to improve performance. Since CPython # Reduce garbage collection frequency to improve performance. Since CPython
# uses refcounting, garbage collection only collects objects with cyclic # uses refcounting, garbage collection only collects objects with cyclic