From aa14e3698dc29ff8dfc595efea7987b8a3d02af1 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:00:56 +0200 Subject: [PATCH] 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 --- requirements-dev.in | 1 + src/django_components/apps.py | 10 ++- src/django_components/component.py | 2 +- .../util/django_monkeypatch.py | 39 ++++++++- .../integration_template_partials.html | 13 +++ tests/test_integration_template_partials.py | 84 +++++++++++++++++++ tests/test_templatetags_extends.py | 21 +++-- tox.ini | 2 + 8 files changed, 159 insertions(+), 13 deletions(-) create mode 100644 tests/templates/integration_template_partials.html create mode 100644 tests/test_integration_template_partials.py diff --git a/requirements-dev.in b/requirements-dev.in index 99c55757..89cdeb10 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,5 +1,6 @@ django djc-core-html-parser +django-template-partials tox pytest pytest-asyncio diff --git a/src/django_components/apps.py b/src/django_components/apps.py index 15217168..21408c31 100644 --- a/src/django_components/apps.py +++ b/src/django_components/apps.py @@ -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() diff --git a/src/django_components/component.py b/src/django_components/component.py index bce340b4..11678f46 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -3250,7 +3250,7 @@ class Component(metaclass=ComponentMeta): ), ) ``` - """ # noqa: 501 + """ # noqa: E501 # TODO_v1 - Remove, superseded by `deps_strategy` if type is not None: diff --git a/src/django_components/util/django_monkeypatch.py b/src/django_components/util/django_monkeypatch.py index 722eba02..c9db1e38 100644 --- a/src/django_components/util/django_monkeypatch.py +++ b/src/django_components/util/django_monkeypatch.py @@ -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,10 +118,14 @@ 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() - self.extra_data = getattr(parser, "extra_data", {}) - # ---------------- END OF ADDED IN Django v5.1 ---------------- + 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 except Exception as e: if self.engine.debug: @@ -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) diff --git a/tests/templates/integration_template_partials.html b/tests/templates/integration_template_partials.html new file mode 100644 index 00000000..9d3f92ef --- /dev/null +++ b/tests/templates/integration_template_partials.html @@ -0,0 +1,13 @@ + +{% load component_tags %} +{% load partials %} + +{% partialdef test___partial inline %} + +{% component_css_dependencies %} + +{% component "calendar" date=date / %} + +{% component_js_dependencies %} + +{% endpartialdef %} diff --git a/tests/test_integration_template_partials.py b/tests/test_integration_template_partials.py new file mode 100644 index 00000000..2e37309b --- /dev/null +++ b/tests/test_integration_template_partials.py @@ -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 = """ +
+
Today's date is {{ date }}
+
+ """ + 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"