diff --git a/src/django_components/component.py b/src/django_components/component.py index ef4e636e..3edea421 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -1,6 +1,7 @@ import inspect import types from collections import deque +from contextlib import contextmanager from dataclasses import dataclass from typing import ( Any, @@ -8,6 +9,7 @@ from typing import ( ClassVar, Deque, Dict, + Generator, Generic, List, Literal, @@ -520,22 +522,7 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co context_data = self.get_context_data(*args, **kwargs) self._validate_outputs(context_data) - with context.update(context_data): - template = self.get_template(context) - _monkeypatch_template(template) - - if context.template is None: - # Associate the newly-created Context with a Template, otherwise we get - # an error when we try to use `{% include %}` tag inside the template? - # See https://github.com/EmilStenstrom/django-components/issues/580 - context.template = template - context.template_name = template.name - - # Set `Template._dc_is_component_nested` based on whether we're currently INSIDE - # the `{% extends %}` tag. - # Part of fix for https://github.com/EmilStenstrom/django-components/issues/508 - template._dc_is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY)) - + with _prepare_template(self, context, context_data) as template: # Support passing slots explicitly to `render` method if has_slots: fill_content = self._fills_from_slots_data( @@ -843,3 +830,35 @@ def _monkeypatch_template(template: Template) -> None: # See https://stackoverflow.com/a/42154067/9788634 template.render = types.MethodType(_template_render, template) + + +@contextmanager +def _maybe_bind_template(context: Context, template: Template) -> Generator[None, Any, None]: + if context.template is None: + with context.bind_template(template): + yield + else: + yield + + +@contextmanager +def _prepare_template( + component: Component, + context: Context, + context_data: Any, +) -> Generator[Template, Any, None]: + with context.update(context_data): + # Associate the newly-created Context with a Template, otherwise we get + # an error when we try to use `{% include %}` tag inside the template? + # See https://github.com/EmilStenstrom/django-components/issues/580 + # And https://github.com/EmilStenstrom/django-components/issues/634 + template = component.get_template(context) + _monkeypatch_template(template) + + # Set `Template._dc_is_component_nested` based on whether we're currently INSIDE + # the `{% extends %}` tag. + # Part of fix for https://github.com/EmilStenstrom/django-components/issues/508 + template._dc_is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY)) + + with _maybe_bind_template(context, template): + yield template diff --git a/tests/test_component.py b/tests/test_component.py index df127f6d..f09726c8 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -3,6 +3,7 @@ Tests focusing on the Component class. For tests focusing on the `component` tag, see `test_templatetags_component.py` """ +import re import sys from typing import Any, Dict, Tuple, Union, no_type_check @@ -14,13 +15,16 @@ else: from unittest import skipIf +from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.http import HttpRequest, HttpResponse from django.template import Context, RequestContext, Template, TemplateSyntaxError from django.template.base import TextNode +from django.test import Client +from django.urls import path from django.utils.safestring import SafeString -from django_components import Component, SlotFunc, registry, types +from django_components import Component, ComponentView, SlotFunc, register, registry, types from django_components.slots import SlotRef from .django_test_setup import setup_test_config @@ -29,6 +33,21 @@ from .testutils import BaseTestCase, parametrize_context_behavior setup_test_config({"autodiscover": False}) +# Client for testing endpoints via requests +class CustomClient(Client): + def __init__(self, urlpatterns=None, *args, **kwargs): + import types + + if urlpatterns: + urls_module = types.ModuleType("urls") + urls_module.urlpatterns = urlpatterns # type: ignore + settings.ROOT_URLCONF = urls_module + else: + settings.ROOT_URLCONF = __name__ + settings.SECRET_KEY = "secret" # noqa + super().__init__(*args, **kwargs) + + # Component typings CompArgs = Tuple[int, str] @@ -772,6 +791,7 @@ class ComponentRenderTest(BaseTestCase): ) # See https://github.com/EmilStenstrom/django-components/issues/580 + # And https://github.com/EmilStenstrom/django-components/issues/634 # And https://github.com/EmilStenstrom/django-components/commit/fee26ec1d8b46b5ee065ca1ce6143889b0f96764 @parametrize_context_behavior(["django", "isolated"]) def test_render_with_include_and_request_context(self): @@ -793,6 +813,63 @@ class ComponentRenderTest(BaseTestCase): """, ) + # See https://github.com/EmilStenstrom/django-components/issues/580 + # And https://github.com/EmilStenstrom/django-components/issues/634 + @parametrize_context_behavior(["django", "isolated"]) + def test_request_context_is_populated_from_context_processors(self): + @register("thing") + class Thing(Component): + template: types.django_html = """ + Rendered {{ how }} +