Repository: carltongibson/django-template-partials (language: python)

Issue:
Add wrap_loaders() helper. 
Currently one has to override the template loaders manually in order to add the partials loader. 
That's rubbish. 

Much better would be a `wrap_loaders()` helper that could do this for you. 

* Should be called from an `AppConfig.ready()`
* The `template_partials` app should provide a default `AppConfig` that does this. 
    *  People would just then add `template_partials` to `INSTALLED_APPS` and they'd be done. 
* For completeness, an optional `AppConfig` that doesn't do this can be added, so that folks can opt-out.

(I'm happy to elaborate if needed.)


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 8d446eb..e0ae98d 100644
--- a/README.md
+++ b/README.md
@@ -21,32 +21,21 @@ Install with pip:
 pip install django-template-partials
 ```
 
-Then set up your project:
+Then set up your project, as per this example:
 
 ```python
-# Install app and configure loader.
-default_loaders = [
-    "django.template.loaders.filesystem.Loader",
-    "django.template.loaders.app_directories.Loader",
-]
-cached_loaders = [("django.template.loaders.cached.Loader", default_loaders)]
-partial_loaders = [("template_partials.loader.Loader", cached_loaders)]
 TEMPLATES = [
     {
         "BACKEND": "django.template.backends.django.DjangoTemplates",
         "DIRS": [],
-        # Comment this out when manually defining loaders.
-        # "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",
+               ...,
+            ],
+            "builtins": [
+                # If you want to avoid doing {% load partials %} in your templates
+                "template_partials.templatetags.partials"
             ],
-            "debug": True,
-            # TODO: Add wrap_loaded function to the called from an AppConfig.ready().
-            "loaders": partial_loaders,
         },
     },
 ]
@@ -58,8 +47,8 @@ INSTALLED_APPS = [
 
 ## Usage
 
-Load the `partials` tags and define a re-usable partial at the top of your
-template:
+Load the `partials` tags (if you haven't specified them via "builtins" as suggested above)
+and define a re-usable partial at the top of your template:
 
 ```html
 {% load partials %}
@@ -91,7 +80,8 @@ CONTENT
 {% endblock main %}
 ```
 
-`django-template-partials` is also integrated with the template loader, so you can pass a template plus a partial name to the loader to have just that part rendered:
+`django-template-partials` is also integrated with the template loader, so you can pass a template plus a partial name to the loader to 
+have just that part rendered:
 
 ```python
 self.template_name = "example.html#test-partial"
@@ -99,6 +89,62 @@ self.template_name = "example.html#test-partial"
 
 The rest of your view logic remains the same.
 
+## Automatic configuration of template loader integration
+
+By default, if you just add `"template_partials"` to your `INSTALLED_APPS`, a template loader which
+can handle partials will be automatically configured for any template engine with a backend of
+`"django.template.backends.django.DjangoTemplates"`.
+
+If you want to avoid this autoconfiguration, set up your `INSTALLED_APPS` like this:
+
+```python
+INSTALLED_APPS = [
+    "template_partials.apps.SimpleAppConfig",
+    ...,
+]
+```
+If you do this, you will need to configure the template loader yourself. For this, a `wrap_loaders()` function is
+provided, and it can be used to configure any specific template engine instance with a loader that handles partials.
+For example, you can use the [`NAME`](https://docs.djangoproject.com/en/4.2/ref/settings/#std-setting-TEMPLATES-NAME)
+key:
+
+```python
+from template_partials.apps import wrap_loaders
+
+TEMPLATES = [
+    ...,
+    {
+        "BACKEND": "...",
+        "NAME": "myname",
+        "OPTIONS": {
+           ...,
+        },
+    },
+    ...,
+]
+
+wrap_loaders("myname")
+```
+Note that if `NAME` isn't provided, the penultimate element of the `BACKEND` value is used - for example,
+`"django.template.backends.django.DjangoTemplates"` would be equivalent to a `NAME` of `"django"`.
+
+What `wrap_loaders` does is something like this:
+
+```python
+from django.conf import settings
+
+default_loaders = [
+    "django.template.loaders.filesystem.Loader",
+    "django.template.loaders.app_directories.Loader",
+]
+cached_loaders = [("django.template.loaders.cached.Loader", default_loaders)]
+partial_loaders = [("template_partials.loader.Loader", cached_loaders)]
+
+settings.TEMPLATES[...]['OPTIONS']['loaders'] = partial_loaders
+```
+
+where `TEMPLATES[...]` is the entry in `TEMPLATES` with the `NAME` matching what's passed to `wrap_loaders()`.
+
 ## Documentation
 
 Fuller docs and write up still ***COMING SOON***, but the talk explains most of
@@ -122,13 +168,13 @@ source .venv/bin/activate
 python -m pip install -e .[tests]
 ```
 
-Then you can run the tests with the `just` command runner: 
+Then you can run the tests with the `just` command runner:
 
 ```sh
 just test
 ```
 
-Or with coverage: 
+Or with coverage:
 
 ```sh
 just coverage
diff --git a/src/template_partials/apps.py b/src/template_partials/apps.py
new file mode 100644
index 0000000..64b504c
--- /dev/null
+++ b/src/template_partials/apps.py
@@ -0,0 +1,46 @@
+"""
+django-template-partials
+
+App configuration to set up a partials loader automatically.
+"""
+from django.apps import AppConfig
+from django.conf import settings
+
+def wrap_loaders(name):
+    for template_config in settings.TEMPLATES:
+        engine_name = template_config.get('NAME')
+        if not engine_name:
+            engine_name = template_config["BACKEND"].split(".")[-2]
+        if engine_name == name:
+            loaders = template_config.setdefault("OPTIONS", {}).get("loaders", [])
+            already_configured = (loaders and isinstance(loaders, (list, tuple)) and
+                                  isinstance(loaders[0], tuple) and
+                                  loaders[0][0] == "template_partials.loader.Loader")
+            if not already_configured:
+                template_config.pop("APP_DIRS", None)
+                default_loaders = [
+                    "django.template.loaders.filesystem.Loader",
+                    "django.template.loaders.app_directories.Loader",
+                ]
+                cached_loaders = [("django.template.loaders.cached.Loader", default_loaders)]
+                partial_loaders = [("template_partials.loader.Loader", cached_loaders)]
+                template_config['OPTIONS']['loaders'] = partial_loaders
+            break
+
+class LoaderAppConfig(AppConfig):
+    """
+    This, the default configuration, does the automatic setup of a partials loader.
+    """
+    name = "template_partials"
+    default = True
+
+    def ready(self):
+        wrap_loaders("django")
+
+
+class SimpleAppConfig(AppConfig):
+    """
+    This, the non-default configuration, allows the user to opt-out of the automatic configuration. They just need to
+    add "template_partials.apps.SimpleAppConfig" to INSTALLED_APPS instead of "template_partials".
+    """
+    name = "template_partials"


Test patch:
diff --git a/tests/settings.py b/tests/settings.py
index f3195b7..04da95b 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -1,15 +1,8 @@
-# Install app and configure loader.
-default_loaders = [
-    "django.template.loaders.filesystem.Loader",
-    "django.template.loaders.app_directories.Loader",
-]
-cached_loaders = [("django.template.loaders.cached.Loader", default_loaders)]
-partial_loaders = [("template_partials.loader.Loader", cached_loaders)]
 TEMPLATES = [
     {
         "BACKEND": "django.template.backends.django.DjangoTemplates",
         "DIRS": [],
-        # "APP_DIRS": True,
+        "APP_DIRS": True,
         "OPTIONS": {
             "context_processors": [
                 "django.template.context_processors.debug",
@@ -18,8 +11,6 @@
                 "django.contrib.messages.context_processors.messages",
             ],
             "debug": True,
-            # TODO: Add wrap_loaded function to the called from an AppConfig.ready().
-            "loaders": partial_loaders,
         },
     },
 ]