Patch TemplateProxy to restore compatibility with template_partials (#1328)

Co-authored-by: EmilStenstrom <224130+EmilStenstrom@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Juro Oravec <juraj.oravec.josefson@gmail.com>
This commit is contained in:
Copilot 2025-08-14 11:00:56 +02:00 committed by GitHub
parent e9b7470850
commit aa14e3698d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 159 additions and 13 deletions

View file

@ -1,5 +1,6 @@
django
djc-core-html-parser
django-template-partials
tox
pytest
pytest-asyncio

View file

@ -19,7 +19,11 @@ class ComponentsConfig(AppConfig):
from django_components.component_registry import registry
from django_components.components.dynamic import DynamicComponent
from django_components.extension import extensions
from django_components.util.django_monkeypatch import monkeypatch_include_node, monkeypatch_template_cls
from django_components.util.django_monkeypatch import (
monkeypatch_include_node,
monkeypatch_template_cls,
monkeypatch_template_proxy_cls,
)
# NOTE: This monkeypatch is applied here, before Django processes any requests.
# To make django-components work with django-debug-toolbar-template-profiler
@ -27,6 +31,10 @@ class ComponentsConfig(AppConfig):
monkeypatch_template_cls(Template)
monkeypatch_include_node(IncludeNode)
# This makes django-components work with django-template-partials
# NOTE: Delete when Django 5.2 reaches end of life
monkeypatch_template_proxy_cls()
# Import modules set in `COMPONENTS.libraries` setting
import_libraries()

View file

@ -3250,7 +3250,7 @@ class Component(metaclass=ComponentMeta):
),
)
```
""" # noqa: 501
""" # noqa: E501
# TODO_v1 - Remove, superseded by `deps_strategy`
if type is not None:

View file

@ -1,5 +1,6 @@
from typing import Any, Optional, Type
from django import VERSION as DJANGO_VERSION
from django.template import Context, NodeList, Template
from django.template.base import Node, Origin, Parser
from django.template.loader_tags import IncludeNode
@ -117,8 +118,12 @@ def monkeypatch_template_compile_nodelist(template_cls: Type[Template]) -> None:
)
try:
# ---------------- ADDED IN Django v5.1 - See https://github.com/django/django/commit/35bbb2c9c01882b1d77b0b8c737ac646144833d4 # noqa: E501
nodelist = parser.parse()
if DJANGO_VERSION >= (5, 1):
# ---------------- ADDED IN Django v5.1 - See https://github.com/django/django/commit/35bbb2c9c01882b1d77b0b8c737ac646144833d4 # noqa: E501
# NOTE: This must be enabled ONLY for 5.1 and later. Previously it was also for older
# Django versions, and this led to compatibility issue with django-template-partials.
# See https://github.com/carltongibson/django-template-partials/pull/85#issuecomment-3187354790
self.extra_data = getattr(parser, "extra_data", {})
# ---------------- END OF ADDED IN Django v5.1 ----------------
return nodelist
@ -243,5 +248,33 @@ def monkeypatch_include_render(include_node_cls: Type[Node]) -> None:
include_node_cls.render = _include_render
# NOTE: Remove once Django v5.2 reaches end of life
# See https://github.com/django-components/django-components/issues/1323#issuecomment-3163478287
def monkeypatch_template_proxy_cls() -> None:
# Patch TemplateProxy if template_partials is installed
# See https://github.com/django-components/django-components/issues/1323#issuecomment-3164224042
try:
from template_partials.templatetags.partials import TemplateProxy
except ImportError:
# template_partials is in INSTALLED_APPS but not actually installed
# This is fine, just skip the patching
return
if is_cls_patched(TemplateProxy):
return
monkeypatch_template_proxy_render(TemplateProxy)
TemplateProxy._djc_patched = True
def monkeypatch_template_proxy_render(template_proxy_cls: Type[Any]) -> None:
# NOTE: TemplateProxy.render() is same logic as Template.render(), just duplicated.
# So we can instead reuse Template.render()
def _template_proxy_render(self: Any, context: Context, *args: Any, **kwargs: Any) -> str:
return Template.render(self, context)
template_proxy_cls.render = _template_proxy_render
def is_cls_patched(cls: Type[Any]) -> bool:
return getattr(cls, "_djc_patched", False)

View file

@ -0,0 +1,13 @@
<!-- TODO: Delete after Django 5.2 reaches end of life -->
{% load component_tags %}
{% load partials %}
{% partialdef test___partial inline %}
{% component_css_dependencies %}
{% component "calendar" date=date / %}
{% component_js_dependencies %}
{% endpartialdef %}

View file

@ -0,0 +1,84 @@
"""
Tests for template_partials integration with django-components.
See https://github.com/django-components/django-components/issues/1327
and https://github.com/django-components/django-components/issues/1323.
This file can be deleted after Django 5.2 reached end of life.
See https://github.com/django-components/django-components/issues/1323#issuecomment-3163478287.
"""
from typing import NamedTuple
import pytest
from django.shortcuts import render
from django.http import HttpRequest
from django_components import Component, register
from django_components.testing import djc_test
from .testutils import setup_test_config
try:
from template_partials.templatetags.partials import TemplateProxy
except ImportError:
TemplateProxy = None
setup_test_config(components={"autodiscover": False})
# See https://github.com/django-components/django-components/issues/1323#issuecomment-3156654329
@djc_test(
django_settings={"INSTALLED_APPS": ("template_partials", "django_components", "tests.test_app")}
)
class TestTemplatePartialsIntegration:
@pytest.mark.skipif(TemplateProxy is None, reason="template_partials not available")
def test_render_partial(self):
@register("calendar")
class Calendar(Component):
template = """
<div class="calendar-component">
<div>Today's date is <span>{{ date }}</span></div>
</div>
"""
css = """
.calendar-component { width: 200px; background: pink; }
.calendar-component span { font-weight: bold; }
"""
js = """
(function(){
if (document.querySelector(".calendar-component")) {
document.querySelector(".calendar-component").onclick = function(){ alert("Clicked calendar!"); };
}
})()
""" # noqa: E501
class Kwargs(NamedTuple):
date: str
def get_template_data(self, args, kwargs: Kwargs, slots, context):
return {
"date": kwargs.date,
}
# NOTE: When a full template is rendered (without the `#` syntax), the output should be as usual.
request = HttpRequest()
result = render(request, "integration_template_partials.html")
content = result.content
assert b"<!-- _RENDERED" not in content
assert b"width: 200px;" in content
assert b'alert("Clicked calendar!")' in content
# NOTE: When a partial is rendered with the `#` syntax, what *actually*
# gets rendered is `TemplateProxy` from template_partials, instead of Django's own `Template` class.
# Hence, the monkeypatching that we've done on the Template class does NOT apply to TemplateProxy.
# So we want to check that the result HAS its CSS/JS processed, which means that the monkeypatching
# works as expected.
request2 = HttpRequest()
result2 = render(request2, "integration_template_partials.html#test___partial")
content2 = result2.content
assert b"<!-- _RENDERED" not in content2
assert b"width: 200px;" in content2
assert b'alert("Clicked calendar!")' in content2

View file

@ -581,16 +581,18 @@ class TestExtendsCompat:
# NOTE: It's important that the <script> tags are rendered outside of <div> and <outer> tags,
# because that tells us that the JS/CSS is rendered by the parent template, not the component
# inside the include.
expected = """
# NOTE 2: The IDs differ when rendered as part of whole test suite vs as a single test.
comp_id = "ca1bc41" if "ca1bc41" in rendered else "ca1bc40"
expected = f"""
<body>
<outer>
<div data-djc-id-ca1bc41>Hello</div>
<div data-djc-id-{comp_id}>Hello</div>
</outer>
<script src="django_components/django_components.min.js"></script>
<script type="application/json" data-djc>{"loadedCssUrls": ["c3R5bGUuY3Nz"],
<script type="application/json" data-djc>{{"loadedCssUrls": ["c3R5bGUuY3Nz"],
"loadedJsUrls": ["c2NyaXB0Lmpz"],
"toLoadCssTags": [],
"toLoadJsTags": []}</script>
"toLoadJsTags": []}}</script>
<script src="script.js"></script>
</body>
"""
@ -636,17 +638,20 @@ class TestExtendsCompat:
template_obj = Template(template)
context = Context()
rendered = template_obj.render(context)
expected = """
# NOTE: The IDs differ when rendered as part of whole test suite vs as a single test.
comp_id = "ca1bc45" if "ca1bc45" in rendered else "ca1bc44"
expected = f"""
<html>
<body data-djc-id-ca1bc43>
<outer>
<div data-djc-id-ca1bc45>Hello</div>
<div data-djc-id-{comp_id}>Hello</div>
</outer>
<script src="django_components/django_components.min.js"></script>
<script type="application/json" data-djc>{"loadedCssUrls": ["c3R5bGUuY3Nz"],
<script type="application/json" data-djc>{{"loadedCssUrls": ["c3R5bGUuY3Nz"],
"loadedJsUrls": ["c2NyaXB0Lmpz"],
"toLoadCssTags": [],
"toLoadJsTags": []}</script>
"toLoadJsTags": []}}</script>
<script src="script.js"></script>
</body>
</html>

View file

@ -44,6 +44,8 @@ deps =
requests
types-requests
whitenoise
# NOTE: Delete when Django 5.2 reaches end of life
django-template-partials
commands = pytest {posargs}
[testenv:flake8]