Repository: jazzband/django-debug-toolbar

Issue:
False postive for debug_toolbar.W006 when manually declaring cached template loader. 
Hi. 

Manually declare template loaders, using the cached loader: 

```
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        # "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
            "loaders": [
                (
                    "django.template.loaders.cached.Loader",
                    [
                        "django.template.loaders.filesystem.Loader",
                        "django.template.loaders.app_directories.Loader",
                    ]
                ),
            ],
        },
    },
]
```

The DDT `APP_DIRS` check then gives a false positive: 

```
?: (debug_toolbar.W006) At least one DjangoTemplates TEMPLATES configuration needs to use django.template.loaders.app_directories.Loader or have APP_DIRS set to True.
        HINT: Include django.template.loaders.app_directories.Loader in ["OPTIONS"]["loaders"]. Alternatively use APP_DIRS=True for at least one django.template.backends.django.DjangoTemplates backend configuration.
```

In: 

https://github.com/jazzband/django-debug-toolbar/blob/de7e19c5ef08445ca592c75dfa3dad22c04992a1/debug_toolbar/apps.py#L26

It looks like checking for and walking `loader.loaders` would resolve this. 🤔
False postive for debug_toolbar.W006 when manually declaring cached template loader. 
Hi. 

Manually declare template loaders, using the cached loader: 

```
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        # "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
            "loaders": [
                (
                    "django.template.loaders.cached.Loader",
                    [
                        "django.template.loaders.filesystem.Loader",
                        "django.template.loaders.app_directories.Loader",
                    ]
                ),
            ],
        },
    },
]
```

The DDT `APP_DIRS` check then gives a false positive: 

```
?: (debug_toolbar.W006) At least one DjangoTemplates TEMPLATES configuration needs to use django.template.loaders.app_directories.Loader or have APP_DIRS set to True.
        HINT: Include django.template.loaders.app_directories.Loader in ["OPTIONS"]["loaders"]. Alternatively use APP_DIRS=True for at least one django.template.backends.django.DjangoTemplates backend configuration.
```

In: 

https://github.com/jazzband/django-debug-toolbar/blob/de7e19c5ef08445ca592c75dfa3dad22c04992a1/debug_toolbar/apps.py#L26

It looks like checking for and walking `loader.loaders` would resolve this. 🤔


Discussion:
Hey

Thanks for the reoprt! This came up already; if `OPTIONS["debug"]` defaults to `DEBUG`; and if `debug` is `False` the cached loader is used automagically. 

See https://docs.djangoproject.com/en/4.2/releases/1.11/#miscellaneous

> The [cached template loader](https://docs.djangoproject.com/en/4.2/ref/templates/api/#django.template.loaders.cached.Loader) is now enabled if [OPTIONS['loaders']](https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-TEMPLATES-OPTIONS) isn’t specified and [OPTIONS['debug']](https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-TEMPLATES-OPTIONS) is False (the latter option defaults to the value of [DEBUG](https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-DEBUG)). This could be backwards-incompatible if you have some [template tags that aren’t thread safe](https://docs.djangoproject.com/en/4.2/howto/custom-template-tags/#template-tag-thread-safety).

See #1550 and #1704 too.

We probably should still fix this but it has been declared as low priority in the past.
Yep, agreed. I don't think it's a biggie. (It's pretty niche.) 

Current work around is: 

```
SILENCED_SYSTEM_CHECKS = [
    "debug_toolbar.W006",  # See jazzband/django-debug-toolbar#1780
]
```

(I may be able to swing to it at some point, soonish) 
Yes, or you (or any readers of this issue, not writing this for you specifically but for those who are drive-by-copy-pasting) could remove the `loaders` entry in the `TEMPLATES` configuration if you're fine with not using the cached loader when debugging. 

This would be sufficient:

```python
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
```

(It's what I use these days.)
Yep 👍. 

Since the autoreloader support for templates was added in Django 4.1, the cached loader is used in development too. As such, there's almost no need to declare loaders for the majority case. 

(I'm only doing it myself because I'm tweaking...)
Hey

Thanks for the reoprt! This came up already; if `OPTIONS["debug"]` defaults to `DEBUG`; and if `debug` is `False` the cached loader is used automagically. 

See https://docs.djangoproject.com/en/4.2/releases/1.11/#miscellaneous

> The [cached template loader](https://docs.djangoproject.com/en/4.2/ref/templates/api/#django.template.loaders.cached.Loader) is now enabled if [OPTIONS['loaders']](https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-TEMPLATES-OPTIONS) isn’t specified and [OPTIONS['debug']](https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-TEMPLATES-OPTIONS) is False (the latter option defaults to the value of [DEBUG](https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-DEBUG)). This could be backwards-incompatible if you have some [template tags that aren’t thread safe](https://docs.djangoproject.com/en/4.2/howto/custom-template-tags/#template-tag-thread-safety).

See #1550 and #1704 too.

We probably should still fix this but it has been declared as low priority in the past.
Yep, agreed. I don't think it's a biggie. (It's pretty niche.) 

Current work around is: 

```
SILENCED_SYSTEM_CHECKS = [
    "debug_toolbar.W006",  # See jazzband/django-debug-toolbar#1780
]
```

(I may be able to swing to it at some point, soonish) 
Yes, or you (or any readers of this issue, not writing this for you specifically but for those who are drive-by-copy-pasting) could remove the `loaders` entry in the `TEMPLATES` configuration if you're fine with not using the cached loader when debugging. 

This would be sufficient:

```python
TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "DIRS": [],
        "APP_DIRS": True,
        "OPTIONS": {
            "context_processors": [
                "django.template.context_processors.debug",
                "django.template.context_processors.request",
                "django.contrib.auth.context_processors.auth",
                "django.contrib.messages.context_processors.messages",
            ],
        },
    },
]
```

(It's what I use these days.)
Yep 👍. 

Since the autoreloader support for templates was added in Django 4.1, the cached loader is used in development too. As such, there's almost no need to declare loaders for the majority case. 

(I'm only doing it myself because I'm tweaking...)

Resolving patch:
diff --git a/debug_toolbar/apps.py b/debug_toolbar/apps.py
--- a/debug_toolbar/apps.py
+++ b/debug_toolbar/apps.py
@@ -32,8 +32,26 @@ def check_template_config(config):
     included in the loaders.
     If custom loaders are specified, then APP_DIRS must be True.
     """
+
+    def flat_loaders(loaders):
+        """
+        Recursively flatten the settings list of template loaders.
+
+        Check for (loader, [child_loaders]) tuples.
+        Django's default cached loader uses this pattern.
+        """
+        for loader in loaders:
+            if isinstance(loader, tuple):
+                yield loader[0]
+                yield from flat_loaders(loader[1])
+            else:
+                yield loader
+
     app_dirs = config.get("APP_DIRS", False)
     loaders = config.get("OPTIONS", {}).get("loaders", None)
+    if loaders:
+        loaders = list(flat_loaders(loaders))
+
     # By default the app loader is included.
     has_app_loaders = (
         loaders is None or "django.template.loaders.app_directories.Loader" in loaders


Test patch:
diff --git a/tests/test_checks.py b/tests/test_checks.py
--- a/tests/test_checks.py
+++ b/tests/test_checks.py
@@ -198,3 +198,32 @@ def test_check_w006_invalid(self):
     )
     def test_check_w006_valid(self):
         self.assertEqual(run_checks(), [])
+
+    @override_settings(
+        TEMPLATES=[
+            {
+                "NAME": "use_loaders",
+                "BACKEND": "django.template.backends.django.DjangoTemplates",
+                "APP_DIRS": False,
+                "OPTIONS": {
+                    "context_processors": [
+                        "django.template.context_processors.debug",
+                        "django.template.context_processors.request",
+                        "django.contrib.auth.context_processors.auth",
+                        "django.contrib.messages.context_processors.messages",
+                    ],
+                    "loaders": [
+                        (
+                            "django.template.loaders.cached.Loader",
+                            [
+                                "django.template.loaders.filesystem.Loader",
+                                "django.template.loaders.app_directories.Loader",
+                            ],
+                        ),
+                    ],
+                },
+            },
+        ]
+    )
+    def test_check_w006_valid_nested_loaders(self):
+        self.assertEqual(run_checks(), [])