<NME> cache.py
<BEF> import functools
import time

from asgiref.local import Local
from django.conf import settings
try:
    from django.utils.connection import ConnectionProxy
except ImportError:
    # Django 3.1 and below doesn't have a ConnectionProxy
    pass

from django.conf import settings
from django.core import cache
# the Commands table in the panel content.
WRAPPED_CACHE_METHODS = [
    "add",
    "get",
    "set",
    "get_or_set",
    "touch",
    "delete",
    "clear",
    "get_many",
    "set_many",
    "delete_many",
    "has_key",
    "incr",
    "decr",
    "incr_version",
    "decr_version",
]


class CachePanel(Panel):
    """
    Panel that displays the cache statistics.
    """

    template = "debug_toolbar/panels/cache.html"

    _context_locals = Local()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.total_time = 0
        self.hits = 0
        self.misses = 0
        self.calls = []
        self.counts = {name: 0 for name in WRAPPED_CACHE_METHODS}

    @classmethod
    def current_instance(cls):
        """
        Return the currently enabled CachePanel instance or None.

        If a request is in process with a CachePanel enabled, this will return that
        panel (based on the current thread or async task).  Otherwise it will return
        None.
        """
        return getattr(cls._context_locals, "current_instance", None)

    @classmethod
    def ready(cls):
        if not hasattr(CacheHandler, "_djdt_patched"):
            # Wrap the CacheHander.create_connection() method to monkey patch any new
            # cache connections that are opened while instrumentation is enabled.  In
            # the interests of thread safety, this is done once at startup time and
            # never removed.
            original_method = CacheHandler.create_connection

            @functools.wraps(original_method)
            def wrapper(self, alias):
                cache = original_method(self, alias)
                panel = cls.current_instance()
                if panel is not None:
                    panel._monkey_patch_cache(cache)
                return cache

            CacheHandler.create_connection = wrapper
            CacheHandler._djdt_patched = True

    def _store_call_info(
        self,
        name,
        time_taken,
        return_value,
        args,
        kwargs,
        trace,
        template_info,
        backend,
    ):
        if name == "get" or name == "get_or_set":
            if return_value is None:
                self.misses += 1
            else:
                self.hits += 1
        elif name == "get_many":
            if "keys" in kwargs:
                keys = kwargs["keys"]
            else:
                keys = args[0]
            self.hits += len(return_value)
            self.misses += len(keys) - len(return_value)
        time_taken *= 1000

        self.total_time += time_taken
        self.counts[name] += 1
        self.calls.append(
            {
                "time": time_taken,
                "name": name,
                "args": args,
                "kwargs": kwargs,
                "trace": render_stacktrace(trace),
                "template_info": template_info,
                "backend": backend,
            }
        )

    def _record_call(self, cache, name, original_method, args, kwargs):
        # Some cache backends implement certain cache methods in terms of other cache
        # methods (e.g. get_or_set() in terms of get() and add()).  In order to only
        # record the calls made directly by the user code, set the _djdt_recording flag
        # here to cause the monkey patched cache methods to skip recording additional
        # calls made during the course of this call.
        cache._djdt_recording = True
        t = time.time()
        value = original_method(*args, **kwargs)
        t = time.time() - t
        cache._djdt_recording = False

        self._store_call_info(
            name=name,
            time_taken=t,
            return_value=value,
            args=args,
            kwargs=kwargs,
            trace=get_stack_trace(skip=2),
            template_info=get_template_info(),
            backend=cache,
        )
        return value

    def _monkey_patch_method(self, cache, name):
        original_method = getattr(cache, name)

        @functools.wraps(original_method)
        def wrapper(*args, **kwargs):
            # If this call is being made as part of the implementation of another cache
            # method, don't record it.
            if cache._djdt_recording:
                return original_method(*args, **kwargs)
            else:
                return self._record_call(cache, name, original_method, args, kwargs)

        wrapper._djdt_wrapped = original_method
        setattr(cache, name, wrapper)

    def _monkey_patch_cache(self, cache):
        if not hasattr(cache, "_djdt_patched"):
            for name in WRAPPED_CACHE_METHODS:
                self._monkey_patch_method(cache, name)
            cache._djdt_patched = True
            cache._djdt_recording = False

    @staticmethod
    def _unmonkey_patch_cache(cache):
        if hasattr(cache, "_djdt_patched"):
            for name in WRAPPED_CACHE_METHODS:
                original_method = getattr(cache, name)._djdt_wrapped
                if original_method.__func__ == getattr(cache.__class__, name):
                    delattr(cache, name)
                else:
                    setattr(cache, name, original_method)
            del cache._djdt_patched
            del cache._djdt_recording

    # Implement the Panel API

    nav_title = _("Cache")

    @property
    def nav_subtitle(self):
        cache_calls = len(self.calls)
        return ngettext(
            "%(cache_calls)d call in %(time).2fms",
            "%(cache_calls)d calls in %(time).2fms",
            cache_calls,
        ) % {"cache_calls": cache_calls, "time": self.total_time}

    @property
    def title(self):
        count = len(getattr(settings, "CACHES", ["default"]))
        return ngettext(
            "Cache calls from %(count)d backend",
            "Cache calls from %(count)d backends",
            count,
        ) % {"count": count}

    def enable_instrumentation(self):
        # Monkey patch all open cache connections.  Django maintains cache connections
        # on a per-thread/async task basis, so this will not affect any concurrent
        # requests.  The monkey patch of CacheHander.create_connection() installed in
        # the .ready() method will ensure that any new cache connections that get opened
        # during this request will also be monkey patched.
        for cache in caches.all(initialized_only=True):
            self._monkey_patch_cache(cache)
        # Mark this panel instance as the current one for the active thread/async task
        # context.  This will be used by the CacheHander.create_connection() monkey
        # patch.
        self._context_locals.current_instance = self

    def disable_instrumentation(self):
        if hasattr(self._context_locals, "current_instance"):
            del self._context_locals.current_instance
        for cache in caches.all(initialized_only=True):
            self._unmonkey_patch_cache(cache)

    def generate_stats(self, request, response):
        self.record_stats(
            {
                "total_calls": len(self.calls),
                "calls": self.calls,
                "total_time": self.total_time,
                "hits": self.hits,
                "misses": self.misses,
                "counts": self.counts,
            }
        )

    def generate_server_timing(self, request, response):
        stats = self.get_stats()
        value = stats.get("total_time", 0)
        title = "Cache {} Calls".format(stats.get("total_calls", 0))
        self.record_server_timing("total_time", title, value)
        else:
            cache.caches = CacheHandlerPatch()

        try:
            cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)
        except NameError:
            pass

    def disable_instrumentation(self):
        cache.caches = original_caches

<MSG> Avoid a too broad try...except NameError clause (#1443)

Refs #1437
<DFF> @@ -6,8 +6,7 @@ from collections import OrderedDict
 try:
     from django.utils.connection import ConnectionProxy
 except ImportError:
-    # Django 3.1 and below doesn't have a ConnectionProxy
-    pass
+    ConnectionProxy = None
 
 from django.conf import settings
 from django.core import cache
@@ -257,10 +256,9 @@ class CachePanel(Panel):
         else:
             cache.caches = CacheHandlerPatch()
 
-        try:
+        # Wrap the patched cache inside Django's ConnectionProxy
+        if ConnectionProxy:
             cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)
-        except NameError:
-            pass
 
     def disable_instrumentation(self):
         cache.caches = original_caches

{'commit': "diff --git a/debug_toolbar/panels/cache.py b/debug_toolbar/panels/cache.py\nindex e7f3f46..b4c4861 100644\n--- a/debug_toolbar/panels/cache.py\n+++ b/debug_toolbar/panels/cache.py\n@@ -6,8 +6,7 @@ from collections import OrderedDict\n try:\n     from django.utils.connection import ConnectionProxy\n except ImportError:\n-    # Django 3.1 and below doesn't have a ConnectionProxy\n-    pass\n+    ConnectionProxy = None\n \n from django.conf import settings\n from django.core import cache\n@@ -257,10 +256,9 @@ class CachePanel(Panel):\n         else:\n             cache.caches = CacheHandlerPatch()\n \n-        try:\n+        # Wrap the patched cache inside Django's ConnectionProxy\n+        if ConnectionProxy:\n             cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)\n-        except NameError:\n-            pass\n \n     def disable_instrumentation(self):\n         cache.caches = original_caches\n", 'commit_subject': 'Avoid a too broad try...except NameError clause (#1443)', 'repo_name': 'jazzband/django-debug-toolbar', 'committer': {'name': 'GitHub', 'email': '1505422b2465e9a84f6fdfaa161078890c593f06@github.com', 'time_sec': '1611432867', 'tz_offset': '60', 'date': {'seconds': '1611432867'}}, 'commit_tree': '11a17bd6b01d6e2873b2259ecf6b6c9ce38e4b51', 'commit_message': 'Avoid a too broad try...except NameError clause (#1443)\n\nRefs #1437', 'language_name': 'JavaScript', 'license': 'bsd-3-clause', 'hunks': "@@ -6,8 +6,7 @@ from collections import OrderedDict\n try:\n     from django.utils.connection import ConnectionProxy\n except ImportError:\n-    # Django 3.1 and below doesn't have a ConnectionProxy\n-    pass\n+    ConnectionProxy = None\n \n from django.conf import settings\n from django.core import cache\n@@ -257,10 +256,9 @@ class CachePanel(Panel):\n         else:\n             cache.caches = CacheHandlerPatch()\n \n-        try:\n+        # Wrap the patched cache inside Django's ConnectionProxy\n+        if ConnectionProxy:\n             cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)\n-        except NameError:\n-            pass\n \n     def disable_instrumentation(self):\n         cache.caches = original_caches\n", 'addition_count': 3, 'deletion_count': 5, 'src_file': 'a/debug_toolbar/panels/cache.py', 'tgt_file': 'b/debug_toolbar/panels/cache.py', 'file_extension': '.py', 'before_file': 'import functools\nimport time\n\nfrom asgiref.local import Local\nfrom django.conf import settings\ntry:\n    from django.utils.connection import ConnectionProxy\nexcept ImportError:\n    # Django 3.1 and below doesn\'t have a ConnectionProxy\n    pass\n\nfrom django.conf import settings\nfrom django.core import cache\n# the Commands table in the panel content.\nWRAPPED_CACHE_METHODS = [\n    "add",\n    "get",\n    "set",\n    "get_or_set",\n    "touch",\n    "delete",\n    "clear",\n    "get_many",\n    "set_many",\n    "delete_many",\n    "has_key",\n    "incr",\n    "decr",\n    "incr_version",\n    "decr_version",\n]\n\n\nclass CachePanel(Panel):\n    """\n    Panel that displays the cache statistics.\n    """\n\n    template = "debug_toolbar/panels/cache.html"\n\n    _context_locals = Local()\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.total_time = 0\n        self.hits = 0\n        self.misses = 0\n        self.calls = []\n        self.counts = {name: 0 for name in WRAPPED_CACHE_METHODS}\n\n    @classmethod\n    def current_instance(cls):\n        """\n        Return the currently enabled CachePanel instance or None.\n\n        If a request is in process with a CachePanel enabled, this will return that\n        panel (based on the current thread or async task).  Otherwise it will return\n        None.\n        """\n        return getattr(cls._context_locals, "current_instance", None)\n\n    @classmethod\n    def ready(cls):\n        if not hasattr(CacheHandler, "_djdt_patched"):\n            # Wrap the CacheHander.create_connection() method to monkey patch any new\n            # cache connections that are opened while instrumentation is enabled.  In\n            # the interests of thread safety, this is done once at startup time and\n            # never removed.\n            original_method = CacheHandler.create_connection\n\n            @functools.wraps(original_method)\n            def wrapper(self, alias):\n                cache = original_method(self, alias)\n                panel = cls.current_instance()\n                if panel is not None:\n                    panel._monkey_patch_cache(cache)\n                return cache\n\n            CacheHandler.create_connection = wrapper\n            CacheHandler._djdt_patched = True\n\n    def _store_call_info(\n        self,\n        name,\n        time_taken,\n        return_value,\n        args,\n        kwargs,\n        trace,\n        template_info,\n        backend,\n    ):\n        if name == "get" or name == "get_or_set":\n            if return_value is None:\n                self.misses += 1\n            else:\n                self.hits += 1\n        elif name == "get_many":\n            if "keys" in kwargs:\n                keys = kwargs["keys"]\n            else:\n                keys = args[0]\n            self.hits += len(return_value)\n            self.misses += len(keys) - len(return_value)\n        time_taken *= 1000\n\n        self.total_time += time_taken\n        self.counts[name] += 1\n        self.calls.append(\n            {\n                "time": time_taken,\n                "name": name,\n                "args": args,\n                "kwargs": kwargs,\n                "trace": render_stacktrace(trace),\n                "template_info": template_info,\n                "backend": backend,\n            }\n        )\n\n    def _record_call(self, cache, name, original_method, args, kwargs):\n        # Some cache backends implement certain cache methods in terms of other cache\n        # methods (e.g. get_or_set() in terms of get() and add()).  In order to only\n        # record the calls made directly by the user code, set the _djdt_recording flag\n        # here to cause the monkey patched cache methods to skip recording additional\n        # calls made during the course of this call.\n        cache._djdt_recording = True\n        t = time.time()\n        value = original_method(*args, **kwargs)\n        t = time.time() - t\n        cache._djdt_recording = False\n\n        self._store_call_info(\n            name=name,\n            time_taken=t,\n            return_value=value,\n            args=args,\n            kwargs=kwargs,\n            trace=get_stack_trace(skip=2),\n            template_info=get_template_info(),\n            backend=cache,\n        )\n        return value\n\n    def _monkey_patch_method(self, cache, name):\n        original_method = getattr(cache, name)\n\n        @functools.wraps(original_method)\n        def wrapper(*args, **kwargs):\n            # If this call is being made as part of the implementation of another cache\n            # method, don\'t record it.\n            if cache._djdt_recording:\n                return original_method(*args, **kwargs)\n            else:\n                return self._record_call(cache, name, original_method, args, kwargs)\n\n        wrapper._djdt_wrapped = original_method\n        setattr(cache, name, wrapper)\n\n    def _monkey_patch_cache(self, cache):\n        if not hasattr(cache, "_djdt_patched"):\n            for name in WRAPPED_CACHE_METHODS:\n                self._monkey_patch_method(cache, name)\n            cache._djdt_patched = True\n            cache._djdt_recording = False\n\n    @staticmethod\n    def _unmonkey_patch_cache(cache):\n        if hasattr(cache, "_djdt_patched"):\n            for name in WRAPPED_CACHE_METHODS:\n                original_method = getattr(cache, name)._djdt_wrapped\n                if original_method.__func__ == getattr(cache.__class__, name):\n                    delattr(cache, name)\n                else:\n                    setattr(cache, name, original_method)\n            del cache._djdt_patched\n            del cache._djdt_recording\n\n    # Implement the Panel API\n\n    nav_title = _("Cache")\n\n    @property\n    def nav_subtitle(self):\n        cache_calls = len(self.calls)\n        return ngettext(\n            "%(cache_calls)d call in %(time).2fms",\n            "%(cache_calls)d calls in %(time).2fms",\n            cache_calls,\n        ) % {"cache_calls": cache_calls, "time": self.total_time}\n\n    @property\n    def title(self):\n        count = len(getattr(settings, "CACHES", ["default"]))\n        return ngettext(\n            "Cache calls from %(count)d backend",\n            "Cache calls from %(count)d backends",\n            count,\n        ) % {"count": count}\n\n    def enable_instrumentation(self):\n        # Monkey patch all open cache connections.  Django maintains cache connections\n        # on a per-thread/async task basis, so this will not affect any concurrent\n        # requests.  The monkey patch of CacheHander.create_connection() installed in\n        # the .ready() method will ensure that any new cache connections that get opened\n        # during this request will also be monkey patched.\n        for cache in caches.all(initialized_only=True):\n            self._monkey_patch_cache(cache)\n        # Mark this panel instance as the current one for the active thread/async task\n        # context.  This will be used by the CacheHander.create_connection() monkey\n        # patch.\n        self._context_locals.current_instance = self\n\n    def disable_instrumentation(self):\n        if hasattr(self._context_locals, "current_instance"):\n            del self._context_locals.current_instance\n        for cache in caches.all(initialized_only=True):\n            self._unmonkey_patch_cache(cache)\n\n    def generate_stats(self, request, response):\n        self.record_stats(\n            {\n                "total_calls": len(self.calls),\n                "calls": self.calls,\n                "total_time": self.total_time,\n                "hits": self.hits,\n                "misses": self.misses,\n                "counts": self.counts,\n            }\n        )\n\n    def generate_server_timing(self, request, response):\n        stats = self.get_stats()\n        value = stats.get("total_time", 0)\n        title = "Cache {} Calls".format(stats.get("total_calls", 0))\n        self.record_server_timing("total_time", title, value)\n        else:\n            cache.caches = CacheHandlerPatch()\n\n        try:\n            cache.cache = ConnectionProxy(cache.caches, DEFAULT_CACHE_ALIAS)\n        except NameError:\n            pass\n\n    def disable_instrumentation(self):\n        cache.caches = original_caches\n', 'message': 'Avoid a too broad try...except NameError clause (#1443)\n\nRefs #1437', 'author_name': 'GitHub', 'lang': 'py'}