diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78c1d22b..e4f279c1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -186,6 +186,40 @@ Summary:
)
```
+- `Component.template` no longer accepts a Template instance, only plain string.
+
+ Before:
+
+ ```py
+ class MyComponent(Component):
+ template = Template("{{ my_var }}")
+ ```
+
+ Instead, either:
+
+ 1. Set `Component.template` to a plain string.
+
+ ```py
+ class MyComponent(Component):
+ template = "{{ my_var }}"
+ ```
+
+ 2. Move the template to it's own HTML file and set `Component.template_file`.
+
+ ```py
+ class MyComponent(Component):
+ template_file = "my_template.html"
+ ```
+
+ 3. Or, if you dynamically created the template, render the template inside `Component.on_render()`.
+
+ ```py
+ class MyComponent(Component):
+ def on_render(self, context, template):
+ dynamic_template = do_something_dynamic()
+ return dynamic_template.render(context)
+ ```
+
- The `Component.Url` class was merged with `Component.View`.
Instead of `Component.Url.public`, use `Component.View.public`.
@@ -404,6 +438,51 @@ Summary:
Since `get_context_data()` is widely used, it will remain available until v2.
+- `Component.get_template_name()` and `Component.get_template()` are now deprecated. Use `Component.template`,
+`Component.template_file` or `Component.on_render()` instead.
+
+ `Component.get_template_name()` and `Component.get_template()` will be removed in v1.
+
+ In v1, each Component will have at most one static template.
+ This is needed to enable support for Markdown, Pug, or other pre-processing of templates by extensions.
+
+ If you are using the deprecated methods to point to different templates, there's 2 ways to migrate:
+
+ 1. Split the single Component into multiple Components, each with its own template. Then switch between them in `Component.on_render()`:
+
+ ```py
+ class MyComponentA(Component):
+ template_file = "a.html"
+
+ class MyComponentB(Component):
+ template_file = "b.html"
+
+ class MyComponent(Component):
+ def on_render(self, context, template):
+ if context["a"]:
+ return MyComponentA.render(context)
+ else:
+ return MyComponentB.render(context)
+ ```
+
+ 2. Alternatively, use `Component.on_render()` with Django's `get_template()` to dynamically render different templates:
+
+ ```py
+ from django.template.loader import get_template
+
+ class MyComponent(Component):
+ def on_render(self, context, template):
+ if context["a"]:
+ template_name = "a.html"
+ else:
+ template_name = "b.html"
+
+ actual_template = get_template(template_name)
+ return actual_template.render(context)
+ ```
+
+ Read more in [django-components#1204](https://github.com/django-components/django-components/discussions/1204).
+
- The `type` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy` instead. The `type` kwarg will be removed in v1.
Before:
@@ -651,6 +730,22 @@ Summary:
**Miscellaneous**
+- Template caching with `cached_template()` helper and `template_cache_size` setting is deprecated.
+ These will be removed in v1.
+
+ This feature made sense if you were dynamically generating templates for components using
+ `Component.get_template_string()` and `Component.get_template()`.
+
+ However, in v1, each Component will have at most one static template. This static template
+ is cached internally per component class, and reused across renders.
+
+ This makes the template caching feature obsolete.
+
+ If you relied on `cached_template()`, you should either:
+
+ 1. Wrap the templates as Components.
+ 2. Manage the cache of Templates yourself.
+
- The `debug_highlight_components` and `debug_highlight_slots` settings are deprecated.
These will be removed in v1.
@@ -1004,6 +1099,35 @@ Summary:
can now be accessed also outside of the render call. So now its possible to take the component
instance out of `get_template_data()` (although this is not recommended).
+- Components can now be defined without a template.
+
+ Previously, the following would raise an error:
+
+ ```py
+ class MyComponent(Component):
+ pass
+ ```
+
+ "Template-less" components can be used together with `Component.on_render()` to dynamically
+ pick what to render:
+
+ ```py
+ class TableNew(Component):
+ template_file = "table_new.html"
+
+ class TableOld(Component):
+ template_file = "table_old.html"
+
+ class Table(Component):
+ def on_render(self, context, template):
+ if self.kwargs.get("feat_table_new_ui"):
+ return TableNew.render(args=self.args, kwargs=self.kwargs, slots=self.slots)
+ else:
+ return TableOld.render(args=self.args, kwargs=self.kwargs, slots=self.slots)
+ ```
+
+ "Template-less" components can be also used as a base class for other components, or as mixins.
+
- Passing `Slot` instance to `Slot` constructor raises an error.
#### Fix
diff --git a/docs/reference/commands.md b/docs/reference/commands.md
index 8ea93d65..aacbca67 100644
--- a/docs/reference/commands.md
+++ b/docs/reference/commands.md
@@ -461,8 +461,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
## `upgradecomponent`
```txt
-usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color]
- [--force-color] [--skip-checks]
+usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
+ [--skip-checks]
```
diff --git a/docs/reference/template_tags.md b/docs/reference/template_tags.md
index 7093c2b6..af62e154 100644
--- a/docs/reference/template_tags.md
+++ b/docs/reference/template_tags.md
@@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
-See source code
+See source code
diff --git a/sampleproject/sampleproject/settings.py b/sampleproject/sampleproject/settings.py
index 69043457..5fbdbac2 100644
--- a/sampleproject/sampleproject/settings.py
+++ b/sampleproject/sampleproject/settings.py
@@ -94,7 +94,6 @@ COMPONENTS = ComponentsSettings(
dirs=[BASE_DIR / "components"],
# app_dirs=["components"],
# libraries=[],
- # template_cache_size=128,
# context_behavior="isolated", # "django" | "isolated"
)
diff --git a/src/django_components/app_settings.py b/src/django_components/app_settings.py
index bba80c1f..53bfacc8 100644
--- a/src/django_components/app_settings.py
+++ b/src/django_components/app_settings.py
@@ -620,8 +620,11 @@ class ComponentsSettings(NamedTuple):
```
"""
+ # TODO_V1 - remove
template_cache_size: Optional[int] = None
"""
+ DEPRECATED. Template caching will be removed in v1.
+
Configure the maximum amount of Django templates to be cached.
Defaults to `128`.
diff --git a/src/django_components/cache.py b/src/django_components/cache.py
index b5c905d4..a14954ed 100644
--- a/src/django_components/cache.py
+++ b/src/django_components/cache.py
@@ -6,13 +6,10 @@ from django.core.cache.backends.locmem import LocMemCache
from django_components.app_settings import app_settings
from django_components.util.cache import LRUCache
+# TODO_V1 - Remove, won't be needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
+#
# This stores the parsed Templates. This is strictly local for now, as it stores instances.
# NOTE: Lazily initialized so it can be configured based on user-defined settings.
-#
-# TODO: Once we handle whole template parsing ourselves, this could store just
-# the parsed template AST (+metadata) instead of Template instances. In that case
-# we could open this up to be stored non-locally and shared across processes.
-# This would also allow us to remove our custom `LRUCache` implementation.
template_cache: Optional[LRUCache] = None
# This stores the inlined component JS and CSS files (e.g. `Component.js` and `Component.css`).
@@ -20,6 +17,7 @@ template_cache: Optional[LRUCache] = None
component_media_cache: Optional[BaseCache] = None
+# TODO_V1 - Remove, won't be needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
def get_template_cache() -> LRUCache:
global template_cache
if template_cache is None:
diff --git a/src/django_components/commands/upgrade.py b/src/django_components/commands/upgrade.py
index 627eb316..a160c6f0 100644
--- a/src/django_components/commands/upgrade.py
+++ b/src/django_components/commands/upgrade.py
@@ -6,7 +6,7 @@ from typing import Any
from django.conf import settings
from django.template.engine import Engine
-from django_components.template_loader import Loader
+from django_components.template_loader import DjcLoader
from django_components.util.command import CommandArg, ComponentCommand
@@ -24,7 +24,7 @@ class UpgradeCommand(ComponentCommand):
def handle(self, *args: Any, **options: Any) -> None:
current_engine = Engine.get_default()
- loader = Loader(current_engine)
+ loader = DjcLoader(current_engine)
dirs = loader.get_dirs(include_apps=False)
if settings.BASE_DIR:
diff --git a/src/django_components/component.py b/src/django_components/component.py
index eb4f1283..914083ab 100644
--- a/src/django_components/component.py
+++ b/src/django_components/component.py
@@ -1,5 +1,4 @@
import sys
-from contextlib import contextmanager
from dataclasses import dataclass
from types import MethodType
from typing import (
@@ -7,7 +6,6 @@ from typing import (
Callable,
ClassVar,
Dict,
- Generator,
List,
Mapping,
NamedTuple,
@@ -19,12 +17,10 @@ from typing import (
)
from weakref import ReferenceType, WeakValueDictionary, finalize
-from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media as MediaCls
from django.http import HttpRequest, HttpResponse
-from django.template.base import NodeList, Origin, Parser, Template, Token
+from django.template.base import NodeList, Parser, Template, Token
from django.template.context import Context, RequestContext
-from django.template.loader import get_template
from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext
from django.test.signals import template_rendered
from django.views import View
@@ -72,12 +68,11 @@ from django_components.slots import (
normalize_slot_fills,
resolve_fills,
)
-from django_components.template import cached_template
+from django_components.template import cache_component_template_file, prepare_component_template
from django_components.util.context import gen_context_processors_data, snapshot_context
-from django_components.util.django_monkeypatch import is_template_cls_patched
from django_components.util.exception import component_error_message
from django_components.util.logger import trace_component_msg
-from django_components.util.misc import default, gen_id, get_import_path, hash_comp_cls, to_dict
+from django_components.util.misc import default, gen_id, hash_comp_cls, to_dict
from django_components.util.template_tag import TagAttr
from django_components.util.weakref import cached_ref
@@ -412,14 +407,23 @@ class ComponentTemplateNameDescriptor:
class ComponentMeta(ComponentMediaMeta):
- def __new__(mcs, name: Any, bases: Tuple, attrs: Dict) -> Any:
+ def __new__(mcs, name: str, bases: Tuple[Type, ...], attrs: Dict) -> Type:
# If user set `template_name` on the class, we instead set it to `template_file`,
# because we want `template_name` to be the descriptor that proxies to `template_file`.
if "template_name" in attrs:
attrs["template_file"] = attrs.pop("template_name")
attrs["template_name"] = ComponentTemplateNameDescriptor()
- return super().__new__(mcs, name, bases, attrs)
+ cls = super().__new__(mcs, name, bases, attrs)
+
+ # If the component defined `template_file`, then associate this Component class
+ # with that template file path.
+ # This way, when we will be instantiating `Template` in order to load the Component's template,
+ # and its template_name matches this path, then we know that the template belongs to this Component class.
+ if "template_file" in attrs and attrs["template_file"]:
+ cache_component_template_file(cls)
+
+ return cls
# This runs when a Component class is being deleted
def __del__(cls) -> None:
@@ -646,25 +650,47 @@ class Component(metaclass=ComponentMeta):
- Relative to the directory where the Component's Python file is defined.
- Relative to one of the component directories, as set by
- [`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
+ [`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
or
- [`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
+ [`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `/components/`).
- Relative to the template directories, as set by Django's `TEMPLATES` setting (e.g. `/templates/`).
- Only one of [`template_file`](../api#django_components.Component.template_file),
- [`get_template_name`](../api#django_components.Component.get_template_name),
- [`template`](../api#django_components.Component.template)
- or [`get_template`](../api#django_components.Component.get_template) must be defined.
+ !!! warning
+
+ Only one of [`template_file`](../api#django_components.Component.template_file),
+ [`get_template_name`](../api#django_components.Component.get_template_name),
+ [`template`](../api#django_components.Component.template)
+ or [`get_template`](../api#django_components.Component.get_template) must be defined.
**Example:**
- ```py
- class MyComponent(Component):
- template_file = "path/to/template.html"
+ Assuming this project layout:
- def get_template_data(self, args, kwargs, slots, context):
- return {"name": "World"}
+ ```txt
+ |- components/
+ |- table/
+ |- table.html
+ |- table.css
+ |- table.js
+ ```
+
+ Template name can be either relative to the python file (`components/table/table.py`):
+
+ ```python
+ class Table(Component):
+ template_file = "table.html"
+ ```
+
+ Or relative to one of the directories in
+ [`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
+ or
+ [`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
+ (`components/`):
+
+ ```python
+ class Table(Component):
+ template_file = "table/table.html"
```
"""
@@ -677,53 +703,142 @@ class Component(metaclass=ComponentMeta):
For historical reasons, django-components used `template_name` to align with Django's
[TemplateView](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.TemplateView).
- `template_file` was introduced to align with `js/js_file` and `css/css_file`.
+ `template_file` was introduced to align with
+ [`js`](../api#django_components.Component.js)/[`js_file`](../api#django_components.Component.js_file)
+ and [`css`](../api#django_components.Component.css)/[`css_file`](../api#django_components.Component.css_file).
- Setting and accessing this attribute is proxied to `template_file`.
+ Setting and accessing this attribute is proxied to
+ [`template_file`](../api#django_components.Component.template_file).
"""
+ # TODO_v1 - Remove
def get_template_name(self, context: Context) -> Optional[str]:
"""
- Filepath to the Django template associated with this component.
+ DEPRECATED: Use instead [`Component.template_file`](../api#django_components.Component.template_file),
+ [`Component.template`](../api#django_components.Component.template) or
+ [`Component.on_render()`](../api#django_components.Component.on_render).
+ Will be removed in v1.
- The filepath must be relative to either the file where the component class was defined,
- or one of the roots of `STATIFILES_DIRS`.
+ Same as [`Component.template_file`](../api#django_components.Component.template_file),
+ but allows to dynamically resolve the template name at render time.
- Only one of [`template_file`](../api#django_components.Component.template_file),
- [`get_template_name`](../api#django_components.Component.get_template_name),
- [`template`](../api#django_components.Component.template)
- or [`get_template`](../api#django_components.Component.get_template) must be defined.
+ See [`Component.template_file`](../api#django_components.Component.template_file)
+ for more info and examples.
+
+ !!! warning
+
+ The context is not fully populated at the point when this method is called.
+
+ If you need to access the context, either use
+ [`Component.on_render_before()`](../api#django_components.Component.on_render_before) or
+ [`Component.on_render()`](../api#django_components.Component.on_render).
+
+ !!! warning
+
+ Only one of
+ [`template_file`](../api#django_components.Component.template_file),
+ [`get_template_name()`](../api#django_components.Component.get_template_name),
+ [`template`](../api#django_components.Component.template)
+ or
+ [`get_template()`](../api#django_components.Component.get_template)
+ must be defined.
+
+ Args:
+ context (Context): The Django template\
+ [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)\
+ in which the component is rendered.
+
+ Returns:
+ Optional[str]: The filepath to the template.
"""
return None
- template: Optional[Union[str, Template]] = None
+ template: Optional[str] = None
"""
- Inlined Django template associated with this component. Can be a plain string or a Template instance.
+ Inlined Django template (as a plain string) associated with this component.
- Only one of [`template_file`](../api#django_components.Component.template_file),
- [`get_template_name`](../api#django_components.Component.get_template_name),
- [`template`](../api#django_components.Component.template)
- or [`get_template`](../api#django_components.Component.get_template) must be defined.
+ !!! warning
+
+ Only one of
+ [`template_file`](../api#django_components.Component.template_file),
+ [`template`](../api#django_components.Component.template),
+ [`get_template_name()`](../api#django_components.Component.get_template_name),
+ or
+ [`get_template()`](../api#django_components.Component.get_template)
+ must be defined.
**Example:**
- ```py
- class MyComponent(Component):
- template = "Hello, {{ name }}!"
+ ```python
+ class Table(Component):
+ template = '''
+
+ {{ my_var }}
+
+ '''
+ ```
- def get_template_data(self, args, kwargs, slots, context):
- return {"name": "World"}
+ **Syntax highlighting**
+
+ When using the inlined template, you can enable syntax highlighting
+ with `django_components.types.django_html`.
+
+ Learn more about [syntax highlighting](../../concepts/fundamentals/single_file_components/#syntax-highlighting).
+
+ ```djc_py
+ from django_components import Component, types
+
+ class MyComponent(Component):
+ template: types.django_html = '''
+
+ {{ my_var }}
+
+ '''
```
"""
+ # TODO_v1 - Remove
def get_template(self, context: Context) -> Optional[Union[str, Template]]:
"""
- Inlined Django template associated with this component. Can be a plain string or a Template instance.
+ DEPRECATED: Use instead [`Component.template_file`](../api#django_components.Component.template_file),
+ [`Component.template`](../api#django_components.Component.template) or
+ [`Component.on_render()`](../api#django_components.Component.on_render).
+ Will be removed in v1.
- Only one of [`template_file`](../api#django_components.Component.template_file),
- [`get_template_name`](../api#django_components.Component.get_template_name),
- [`template`](../api#django_components.Component.template)
- or [`get_template`](../api#django_components.Component.get_template) must be defined.
+ Same as [`Component.template`](../api#django_components.Component.template),
+ but allows to dynamically resolve the template at render time.
+
+ The template can be either plain string or
+ a [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) instance.
+
+ See [`Component.template`](../api#django_components.Component.template) for more info and examples.
+
+ !!! warning
+
+ Only one of
+ [`template`](../api#django_components.Component.template)
+ [`template_file`](../api#django_components.Component.template_file),
+ [`get_template_name()`](../api#django_components.Component.get_template_name),
+ or
+ [`get_template()`](../api#django_components.Component.get_template)
+ must be defined.
+
+ !!! warning
+
+ The context is not fully populated at the point when this method is called.
+
+ If you need to access the context, either use
+ [`Component.on_render_before()`](../api#django_components.Component.on_render_before) or
+ [`Component.on_render()`](../api#django_components.Component.on_render).
+
+ Args:
+ context (Context): The Django template\
+ [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)\
+ in which the component is rendered.
+
+ Returns:
+ Optional[Union[str, Template]]: The inlined Django template string or\
+ a [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) instance.
"""
return None
@@ -981,8 +1096,10 @@ class Component(metaclass=ComponentMeta):
"""
Main JS associated with this component inlined as string.
- Only one of [`js`](../api#django_components.Component.js) or
- [`js_file`](../api#django_components.Component.js_file) must be defined.
+ !!! warning
+
+ Only one of [`js`](../api#django_components.Component.js) or
+ [`js_file`](../api#django_components.Component.js_file) must be defined.
**Example:**
@@ -990,6 +1107,22 @@ class Component(metaclass=ComponentMeta):
class MyComponent(Component):
js = "console.log('Hello, World!');"
```
+
+ **Syntax highlighting**
+
+ When using the inlined template, you can enable syntax highlighting
+ with `django_components.types.js`.
+
+ Learn more about [syntax highlighting](../../concepts/fundamentals/single_file_components/#syntax-highlighting).
+
+ ```djc_py
+ from django_components import Component, types
+
+ class MyComponent(Component):
+ js: types.js = '''
+ console.log('Hello, World!');
+ '''
+ ```
"""
js_file: ClassVar[Optional[str]] = None
@@ -1000,9 +1133,9 @@ class Component(metaclass=ComponentMeta):
- Relative to the directory where the Component's Python file is defined.
- Relative to one of the component directories, as set by
- [`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
+ [`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
or
- [`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
+ [`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `/components/`).
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `/static/`).
@@ -1012,8 +1145,10 @@ class Component(metaclass=ComponentMeta):
the path is resolved.
2. The file is read and its contents is set to [`Component.js`](../api#django_components.Component.js).
- Only one of [`js`](../api#django_components.Component.js) or
- [`js_file`](../api#django_components.Component.js_file) must be defined.
+ !!! warning
+
+ Only one of [`js`](../api#django_components.Component.js) or
+ [`js_file`](../api#django_components.Component.js_file) must be defined.
**Example:**
@@ -1244,19 +1379,39 @@ class Component(metaclass=ComponentMeta):
"""
Main CSS associated with this component inlined as string.
- Only one of [`css`](../api#django_components.Component.css) or
- [`css_file`](../api#django_components.Component.css_file) must be defined.
+ !!! warning
+
+ Only one of [`css`](../api#django_components.Component.css) or
+ [`css_file`](../api#django_components.Component.css_file) must be defined.
**Example:**
```py
class MyComponent(Component):
css = \"\"\"
- .my-class {
- color: red;
- }
+ .my-class {
+ color: red;
+ }
\"\"\"
```
+
+ **Syntax highlighting**
+
+ When using the inlined template, you can enable syntax highlighting
+ with `django_components.types.css`.
+
+ Learn more about [syntax highlighting](../../concepts/fundamentals/single_file_components/#syntax-highlighting).
+
+ ```djc_py
+ from django_components import Component, types
+
+ class MyComponent(Component):
+ css: types.css = '''
+ .my-class {
+ color: red;
+ }
+ '''
+ ```
"""
css_file: ClassVar[Optional[str]] = None
@@ -1267,9 +1422,9 @@ class Component(metaclass=ComponentMeta):
- Relative to the directory where the Component's Python file is defined.
- Relative to one of the component directories, as set by
- [`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
+ [`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
or
- [`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
+ [`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
(e.g. `/components/`).
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `/static/`).
@@ -1279,8 +1434,10 @@ class Component(metaclass=ComponentMeta):
the path is resolved.
2. The file is read and its contents is set to [`Component.css`](../api#django_components.Component.css).
- Only one of [`css`](../api#django_components.Component.css) or
- [`css_file`](../api#django_components.Component.css_file) must be defined.
+ !!! warning
+
+ Only one of [`css`](../api#django_components.Component.css) or
+ [`css_file`](../api#django_components.Component.css_file) must be defined.
**Example:**
@@ -1632,7 +1789,7 @@ class Component(metaclass=ComponentMeta):
# PUBLIC API - HOOKS (Configurable by users)
# #####################################
- def on_render_before(self, context: Context, template: Template) -> None:
+ def on_render_before(self, context: Context, template: Optional[Template]) -> None:
"""
Hook that runs just before the component's template is rendered.
@@ -1640,7 +1797,7 @@ class Component(metaclass=ComponentMeta):
"""
pass
- def on_render_after(self, context: Context, template: Template, content: str) -> Optional[SlotResult]:
+ def on_render_after(self, context: Context, template: Optional[Template], content: str) -> Optional[SlotResult]:
"""
Hook that runs just after the component's template was rendered.
It receives the rendered output as the last argument.
@@ -1753,6 +1910,15 @@ class Component(metaclass=ComponentMeta):
"""Deprecated. Use `Component.class_id` instead."""
return self.class_id
+ _template: Optional[Template] = None
+ """
+ Cached [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
+ instance for the [`Component`](../api#django_components.Component),
+ created from
+ [`Component.template`](#django_components.Component.template) or
+ [`Component.template_file`](#django_components.Component.template_file).
+ """
+
# TODO_v3 - Django-specific property to prevent calling the instance as a function.
do_not_call_in_templates: ClassVar[bool] = True
"""
@@ -1850,6 +2016,9 @@ class Component(metaclass=ComponentMeta):
cls.class_id = hash_comp_cls(cls)
comp_cls_id_mapping[cls.class_id] = cls
+ # Make sure that subclassed component will store it's own template, not the parent's.
+ cls._template = None
+
ALL_COMPONENTS.append(cached_ref(cls)) # type: ignore[arg-type]
extensions._init_component_class(cls)
extensions.on_component_class_created(OnComponentClassCreatedContext(cls))
@@ -2276,64 +2445,6 @@ class Component(metaclass=ComponentMeta):
# MISC
# #####################################
- # NOTE: We cache the Template instance. When the template is taken from a file
- # via `get_template_name`, then we leverage Django's template caching with `get_template()`.
- # Otherwise, we use our own `cached_template()` to cache the template.
- #
- # This is important to keep in mind, because the implication is that we should
- # treat Templates AND their nodelists as IMMUTABLE.
- def _get_template(self, context: Context, component_id: str) -> Template:
- template_name = self.get_template_name(context)
- # TODO_REMOVE_IN_V1 - Remove `self.get_template_string` in v1
- template_getter = getattr(self, "get_template_string", self.get_template)
- template_body = template_getter(context)
-
- # `get_template_name()`, `get_template()`, and `template` are mutually exclusive
- #
- # Note that `template` and `template_name` are also mutually exclusive, but this
- # is checked when lazy-loading the template from `template_name`. So if user specified
- # `template_name`, then `template` will be populated with the content of that file.
- if self.template is not None and template_name is not None:
- raise ImproperlyConfigured(
- "Received non-null value from both 'template/template_name' and 'get_template_name' in"
- f" Component {type(self).__name__}. Only one of the two must be set."
- )
- if self.template is not None and template_body is not None:
- raise ImproperlyConfigured(
- "Received non-null value from both 'template/template_name' and 'get_template' in"
- f" Component {type(self).__name__}. Only one of the two must be set."
- )
- if template_name is not None and template_body is not None:
- raise ImproperlyConfigured(
- "Received non-null value from both 'get_template_name' and 'get_template' in"
- f" Component {type(self).__name__}. Only one of the two must be set."
- )
-
- if template_name is not None:
- return get_template(template_name).template
-
- template_body = template_body if template_body is not None else self.template
- if template_body is not None:
- # We got template string, so we convert it to Template
- if isinstance(template_body, str):
- trace_component_msg("COMP_LOAD", component_name=self.name, component_id=component_id, slot_name=None)
- template: Template = cached_template(
- template_string=template_body,
- name=self.template_file or self.name,
- origin=Origin(
- name=self.template_file or get_import_path(self.__class__),
- template_name=self.template_file or self.name,
- ),
- )
- else:
- template = template_body
-
- return template
-
- raise ImproperlyConfigured(
- f"Either 'template_file' or 'template' must be set for Component {type(self).__name__}."
- )
-
def inject(self, key: str, default: Optional[Any] = None) -> Any:
"""
Use this method to retrieve the data that was passed to a [`{% provide %}`](../template_tags#provide) tag
@@ -2796,12 +2907,12 @@ class Component(metaclass=ComponentMeta):
)
# Use RequestContext if request is provided, so that child non-component template tags
# can access the request object too.
- context = context or (RequestContext(request) if request else Context())
+ context = context if context is not None else (RequestContext(request) if request else Context())
# Allow to provide a dict instead of Context
# NOTE: This if/else is important to avoid nested Contexts,
# See https://github.com/django-components/django-components/issues/414
- if not isinstance(context, Context):
+ if not isinstance(context, (Context, RequestContext)):
context = RequestContext(request, context) if request else Context(context)
render_id = _gen_component_id()
@@ -2850,7 +2961,9 @@ class Component(metaclass=ComponentMeta):
# Required for compatibility with Django's {% extends %} tag
# See https://github.com/django-components/django-components/pull/859
- context.render_context.push({BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())})
+ context.render_context.push( # type: ignore[union-attr]
+ {BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())} # type: ignore
+ )
# We pass down the components the info about the component's parent.
# This is used for correctly resolving slot fills, correct rendering order,
@@ -2886,7 +2999,7 @@ class Component(metaclass=ComponentMeta):
template_name=None,
# This field will be modified from within `SlotNodes.render()`:
# - The `default_slot` will be set to the first slot that has the `default` attribute set.
- # If multiple slots have the `default` attribute set, yet have different name, then
+ # - If multiple slots have the `default` attribute set, yet have different name, then
# we will raise an error.
default_slot=None,
# NOTE: This is only a SNAPSHOT of the outer context.
@@ -2937,10 +3050,32 @@ class Component(metaclass=ComponentMeta):
# but instead can render one component at a time.
#############################################################################
- with _prepare_template(component, template_data) as template:
- component_ctx.template_name = template.name
+ # TODO_v1 - Currently we have to pass `template_data` to `prepare_component_template()`,
+ # so that `get_template_string()`, `get_template_name()`, and `get_template()`
+ # have access to the data from `get_template_data()`.
+ #
+ # Because of that there is one layer of `Context.update()` called inside `prepare_component_template()`.
+ #
+ # Once `get_template_string()`, `get_template_name()`, and `get_template()` are removed,
+ # we can remove that layer of `Context.update()`, and NOT pass `template_data`
+ # to `prepare_component_template()`.
+ #
+ # Then we can simply apply `template_data` to the context in the same layer
+ # where we apply `context_processor_data` and `component_vars`.
+ with prepare_component_template(component, template_data) as template:
+ # Set `Template._djc_is_component_nested` based on whether we're currently INSIDE
+ # the `{% extends %}` tag.
+ # Part of fix for https://github.com/django-components/django-components/issues/508
+ # See django_monkeypatch.py
+ if template is not None:
+ template._djc_is_component_nested = bool(
+ context.render_context.get(BLOCK_CONTEXT_KEY) # type: ignore[union-attr]
+ )
- with context.update(
+ # Capture the template name so we can print better error messages (currently used in slots)
+ component_ctx.template_name = template.name if template else None
+
+ with context.update( # type: ignore[union-attr]
{
# Make data from context processors available inside templates
**component.context_processors_data,
@@ -2973,7 +3108,7 @@ class Component(metaclass=ComponentMeta):
context_snapshot = snapshot_context(context)
# Cleanup
- context.render_context.pop()
+ context.render_context.pop() # type: ignore[union-attr]
######################################
# 5. Render component
@@ -3089,26 +3224,31 @@ class Component(metaclass=ComponentMeta):
# Emit signal that the template is about to be rendered
template_rendered.send(sender=template, template=template, context=context)
- # Get the component's HTML
- html_content = template.render(context)
- # Add necessary HTML attributes to work with JS and CSS variables
- updated_html, child_components = set_component_attrs_for_js_and_css(
- html_content=html_content,
- component_id=render_id,
- css_input_hash=css_input_hash,
- css_scope_id=css_scope_id,
- root_attributes=root_attributes,
- )
+ if template is not None:
+ # Get the component's HTML
+ html_content = template.render(context)
- # Prepend an HTML comment to instructs how and what JS and CSS scripts are associated with it.
- updated_html = insert_component_dependencies_comment(
- updated_html,
- component_cls=component_cls,
- component_id=render_id,
- js_input_hash=js_input_hash,
- css_input_hash=css_input_hash,
- )
+ # Add necessary HTML attributes to work with JS and CSS variables
+ updated_html, child_components = set_component_attrs_for_js_and_css(
+ html_content=html_content,
+ component_id=render_id,
+ css_input_hash=css_input_hash,
+ css_scope_id=css_scope_id,
+ root_attributes=root_attributes,
+ )
+
+ # Prepend an HTML comment to instructs how and what JS and CSS scripts are associated with it.
+ updated_html = insert_component_dependencies_comment(
+ updated_html,
+ component_cls=component_cls,
+ component_id=render_id,
+ js_input_hash=js_input_hash,
+ css_input_hash=css_input_hash,
+ )
+ else:
+ updated_html = ""
+ child_components = {}
trace_component_msg(
"COMP_RENDER_END",
@@ -3278,7 +3418,13 @@ class ComponentNode(BaseNode):
node_id: Optional[str] = None,
contents: Optional[str] = None,
) -> None:
- super().__init__(params=params, flags=flags, nodelist=nodelist, node_id=node_id, contents=contents)
+ super().__init__(
+ params=params,
+ flags=flags,
+ nodelist=nodelist,
+ node_id=node_id,
+ contents=contents,
+ )
self.name = name
self.registry = registry
@@ -3370,43 +3516,3 @@ def _get_parent_component_context(context: Context) -> Union[Tuple[None, None],
parent_comp_ctx = component_context_cache[parent_id]
return parent_id, parent_comp_ctx
-
-
-@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,
- template_data: Any,
-) -> Generator[Template, Any, None]:
- context = component.context
- with context.update(template_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/django-components/django-components/issues/580
- # And https://github.com/django-components/django-components/issues/634
- template = component._get_template(context, component_id=component.id)
-
- if not is_template_cls_patched(template):
- raise RuntimeError(
- "Django-components received a Template instance which was not patched."
- "If you are using Django's Template class, check if you added django-components"
- "to INSTALLED_APPS. If you are using a custom template class, then you need to"
- "manually patch the class."
- )
-
- # Set `Template._djc_is_component_nested` based on whether we're currently INSIDE
- # the `{% extends %}` tag.
- # Part of fix for https://github.com/django-components/django-components/issues/508
- # See django_monkeypatch.py
- template._djc_is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
-
- with _maybe_bind_template(context, template):
- yield template
diff --git a/src/django_components/component_media.py b/src/django_components/component_media.py
index 9bc60e37..c8e1581d 100644
--- a/src/django_components/component_media.py
+++ b/src/django_components/component_media.py
@@ -26,10 +26,9 @@ from weakref import WeakKeyDictionary
from django.contrib.staticfiles import finders
from django.core.exceptions import ImproperlyConfigured
from django.forms.widgets import Media as MediaCls
-from django.template import Template, TemplateDoesNotExist
-from django.template.loader import get_template
from django.utils.safestring import SafeData
+from django_components.template import load_component_template
from django_components.util.loader import get_component_dirs, resolve_file
from django_components.util.logger import logger
from django_components.util.misc import flatten, get_import_path, get_module_info, is_glob
@@ -240,6 +239,7 @@ class ComponentMediaInput(Protocol):
class ComponentMedia:
comp_cls: Type["Component"]
resolved: bool = False
+ resolved_relative_files: bool = False
Media: Optional[Type[ComponentMediaInput]] = None
template: Optional[str] = None
template_file: Optional[str] = None
@@ -543,9 +543,13 @@ def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> N
assert isinstance(self.media, MyMedia)
```
"""
+ if comp_media.resolved:
+ return
+
+ comp_media.resolved = True
+
# Do not resolve if this is a base class
- if get_import_path(comp_cls) == "django_components.component.Component" or comp_media.resolved:
- comp_media.resolved = True
+ if get_import_path(comp_cls) == "django_components.component.Component":
return
comp_dirs = get_component_dirs()
@@ -574,8 +578,6 @@ def _resolve_media(comp_cls: Type["Component"], comp_media: ComponentMedia) -> N
comp_cls, comp_media, inlined_attr="css", file_attr="css_file", comp_dirs=comp_dirs, type="static"
)
- comp_media.resolved = True
-
def _normalize_media(media: Type[ComponentMediaInput]) -> None:
"""
@@ -736,6 +738,11 @@ def _resolve_component_relative_files(
as the component class. If so, modify the attributes so the class Django's rendering
will pick up these files correctly.
"""
+ if comp_media.resolved_relative_files:
+ return
+
+ comp_media.resolved_relative_files = True
+
# First check if we even need to resolve anything. If the class doesn't define any
# HTML/JS/CSS files, just skip.
will_resolve_files = False
@@ -953,27 +960,47 @@ def _get_asset(
asset_content = getattr(comp_media, inlined_attr, None)
asset_file = getattr(comp_media, file_attr, None)
- if asset_file is not None:
- # Check if the file is in one of the components' directories
- full_path = resolve_file(asset_file, comp_dirs)
+ # No inlined content, nor file name
+ if asset_content is None and asset_file is None:
+ return None
- if full_path is None:
- # If not, check if it's in the static files
- if type == "static":
- full_path = finders.find(asset_file)
- # Or in the templates
- elif type == "template":
- try:
- template: Template = get_template(asset_file)
- full_path = template.origin.name
- except TemplateDoesNotExist:
- pass
+ if asset_content is not None and asset_file is not None:
+ raise ValueError(
+ f"Received both '{inlined_attr}' and '{file_attr}' in Component {comp_cls.__qualname__}."
+ " Only one of the two must be set."
+ )
- if full_path is None:
- # NOTE: The short name, e.g. `js` or `css` is used in the error message for convenience
- raise ValueError(f"Could not find {inlined_attr} file {asset_file}")
+ # If the content was inlined into the component (e.g. `Component.template = "..."`)
+ # then there's nothing to resolve. Return as is.
+ if asset_content is not None:
+ return asset_content
- # NOTE: Use explicit encoding for compat with Windows, see #1074
- asset_content = Path(full_path).read_text(encoding="utf8")
+ # The rest of the code assumes that we were given only a file name
+ asset_file = cast(str, asset_file)
+
+ if type == "template":
+ # NOTE: While we return on the "source" (plain string) of the template,
+ # by calling `load_component_template()`, we also cache the Template instance.
+ # So later in Component's `render_impl()`, we don't have to re-compile the Template.
+ template = load_component_template(comp_cls, asset_file)
+ return template.source
+
+ # For static files, we have a few options:
+ # 1. Check if the file is in one of the components' directories
+ full_path = resolve_file(asset_file, comp_dirs)
+
+ # 2. If not, check if it's in the static files
+ if full_path is None:
+ full_path = finders.find(asset_file)
+
+ if full_path is None:
+ # NOTE: The short name, e.g. `js` or `css` is used in the error message for convenience
+ raise ValueError(f"Could not find {inlined_attr} file {asset_file}")
+
+ # NOTE: Use explicit encoding for compat with Windows, see #1074
+ asset_content = Path(full_path).read_text(encoding="utf8")
+
+ # TODO: Apply `extensions.on_js_preprocess()` and `extensions.on_css_preprocess()`
+ # NOTE: `on_template_preprocess()` is already applied inside `load_component_template()`
return asset_content
diff --git a/src/django_components/slots.py b/src/django_components/slots.py
index e55bed21..ab502ca4 100644
--- a/src/django_components/slots.py
+++ b/src/django_components/slots.py
@@ -1605,7 +1605,7 @@ def _nodelist_to_slot(
if index_of_last_component_layer is None:
index_of_last_component_layer = 0
- # TODO: Currently there's one more layer before the `_COMPONENT_CONTEXT_KEY` layer, which is
+ # TODO_V1: Currently there's one more layer before the `_COMPONENT_CONTEXT_KEY` layer, which is
# pushed in `_prepare_template()` in `component.py`.
# That layer should be removed when `Component.get_template()` is removed, after which
# the following line can be removed.
diff --git a/src/django_components/template.py b/src/django_components/template.py
index 070e21fb..96951504 100644
--- a/src/django_components/template.py
+++ b/src/django_components/template.py
@@ -1,12 +1,24 @@
-from typing import Any, Optional, Type
+import sys
+from contextlib import contextmanager
+from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Type, Union, cast
+from weakref import ReferenceType, ref
-from django.template import Origin, Template
+from django.core.exceptions import ImproperlyConfigured
+from django.template import Context, Origin, Template
+from django.template.loader import get_template as django_get_template
from django_components.cache import get_template_cache
-from django_components.util.misc import get_import_path
+from django_components.util.django_monkeypatch import is_template_cls_patched
+from django_components.util.loader import get_component_dirs
+from django_components.util.logger import trace_component_msg
+from django_components.util.misc import get_import_path, get_module_info
+
+if TYPE_CHECKING:
+ from django_components.component import Component
-# Central logic for creating Templates from string, so we can cache the results
+# TODO_V1 - Remove, won't be needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
+# Legacy logic for creating Templates from string
def cached_template(
template_string: str,
template_cls: Optional[Type[Template]] = None,
@@ -15,6 +27,8 @@ def cached_template(
engine: Optional[Any] = None,
) -> Template:
"""
+ DEPRECATED. Template caching will be removed in v1.
+
Create a Template instance that will be cached as per the
[`COMPONENTS.template_cache_size`](../settings#django_components.app_settings.ComponentsSettings.template_cache_size)
setting.
@@ -62,3 +76,400 @@ def cached_template(
template = maybe_cached_template
return template
+
+
+########################################################
+# PREPARING COMPONENT TEMPLATES FOR RENDERING
+########################################################
+
+
+@contextmanager
+def prepare_component_template(
+ component: "Component",
+ template_data: Any,
+) -> Generator[Optional[Template], Any, None]:
+ context = component.context
+ with context.update(template_data):
+ template = _get_component_template(component)
+
+ if template is None:
+ # If template is None, then the component is "template-less",
+ # and we skip template processing.
+ yield template
+ return
+
+ if not is_template_cls_patched(template):
+ raise RuntimeError(
+ "Django-components received a Template instance which was not patched."
+ "If you are using Django's Template class, check if you added django-components"
+ "to INSTALLED_APPS. If you are using a custom template class, then you need to"
+ "manually patch the class."
+ )
+
+ with _maybe_bind_template(context, template):
+ yield template
+
+
+# `_maybe_bind_template()` handles two problems:
+#
+# 1. Initially, the binding the template was needed for the context processor data
+# to work when using `RequestContext` (See `RequestContext.bind_template()` in e.g. Django v4.2 or v5.1).
+# But as of djc v0.140 (possibly earlier) we generate and apply the context processor data
+# ourselves in `Component._render_impl()`.
+#
+# Now, we still want to "bind the template" by setting the `Context.template` attribute.
+# This is for compatibility with Django, because we don't know if there isn't some code that relies
+# on the `Context.template` attribute being set.
+#
+# But we don't call `context.bind_template()` explicitly. If we did, then we would
+# be generating and applying the context processor data twice if the context was `RequestContext`.
+# Instead, we only run the same logic as `Context.bind_template()` but inlined.
+#
+# The downstream effect of this is that if the user or some third-party library
+# uses custom subclass of `Context` with custom logic for `Context.bind_template()`,
+# then this custom logic will NOT be applied. In such case they should open an issue.
+#
+# See https://github.com/django-components/django-components/issues/580
+# and https://github.com/django-components/django-components/issues/634
+#
+# 2. Not sure if I (Juro) remember right, but I think that with the binding of templates
+# there was also an issue that in *some* cases the template was already bound to the context
+# by the time we got to rendering the component. This is why we need to check if `context.template`
+# is already set.
+#
+# The cause of this may have been compatibility with Django's `{% extends %}` tag, or
+# maybe when using the "isolated" context behavior. But not sure.
+@contextmanager
+def _maybe_bind_template(context: Context, template: Template) -> Generator[None, Any, None]:
+ if context.template is not None:
+ yield
+ return
+
+ # This code is taken from `Context.bind_template()` from Django v5.1
+ context.template = template
+ try:
+ yield
+ finally:
+ context.template = None
+
+
+########################################################
+# LOADING TEMPLATES FROM FILEPATH
+########################################################
+
+
+# Remember which Component class is currently being loaded
+# This is important, because multiple Components may define the same `template_file`.
+# So we need this global state to help us decide which Component class of the list of components
+# that matched for the given `template_file` should be associated with the template.
+#
+# NOTE: Implemented as a list (stack) to handle the case when calling Django's `get_template()`
+# could lead to more components being loaded at once.
+# (For this to happen, user would have to define a Django template loader that renders other components
+# while resolving the template file.)
+loading_components: List["ComponentRef"] = []
+
+
+def load_component_template(component_cls: Type["Component"], filepath: str) -> Template:
+ if component_cls._template is not None:
+ return component_cls._template
+
+ loading_components.append(ref(component_cls))
+
+ # Use Django's `get_template()` to load the template
+ template = _load_django_template(filepath)
+
+ # If template.origin.component_cls is already set, then this
+ # Template instance was cached by Django / template loaders.
+ # In that case we want to make a copy of the template which would
+ # be owned by the current Component class.
+ # Thus each Component has it's own Template instance with their own Origins
+ # pointing to the correct Component class.
+ if get_component_from_origin(template.origin) is not None:
+ origin_copy = Origin(template.origin.name, template.origin.template_name, template.origin.loader)
+ set_component_to_origin(origin_copy, component_cls)
+ template = Template(template.source, origin=origin_copy, name=template.name, engine=template.engine)
+
+ component_cls._template = template
+
+ loading_components.pop()
+
+ return template
+
+
+def _get_component_template(component: "Component") -> Optional[Template]:
+ trace_component_msg("COMP_LOAD", component_name=component.name, component_id=component.id, slot_name=None)
+
+ # TODO_V1 - Remove, not needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
+ template_sources: Dict[str, Optional[Union[str, Template]]] = {}
+
+ # TODO_V1 - Remove `get_template_name()` in v1
+ template_sources["get_template_name"] = component.get_template_name(component.context)
+
+ # TODO_V1 - Remove `get_template_string()` in v1
+ if hasattr(component, "get_template_string"):
+ template_string_getter = getattr(component, "get_template_string")
+ template_body_from_getter = template_string_getter(component.context)
+ else:
+ template_body_from_getter = None
+ template_sources["get_template_string"] = template_body_from_getter
+
+ # TODO_V1 - Remove `get_template()` in v1
+ template_sources["get_template"] = component.get_template(component.context)
+
+ # NOTE: `component.template` should be populated whether user has set `template` or `template_file`
+ # so we discern between the two cases by checking `component.template_file`
+ if component.template_file is not None:
+ template_sources["template_file"] = component.template_file
+ else:
+ template_sources["template"] = component.template
+
+ # TODO_V1 - Remove this check in v1
+ # Raise if there are multiple sources for the component template
+ sources_with_values = [k for k, v in template_sources.items() if v is not None]
+ if len(sources_with_values) > 1:
+ raise ImproperlyConfigured(
+ f"Component template was set multiple times in Component {component.name}."
+ f"Sources: {sources_with_values}"
+ )
+
+ # Load the template based on the source
+ if template_sources["get_template_name"]:
+ template_name = template_sources["get_template_name"]
+ template: Optional[Template] = _load_django_template(template_name)
+ template_string: Optional[str] = None
+ elif template_sources["get_template_string"]:
+ template_string = template_sources["get_template_string"]
+ template = None
+ elif template_sources["get_template"]:
+ # `Component.get_template()` returns either string or Template instance
+ if hasattr(template_sources["get_template"], "render"):
+ template = template_sources["get_template"]
+ template_string = None
+ else:
+ template = None
+ template_string = template_sources["get_template"]
+ elif component.template or component.template_file:
+ # If the template was loaded from `Component.template_file`, then the Template
+ # instance was already created and cached in `Component._template`.
+ #
+ # NOTE: This is important to keep in mind, because the implication is that we should
+ # treat Templates AND their nodelists as IMMUTABLE.
+ if component.__class__._template is not None:
+ template = component.__class__._template
+ template_string = None
+ # Otherwise user have set `Component.template` as string and we still need to
+ # create the instance.
+ else:
+ template = _create_template_from_string(
+ component,
+ # NOTE: We can't reach this branch if `Component.template` is None
+ cast(str, component.template),
+ is_component_template=True,
+ )
+ template_string = None
+ # No template
+ else:
+ template = None
+ template_string = None
+
+ # We already have a template instance, so we can return it
+ if template is not None:
+ return template
+ # Create the template from the string
+ elif template_string is not None:
+ return _create_template_from_string(component, template_string)
+
+ # Otherwise, Component has no template - this is valid, as it may be instead rendered
+ # via `Component.on_render()`
+ return None
+
+
+def _create_template_from_string(
+ component: "Component",
+ template_string: str,
+ is_component_template: bool = False,
+) -> Template:
+ # Generate a valid Origin instance.
+ # When an Origin instance is created by Django when using Django's loaders, it looks like this:
+ # ```
+ # {
+ # 'name': '/path/to/project/django-components/sampleproject/calendarapp/templates/calendarapp/calendar.html',
+ # 'template_name': 'calendarapp/calendar.html',
+ # 'loader':
+ # }
+ # ```
+ #
+ # Since our template is inlined, we will format as `filepath::ComponentName`
+ #
+ # ```
+ # /path/to/project/django-components/src/calendarapp/calendar.html::Calendar
+ # ```
+ #
+ # See https://docs.djangoproject.com/en/5.2/howto/custom-template-backend/#template-origin-api
+ _, _, module_filepath = get_module_info(component.__class__)
+ origin = Origin(
+ name=f"{module_filepath}::{component.__class__.__name__}",
+ template_name=None,
+ loader=None,
+ )
+
+ set_component_to_origin(origin, component.__class__)
+
+ if is_component_template:
+ template = Template(template_string, name=origin.template_name, origin=origin)
+ component.__class__._template = template
+ else:
+ # TODO_V1 - `cached_template()` won't be needed as there will be only 1 template per component
+ # so we will be able to instead use `template_cache` to store the template
+ template = cached_template(
+ template_string=template_string,
+ name=origin.template_name,
+ origin=origin,
+ )
+
+ return template
+
+
+# When loading a template, use Django's `get_template()` to ensure it triggers Django template loaders
+# See https://github.com/django-components/django-components/issues/901
+#
+# This may raise `TemplateDoesNotExist` if the template doesn't exist.
+# See https://docs.djangoproject.com/en/5.2/ref/templates/api/#template-loaders
+# And https://docs.djangoproject.com/en/5.2/ref/templates/api/#custom-template-loaders
+#
+# TODO_v3 - Instead of loading templates with Django's `get_template()`,
+# we should simply read the files directly (same as we do for JS and CSS).
+# This has the implications that:
+# - We would no longer support Django's template loaders
+# - Instead if users are using template loaders, they should re-create them as djc extensions
+# - We would no longer need to set `TEMPLATES.OPTIONS.loaders` to include
+# `django_components.template_loader.Loader`
+def _load_django_template(template_name: str) -> Template:
+ return django_get_template(template_name).template
+
+
+########################################################
+# ASSOCIATING COMPONENT CLASSES WITH TEMPLATES
+#
+# See https://github.com/django-components/django-components/pull/1222
+########################################################
+
+# NOTE: `ReferenceType` is NOT a generic pre-3.9
+if sys.version_info >= (3, 9):
+ ComponentRef = ReferenceType[Type["Component"]]
+else:
+ ComponentRef = ReferenceType
+
+
+# Remember which Component classes defined `template_file`. Since multiple Components may
+# define the same `template_file`, we store a list of weak references to the Component classes.
+component_template_file_cache: Dict[str, List[ComponentRef]] = {}
+component_template_file_cache_initialized = False
+
+
+# Remember the mapping of `Component.template_file` -> `Component` class, so that we can associate
+# the `Template` instances with the correct Component class in our monkepatched `Template.__init__()`.
+def cache_component_template_file(component_cls: Type["Component"]) -> None:
+ # When a Component class is created before Django is set up,
+ # then `component_template_file_cache_initialized` is False and we leave it for later.
+ # This is necessary because:
+ # 1. We might need to resolve the template_file as relative to the file where the Component class is defined.
+ # 2. To be able to resolve the template_file, Django needs to be set up, because we need to access Django settings.
+ # 3. Django settings may not be available at the time of Component class creation.
+ if not component_template_file_cache_initialized:
+ return
+
+ # NOTE: Avoids circular import
+ from django_components.component_media import ComponentMedia, _resolve_component_relative_files
+
+ # If we access the `Component.template_file` attribute, then this triggers media resolution if it was not done yet.
+ # The problem is that this also causes the loading of the Template, if Component has defined `template_file`.
+ # This triggers `Template.__init__()`, which then triggers another call to `cache_component_template_file()`.
+ #
+ # At the same time, at this point we don't need the media files to be loaded. But we DO need for the relative
+ # file path to be resolved.
+ #
+ # So for this reason, `ComponentMedia.resolved_relative_files` was added to track if the media files were resolved.
+ # Once relative files were resolved, we can safely access the template file from `ComponentMedia` instance
+ # directly, thus avoiding the triggering of the Template loading.
+ comp_media: ComponentMedia = component_cls._component_media # type: ignore[attr-defined]
+ if comp_media.resolved and comp_media.resolved_relative_files:
+ template_file = component_cls.template_file
+ else:
+ # NOTE: This block of code is based on `_resolve_media()` in `component_media.py`
+ if not comp_media.resolved_relative_files:
+ comp_dirs = get_component_dirs()
+ _resolve_component_relative_files(component_cls, comp_media, comp_dirs=comp_dirs)
+
+ template_file = comp_media.template_file
+
+ if template_file is None:
+ return
+
+ if template_file not in component_template_file_cache:
+ component_template_file_cache[template_file] = []
+
+ component_template_file_cache[template_file].append(ref(component_cls))
+
+
+def get_component_by_template_file(template_file: str) -> Optional[Type["Component"]]:
+ # This function is called from within `Template.__init__()`. At that point, Django MUST be already set up,
+ # because Django's `Template.__init__()` accesses the templating engines.
+ #
+ # So at this point we want to call `cache_component_template_file()` for all Components for which
+ # we skipped it earlier.
+ global component_template_file_cache_initialized
+ if not component_template_file_cache_initialized:
+ component_template_file_cache_initialized = True
+
+ # NOTE: Avoids circular import
+ from django_components.component import all_components
+
+ components = all_components()
+ for component in components:
+ cache_component_template_file(component)
+
+ if template_file not in component_template_file_cache or not len(component_template_file_cache[template_file]):
+ return None
+
+ # There is at least one Component class that has this `template_file`.
+ matched_component_refs = component_template_file_cache[template_file]
+
+ # There may be multiple components that define the same `template_file`.
+ # So to find the correct one, we need to check if the currently loading component
+ # is one of the ones that define the `template_file`.
+ #
+ # If there are NO currently loading components, then `Template.__init__()` was NOT triggered by us,
+ # in which case we don't associate any Component class with this Template.
+ if not len(loading_components):
+ return None
+
+ loading_component = loading_components[-1]()
+ if loading_component is None:
+ return None
+
+ for component_ref in matched_component_refs:
+ comp_cls = component_ref()
+ if comp_cls is loading_component:
+ return comp_cls
+
+ return None
+
+
+# NOTE: Used by `@djc_test` to reset the component template file cache
+def _reset_component_template_file_cache() -> None:
+ global component_template_file_cache
+ component_template_file_cache = {}
+
+ global component_template_file_cache_initialized
+ component_template_file_cache_initialized = False
+
+
+# Helpers so we know where in the codebase we set / access the `Origin.component_cls` attribute
+def set_component_to_origin(origin: Origin, component_cls: Type["Component"]) -> None:
+ origin.component_cls = component_cls
+
+
+def get_component_from_origin(origin: Origin) -> Optional[Type["Component"]]:
+ return getattr(origin, "component_cls", None)
diff --git a/src/django_components/template_loader.py b/src/django_components/template_loader.py
index b6d48097..4545ffda 100644
--- a/src/django_components/template_loader.py
+++ b/src/django_components/template_loader.py
@@ -10,7 +10,7 @@ from django.template.loaders.filesystem import Loader as FilesystemLoader
from django_components.util.loader import get_component_dirs
-class Loader(FilesystemLoader):
+class DjcLoader(FilesystemLoader):
def get_dirs(self, include_apps: bool = True) -> List[Path]:
"""
Prepare directories that may contain component files:
@@ -26,3 +26,10 @@ class Loader(FilesystemLoader):
`BASE_DIR` setting is required.
"""
return get_component_dirs(include_apps)
+
+
+# NOTE: Django's template loaders have the pattern of using the `Loader` class name.
+# However, this then makes it harder to track and distinguish between different loaders.
+# So internally we use the name `DjcLoader` instead.
+# But for public API we use the name `Loader` to match Django.
+Loader = DjcLoader
diff --git a/src/django_components/util/django_monkeypatch.py b/src/django_components/util/django_monkeypatch.py
index 5191a62a..6ee295b0 100644
--- a/src/django_components/util/django_monkeypatch.py
+++ b/src/django_components/util/django_monkeypatch.py
@@ -1,7 +1,7 @@
-from typing import Any, Type
+from typing import Any, Optional, Type
from django.template import Context, NodeList, Template
-from django.template.base import Parser
+from django.template.base import Origin, Parser
from django_components.context import _COMPONENT_CONTEXT_KEY, _STRATEGY_CONTEXT_KEY
from django_components.dependencies import COMPONENT_COMMENT_REGEX, render_dependencies
@@ -10,11 +10,61 @@ from django_components.util.template_parser import parse_template
# In some cases we can't work around Django's design, and need to patch the template class.
def monkeypatch_template_cls(template_cls: Type[Template]) -> None:
+ monkeypatch_template_init(template_cls)
monkeypatch_template_compile_nodelist(template_cls)
monkeypatch_template_render(template_cls)
template_cls._djc_patched = True
+# Patch `Template.__init__` to apply `extensions.on_template_preprocess()` if the template
+# belongs to a Component.
+def monkeypatch_template_init(template_cls: Type[Template]) -> None:
+ original_init = template_cls.__init__
+
+ # NOTE: Function signature of Template.__init__ hasn't changed in 11 years, so we can safely patch it.
+ # See https://github.com/django/django/blame/main/django/template/base.py#L139
+ def __init__(
+ self: Template,
+ template_string: Any,
+ origin: Optional[Origin] = None,
+ *args: Any,
+ **kwargs: Any,
+ ) -> None:
+ # NOTE: Avoids circular import
+ from django_components.template import (
+ get_component_by_template_file,
+ get_component_from_origin,
+ set_component_to_origin,
+ )
+
+ # If this Template instance was created by us when loading a template file for a component
+ # with `load_component_template()`, then we do 2 things:
+ #
+ # 1. Associate the Component class with the template by setting it on the `Origin` instance
+ # (`template.origin.component_cls`). This way the `{% component%}` and `{% slot %}` tags
+ # will know inside which Component class they were defined.
+ #
+ # 2. Apply `extensions.on_template_preprocess()` to the template, so extensions can modify
+ # the template string before it's compiled into a nodelist.
+ if get_component_from_origin(origin) is not None:
+ component_cls = get_component_from_origin(origin)
+ elif origin is not None and origin.template_name is not None:
+ component_cls = get_component_by_template_file(origin.template_name)
+ if component_cls is not None:
+ set_component_to_origin(origin, component_cls)
+ else:
+ component_cls = None
+
+ if component_cls is not None:
+ # TODO - Apply extensions.on_template_preprocess() here.
+ # Then also test both cases when template as `template` or `template_file`.
+ pass
+
+ original_init(self, template_string, origin, *args, **kwargs) # type: ignore[misc]
+
+ template_cls.__init__ = __init__
+
+
# Patch `Template.compile_nodelist` to use our custom parser. Our parser makes it possible
# to use template tags as inputs to the component tag:
#
@@ -94,6 +144,8 @@ def monkeypatch_template_render(template_cls: Type[Template]) -> None:
# and `False` otherwise.
isolated_context = not self._djc_is_component_nested
+ # This is original implementation, except we override `isolated_context`,
+ # and we post-process the result with `render_dependencies()`.
with context.render_context.push_state(self, isolated_context=isolated_context):
if context.template is None:
with context.bind_template(self):
diff --git a/src/django_components/util/testing.py b/src/django_components/util/testing.py
index 1bb6bae6..2d708896 100644
--- a/src/django_components/util/testing.py
+++ b/src/django_components/util/testing.py
@@ -10,12 +10,14 @@ import django
from django.conf import settings as _django_settings
from django.core.cache import BaseCache, caches
from django.template import engines
+from django.template.loaders.base import Loader
from django.test import override_settings
from django_components.component import ALL_COMPONENTS, Component, component_node_subclasses_by_name
from django_components.component_media import ComponentMedia
from django_components.component_registry import ALL_REGISTRIES, ComponentRegistry
from django_components.extension import extensions
+from django_components.template import _reset_component_template_file_cache, loading_components
# NOTE: `ReferenceType` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
@@ -457,7 +459,9 @@ def _clear_djc_global_state(
# beause the IDs count will reset to 0, but we won't generate IDs for the Nodes of the cached
# templates. Thus, the IDs will be out of sync between the tests.
for engine in engines.all():
- engine.engine.template_loaders[0].reset()
+ for loader in engine.engine.template_loaders:
+ if isinstance(loader, Loader):
+ loader.reset()
# NOTE: There are 1-2 tests which check Templates, so we need to clear the cache
from django_components.cache import component_media_cache, template_cache
@@ -533,6 +537,10 @@ def _clear_djc_global_state(
# Clear extensions caches
extensions._route_to_url.clear()
+ # Clear other djc state
+ _reset_component_template_file_cache()
+ loading_components.clear()
+
# Clear Django caches
all_caches: List[BaseCache] = list(caches.all())
for cache in all_caches:
diff --git a/tests/test_benchmark_django.py b/tests/test_benchmark_django.py
index 591eeb59..8d1ea2e0 100644
--- a/tests/test_benchmark_django.py
+++ b/tests/test_benchmark_django.py
@@ -66,7 +66,6 @@ if not settings.configured:
}
],
COMPONENTS={
- "template_cache_size": 128,
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},
diff --git a/tests/test_benchmark_django_small.py b/tests/test_benchmark_django_small.py
index 9d9f9059..7ac92eda 100644
--- a/tests/test_benchmark_django_small.py
+++ b/tests/test_benchmark_django_small.py
@@ -37,7 +37,6 @@ if not settings.configured:
}
],
COMPONENTS={
- "template_cache_size": 128,
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},
diff --git a/tests/test_benchmark_djc.py b/tests/test_benchmark_djc.py
index 9eb89f5b..a99bfdd6 100644
--- a/tests/test_benchmark_djc.py
+++ b/tests/test_benchmark_djc.py
@@ -66,7 +66,6 @@ if not settings.configured:
}
],
COMPONENTS={
- "template_cache_size": 128,
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},
diff --git a/tests/test_benchmark_djc_small.py b/tests/test_benchmark_djc_small.py
index 983422c2..5c96b3bc 100644
--- a/tests/test_benchmark_djc_small.py
+++ b/tests/test_benchmark_djc_small.py
@@ -37,7 +37,6 @@ if not settings.configured:
}
],
COMPONENTS={
- "template_cache_size": 128,
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},
diff --git a/tests/test_component.py b/tests/test_component.py
index 907eef59..a6ee40fe 100644
--- a/tests/test_component.py
+++ b/tests/test_component.py
@@ -8,7 +8,6 @@ from typing import Any, NamedTuple
import pytest
from django.conf import settings
-from django.core.exceptions import ImproperlyConfigured
from django.http import HttpRequest, HttpResponse
from django.template import Context, RequestContext, Template
from django.template.base import TextNode
@@ -26,6 +25,7 @@ from django_components import (
register,
types,
)
+from django_components.template import _get_component_template
from django_components.urls import urlpatterns as dc_urlpatterns
from django_components.testing import djc_test
@@ -152,23 +152,43 @@ class TestComponentLegacyApi:
""",
)
-
-@djc_test
-class TestComponent:
+ # TODO_v1 - Remove
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
- def test_empty_component(self, components_settings):
- class EmptyComponent(Component):
- pass
+ def test_get_template_name(self, components_settings):
+ class SvgComponent(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "name": kwargs.pop("name", None),
+ "css_class": kwargs.pop("css_class", None),
+ "title": kwargs.pop("title", None),
+ **kwargs,
+ }
- with pytest.raises(ImproperlyConfigured):
- EmptyComponent.render(args=["123"])
+ def get_template_name(self, context):
+ return f"dynamic_{context['name']}.svg"
- @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
- def test_template_string_static_inlined(self, components_settings):
- class SimpleComponent(Component):
- template: types.django_html = """
- Variable: {{ variable }}
+ assertHTMLEqual(
+ SvgComponent.render(kwargs={"name": "svg1"}),
"""
+
+ """,
+ )
+ assertHTMLEqual(
+ SvgComponent.render(kwargs={"name": "svg2"}),
+ """
+
+ """,
+ )
+
+ # TODO_v1 - Remove
+ @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
+ def test_get_template__string(self, components_settings):
+ class SimpleComponent(Component):
+ def get_template(self, context):
+ content: types.django_html = """
+ Variable: {{ variable }}
+ """
+ return content
def get_template_data(self, args, kwargs, slots, context):
return {
@@ -187,8 +207,29 @@ class TestComponent:
""",
)
+ # TODO_v1 - Remove
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
- def test_template_string_dynamic(self, components_settings):
+ def test_get_template__template(self, components_settings):
+ class TestComponent(Component):
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "variable": kwargs.pop("variable", None),
+ }
+
+ def get_template(self, context):
+ template_str = "Variable: {{ variable }}"
+ return Template(template_str)
+
+ rendered = TestComponent.render(kwargs={"variable": "test"})
+ assertHTMLEqual(
+ rendered,
+ """
+ Variable: test
+ """,
+ )
+
+ # TODO_v1 - Remove
+ def test_get_template_is_cached(self):
class SimpleComponent(Component):
def get_template(self, context):
content: types.django_html = """
@@ -201,6 +242,35 @@ class TestComponent:
"variable": kwargs.get("variable", None),
}
+ comp = SimpleComponent()
+ template_1 = _get_component_template(comp)
+ template_1._test_id = "123" # type: ignore[union-attr]
+
+ template_2 = _get_component_template(comp)
+ assert template_2._test_id == "123" # type: ignore[union-attr]
+
+
+@djc_test
+class TestComponent:
+ @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
+ def test_empty_component(self, components_settings):
+ class EmptyComponent(Component):
+ pass
+
+ EmptyComponent.render(args=["123"])
+
+ @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
+ def test_template_string_static_inlined(self, components_settings):
+ class SimpleComponent(Component):
+ template: types.django_html = """
+ Variable: {{ variable }}
+ """
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "variable": kwargs.get("variable", None),
+ }
+
class Media:
css = "style.css"
js = "script.js"
@@ -235,6 +305,90 @@ class TestComponent:
""",
)
+ # Test that even with cached template loaders, each Component has its own `Template`
+ # even when multiple components point to the same template file.
+ @djc_test(
+ parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR,
+ django_settings={
+ "TEMPLATES": [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [
+ "tests/templates/",
+ "tests/components/",
+ ],
+ "OPTIONS": {
+ "builtins": [
+ "django_components.templatetags.component_tags",
+ ],
+ 'loaders': [
+ ('django.template.loaders.cached.Loader', [
+
+ # Default Django loader
+ 'django.template.loaders.filesystem.Loader',
+ # Including this is the same as APP_DIRS=True
+ 'django.template.loaders.app_directories.Loader',
+ # Components loader
+ 'django_components.template_loader.Loader',
+ ]),
+ ],
+ },
+ }
+ ],
+ },
+ )
+ def test_template_file_static__cached(self, components_settings):
+ class SimpleComponent1(Component):
+ template_file = "simple_template.html"
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "variable": kwargs.get("variable", None),
+ }
+
+ class SimpleComponent2(Component):
+ template_file = "simple_template.html"
+
+ def get_template_data(self, args, kwargs, slots, context):
+ return {
+ "variable": kwargs.get("variable", None),
+ }
+
+ SimpleComponent1.template # Triggers template loading
+ SimpleComponent2.template # Triggers template loading
+
+ # Both components have their own Template instance, but they point to the same template file.
+ assert isinstance(SimpleComponent1._template, Template)
+ assert isinstance(SimpleComponent2._template, Template)
+ assert SimpleComponent1._template is not SimpleComponent2._template
+ assert SimpleComponent1._template.source == SimpleComponent2._template.source
+
+ # The Template instances have different origins, but they point to the same template file.
+ assert SimpleComponent1._template.origin is not SimpleComponent2._template.origin
+ assert SimpleComponent1._template.origin.template_name == SimpleComponent2._template.origin.template_name
+ assert SimpleComponent1._template.origin.name == SimpleComponent2._template.origin.name
+ assert SimpleComponent1._template.origin.loader == SimpleComponent2._template.origin.loader
+
+ # The origins point to their respective Component classes.
+ assert SimpleComponent1._template.origin.component_cls == SimpleComponent1
+ assert SimpleComponent2._template.origin.component_cls == SimpleComponent2
+
+ rendered = SimpleComponent1.render(kwargs={"variable": "test"})
+ assertHTMLEqual(
+ rendered,
+ """
+ Variable: test
+ """,
+ )
+
+ rendered = SimpleComponent2.render(kwargs={"variable": "test"})
+ assertHTMLEqual(
+ rendered,
+ """
+ Variable: test
+ """,
+ )
+
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_file_static__compat(self, components_settings):
class SimpleComponent(Component):
@@ -288,53 +442,6 @@ class TestComponent:
""",
)
- @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
- def test_template_file_dynamic(self, components_settings):
- class SvgComponent(Component):
- def get_template_data(self, args, kwargs, slots, context):
- return {
- "name": kwargs.pop("name", None),
- "css_class": kwargs.pop("css_class", None),
- "title": kwargs.pop("title", None),
- **kwargs,
- }
-
- def get_template_name(self, context):
- return f"dynamic_{context['name']}.svg"
-
- assertHTMLEqual(
- SvgComponent.render(kwargs={"name": "svg1"}),
- """
-
- """,
- )
- assertHTMLEqual(
- SvgComponent.render(kwargs={"name": "svg2"}),
- """
-
- """,
- )
-
- @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
- def test_allows_to_return_template(self, components_settings):
- class TestComponent(Component):
- def get_template_data(self, args, kwargs, slots, context):
- return {
- "variable": kwargs.pop("variable", None),
- }
-
- def get_template(self, context):
- template_str = "Variable: {{ variable }}"
- return Template(template_str)
-
- rendered = TestComponent.render(kwargs={"variable": "test"})
- assertHTMLEqual(
- rendered,
- """
- Variable: test
- """,
- )
-
def test_get_component_by_id(self):
class SimpleComponent(Component):
pass
@@ -369,6 +476,12 @@ class TestComponentRenderAPI:
def test_input(self):
class TestComponent(Component):
+ template: types.django_html = """
+ {% load component_tags %}
+ Variable: {{ variable }}
+ {% slot 'my_slot' / %}
+ """
+
def get_template_data(self, args, kwargs, slots, context):
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
@@ -381,7 +494,7 @@ class TestComponentRenderAPI:
"variable": kwargs["variable"],
}
- def get_template(self, context):
+ def on_render_before(self, context, template):
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
assert isinstance(self.input.context, Context)
@@ -389,13 +502,6 @@ class TestComponentRenderAPI:
my_slot = self.input.slots["my_slot"]
assert my_slot() == "MY_SLOT"
- template_str: types.django_html = """
- {% load component_tags %}
- Variable: {{ variable }}
- {% slot 'my_slot' / %}
- """
- return Template(template_str)
-
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
diff --git a/tests/test_component_media.py b/tests/test_component_media.py
index e693391a..b3e9f2cd 100644
--- a/tests/test_component_media.py
+++ b/tests/test_component_media.py
@@ -210,17 +210,6 @@ class TestMainMedia:
assert AppLvlCompComponent._component_media.js == 'console.log("JS file");\n' # type: ignore[attr-defined]
assert AppLvlCompComponent._component_media.js_file == "app_lvl_comp/app_lvl_comp.js" # type: ignore[attr-defined]
- def test_html_variable(self):
- class VariableHTMLComponent(Component):
- def get_template(self, context):
- return Template("