diff --git a/django/core/cache/__init__.py b/django/core/cache/__init__.py index 1da58153f4..05ef3897d0 100644 --- a/django/core/cache/__init__.py +++ b/django/core/cache/__init__.py @@ -50,9 +50,8 @@ cache = ConnectionProxy(caches, DEFAULT_CACHE_ALIAS) def close_caches(**kwargs): - # Some caches -- python-memcached in particular -- need to do a cleanup at the - # end of a request cycle. If not implemented in a particular backend - # cache.close is a no-op + # Some caches need to do a cleanup at the end of a request cycle. If not + # implemented in a particular backend cache.close() is a no-op. for cache in caches.all(): cache.close() diff --git a/django/core/cache/backends/memcached.py b/django/core/cache/backends/memcached.py index cc5648bb1c..9a717359b8 100644 --- a/django/core/cache/backends/memcached.py +++ b/django/core/cache/backends/memcached.py @@ -3,10 +3,12 @@ import pickle import re import time +import warnings from django.core.cache.backends.base import ( DEFAULT_TIMEOUT, BaseCache, InvalidCacheKey, memcache_key_warnings, ) +from django.utils.deprecation import RemovedInDjango41Warning from django.utils.functional import cached_property @@ -164,6 +166,11 @@ class BaseMemcachedCache(BaseCache): class MemcachedCache(BaseMemcachedCache): "An implementation of a cache binding using python-memcached" 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 diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 06ea0fbbc0..b53552bb0a 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -29,6 +29,8 @@ details on these changes. * ``TransactionTestCase.assertQuerysetEqual()` will no longer automatically call ``repr()`` on a queryset when compared to string values. +* ``django.core.cache.backends.memcached.MemcachedCache`` will be removed. + .. _deprecation-removed-in-4.0: 4.0 diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 606ef98744..bfa62f1508 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -156,9 +156,8 @@ The cache backend to use. The built-in cache backends are: * ``'django.core.cache.backends.dummy.DummyCache'`` * ``'django.core.cache.backends.filebased.FileBasedCache'`` * ``'django.core.cache.backends.locmem.LocMemCache'`` -* ``'django.core.cache.backends.memcached.MemcachedCache'`` -* ``'django.core.cache.backends.memcached.PyLibMCCache'`` * ``'django.core.cache.backends.memcached.PyMemcacheCache'`` +* ``'django.core.cache.backends.memcached.PyLibMCCache'`` You can use a cache backend that doesn't ship with Django by setting :setting:`BACKEND ` to a fully-qualified path of a cache diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 2ab8546026..d5047dfe60 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -660,3 +660,8 @@ Miscellaneous ``TransactionTestCase.assertQuerysetEqual()``, when compared to string values, is deprecated. If you need the previous behavior, explicitly set ``transform`` to ``repr``. + +* The ``django.core.cache.backends.memcached.MemcachedCache`` backend is + deprecated as ``python-memcached`` has some problems and seems to be + unmaintained. Use ``django.core.cache.backends.memcached.PyMemcacheCache`` + or ``django.core.cache.backends.memcached.PyLibMCCache`` instead. diff --git a/docs/topics/cache.txt b/docs/topics/cache.txt index ce9a982a23..66551fc8fe 100644 --- a/docs/topics/cache.txt +++ b/docs/topics/cache.txt @@ -77,18 +77,16 @@ database or filesystem usage. After installing Memcached itself, you'll need to install a Memcached binding. There are several Python Memcached bindings available; the -three most common are `python-memcached`_, `pylibmc`_, and `pymemcache`_. +two supported by Django are `pylibmc`_ and `pymemcache`_. -.. _`python-memcached`: https://pypi.org/project/python-memcached/ .. _`pylibmc`: https://pypi.org/project/pylibmc/ .. _`pymemcache`: https://pypi.org/project/pymemcache/ To use Memcached with Django: * Set :setting:`BACKEND ` to - ``django.core.cache.backends.memcached.MemcachedCache``, - ``django.core.cache.backends.memcached.PyLibMCCache``, or - ``django.core.cache.backends.memcached.PyMemcacheCache`` (depending on your + ``django.core.cache.backends.memcached.PyMemcacheCache`` or + ``django.core.cache.backends.memcached.PyLibMCCache`` (depending on your chosen memcached binding) * Set :setting:`LOCATION ` to ``ip:port`` values, @@ -97,21 +95,21 @@ To use Memcached with Django: ``path`` is the path to a Memcached Unix socket file. In this example, Memcached is running on localhost (127.0.0.1) port 11211, using -the ``python-memcached`` binding:: +the ``pymemcache`` binding:: CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': '127.0.0.1:11211', } } In this example, Memcached is available through a local Unix socket file -:file:`/tmp/memcached.sock` using the ``python-memcached`` binding:: +:file:`/tmp/memcached.sock` using the ``pymemcache`` binding:: CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': 'unix:/tmp/memcached.sock', } } @@ -129,7 +127,7 @@ address 172.19.26.240 and 172.19.26.242, both on port 11211:: CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': [ '172.19.26.240:11211', '172.19.26.242:11211', @@ -143,7 +141,7 @@ on the IP addresses 172.19.26.240 (port 11211), 172.19.26.242 (port 11212), and CACHES = { 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', 'LOCATION': [ '172.19.26.240:11211', '172.19.26.242:11212', @@ -165,6 +163,12 @@ particularly temporary. The ``PyMemcacheCache`` backend was added. +.. deprecated:: 3.2 + + The ``MemcachedCache`` backend is deprecated as ``python-memcached`` has + some problems and seems to be unmaintained. Use ``PyMemcacheCache`` or + ``PyLibMCCache`` instead. + .. _database-caching: Database caching @@ -452,19 +456,6 @@ of 60 seconds, and a maximum capacity of 1000 items:: } } -Here's an example configuration for a ``python-memcached`` based backend with -an object size limit of 2MB:: - - CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': '127.0.0.1:11211', - 'OPTIONS': { - 'server_max_value_length': 1024 * 1024 * 2, - } - } - } - Here's an example configuration for a ``pylibmc`` based backend that enables the binary protocol, SASL authentication, and the ``ketama`` behavior mode:: diff --git a/tests/cache/tests.py b/tests/cache/tests.py index df0483d26e..2853ada233 100644 --- a/tests/cache/tests.py +++ b/tests/cache/tests.py @@ -11,6 +11,7 @@ import tempfile import threading import time import unittest +import warnings from pathlib import Path from unittest import mock, skipIf @@ -35,13 +36,14 @@ from django.template.context_processors import csrf from django.template.response import TemplateResponse from django.test import ( RequestFactory, SimpleTestCase, TestCase, TransactionTestCase, - override_settings, + ignore_warnings, override_settings, ) from django.test.signals import setting_changed from django.utils import timezone, translation from django.utils.cache import ( 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 .models import Poll, expensive_calculation @@ -1276,7 +1278,6 @@ configured_caches = {} for _cache_params in settings.CACHES.values(): configured_caches[_cache_params['BACKEND']] = _cache_params -MemcachedCache_params = configured_caches.get('django.core.cache.backends.memcached.MemcachedCache') PyLibMCCache_params = configured_caches.get('django.core.cache.backends.memcached.PyLibMCCache') PyMemcacheCache_params = configured_caches.get('django.core.cache.backends.memcached.PyMemcacheCache') @@ -1349,10 +1350,7 @@ class BaseMemcachedTests(BaseCacheTests): # By default memcached allows objects up to 1MB. For the cache_db session # backend to always use the current session, memcached needs to delete # the old key if it fails to set. - # pylibmc doesn't seem to have SERVER_MAX_VALUE_LENGTH as far as I can - # tell from a quick check of its source code. This is falling back to - # the default value exposed by python-memcached on my system. - max_value_length = getattr(cache._lib, 'SERVER_MAX_VALUE_LENGTH', 1048576) + max_value_length = 2 ** 20 cache.set('small_value', 'a') self.assertEqual(cache.get('small_value'), 'a') @@ -1361,11 +1359,10 @@ class BaseMemcachedTests(BaseCacheTests): try: cache.set('small_value', large_value) except Exception: - # Some clients (e.g. pylibmc) raise when the value is too large, - # while others (e.g. python-memcached) intentionally return True - # indicating success. This test is primarily checking that the key - # was deleted, so the return/exception behavior for the set() - # itself is not important. + # Most clients (e.g. pymemcache or pylibmc) raise when the value is + # too large. This test is primarily checking that the key was + # deleted, so the return/exception behavior for the set() itself is + # not important. pass # small_value should be deleted, or set if configured to accept larger values value = cache.get('small_value') @@ -1390,6 +1387,11 @@ class BaseMemcachedTests(BaseCacheTests): 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, @@ -1421,6 +1423,32 @@ class MemcachedCacheTests(BaseMemcachedTests, TestCase): 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") @override_settings(CACHES=caches_setting_for_tests( base=PyLibMCCache_params, diff --git a/tests/requirements/py3.txt b/tests/requirements/py3.txt index a51bda55ad..98f9ae94fd 100644 --- a/tests/requirements/py3.txt +++ b/tests/requirements/py3.txt @@ -9,6 +9,7 @@ Pillow >= 6.2.0 # pylibmc/libmemcached can't be built on Windows. pylibmc; sys.platform != 'win32' pymemcache >= 3.4.0 +# RemovedInDjango41Warning. python-memcached >= 1.59 pytz pywatchman; sys.platform != 'win32' diff --git a/tests/runtests.py b/tests/runtests.py index fa933306cd..ad282ce782 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -47,6 +47,12 @@ warnings.simplefilter("error", ResourceWarning) warnings.simplefilter("error", RuntimeWarning) # Ignore known warnings in test dependencies. warnings.filterwarnings("ignore", "'U' mode is deprecated", DeprecationWarning, module='docutils.io') +# RemovedInDjango41Warning: Ignore MemcachedCache deprecation warning. +warnings.filterwarnings( + 'ignore', + 'MemcachedCache is deprecated', + category=RemovedInDjango41Warning, +) RUNTESTS_DIR = os.path.abspath(os.path.dirname(__file__))