refactor: deprecate template caching, get_template_name, get_template, assoc template with Comp cls (#1222)

* refactor: deprecate template caching, get_template_name, get_template, assoc template with Comp cls

* refactor: change implementation

* refactor: handle cached template loader

* refactor: fix tests

* refactor: fix test on windows

* refactor: try to  fix type errors

* refactor: Re-cast `context` to fix type errors

* refactor: fix linter error

* refactor: fix typing

* refactor: more linter fixes

* refactor: more linter errors

* refactor: revert extra node metadata
This commit is contained in:
Juro Oravec 2025-06-01 19:20:22 +02:00 committed by GitHub
parent fa9ae9892f
commit 8677ee7941
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1548 additions and 652 deletions

View file

@ -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

View file

@ -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]
```

View file

@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3172" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L3301" target="_blank">See source code</a>

View file

@ -94,7 +94,6 @@ COMPONENTS = ComponentsSettings(
dirs=[BASE_DIR / "components"],
# app_dirs=["components"],
# libraries=[],
# template_cache_size=128,
# context_behavior="isolated", # "django" | "isolated"
)

View file

@ -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`.

View file

@ -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:

View file

@ -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:

View file

@ -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,12 +650,14 @@ 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. `<root>/components/`).
- Relative to the template directories, as set by Django's `TEMPLATES` setting (e.g. `<root>/templates/`).
!!! 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)
@ -659,12 +665,32 @@ class Component(metaclass=ComponentMeta):
**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),
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.
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 = '''
<div>
{{ my_var }}
</div>
'''
```
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 = '''
<div>
{{ my_var }}
</div>
'''
```
"""
# 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),
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)
or [`get_template`](../api#django_components.Component.get_template) must be defined.
[`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,6 +1096,8 @@ class Component(metaclass=ComponentMeta):
"""
Main JS associated with this component inlined as string.
!!! warning
Only one of [`js`](../api#django_components.Component.js) or
[`js_file`](../api#django_components.Component.js_file) must be defined.
@ -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. `<root>/components/`).
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
@ -1012,6 +1145,8 @@ 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).
!!! warning
Only one of [`js`](../api#django_components.Component.js) or
[`js_file`](../api#django_components.Component.js_file) must be defined.
@ -1244,6 +1379,8 @@ class Component(metaclass=ComponentMeta):
"""
Main CSS associated with this component inlined as string.
!!! warning
Only one of [`css`](../api#django_components.Component.css) or
[`css_file`](../api#django_components.Component.css_file) must be defined.
@ -1257,6 +1394,24 @@ class Component(metaclass=ComponentMeta):
}
\"\"\"
```
**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. `<root>/components/`).
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
@ -1279,6 +1434,8 @@ 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).
!!! warning
Only one of [`css`](../api#django_components.Component.css) or
[`css_file`](../api#django_components.Component.css_file) must be defined.
@ -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,6 +3224,8 @@ class Component(metaclass=ComponentMeta):
# Emit signal that the template is about to be rendered
template_rendered.send(sender=template, template=template, context=context)
if template is not None:
# Get the component's HTML
html_content = template.render(context)
@ -3109,6 +3246,9 @@ class Component(metaclass=ComponentMeta):
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

View file

@ -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)
```
"""
# Do not resolve if this is a base class
if get_import_path(comp_cls) == "django_components.component.Component" or comp_media.resolved:
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":
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,21 +960,38 @@ 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
# No inlined content, nor file name
if asset_content is None and asset_file is None:
return None
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 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
# 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:
# 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 full_path is None:
# NOTE: The short name, e.g. `js` or `css` is used in the error message for convenience
@ -976,4 +1000,7 @@ def _get_asset(
# 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

View file

@ -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.

View file

@ -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': <django.template.loaders.app_directories.Loader object at 0x10b441d90>
# }
# ```
#
# 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)

View file

@ -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

View file

@ -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):

View file

@ -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:

View file

@ -66,7 +66,6 @@ if not settings.configured:
}
],
COMPONENTS={
"template_cache_size": 128,
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},

View file

@ -37,7 +37,6 @@ if not settings.configured:
}
],
COMPONENTS={
"template_cache_size": 128,
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},

View file

@ -66,7 +66,6 @@ if not settings.configured:
}
],
COMPONENTS={
"template_cache_size": 128,
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},

View file

@ -37,7 +37,6 @@ if not settings.configured:
}
],
COMPONENTS={
"template_cache_size": 128,
"autodiscover": False,
"context_behavior": CONTEXT_MODE,
},

View file

@ -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"
assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg1"}),
"""
<svg data-djc-id-ca1bc3e>Dynamic1</svg>
""",
)
assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg2"}),
"""
<svg data-djc-id-ca1bc3f>Dynamic2</svg>
""",
)
# TODO_v1 - Remove
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_template_string_static_inlined(self, components_settings):
def test_get_template__string(self, components_settings):
class SimpleComponent(Component):
template: types.django_html = """
def get_template(self, context):
content: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
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: <strong>{{ variable }}</strong>"
return Template(template_str)
rendered = TestComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
# 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: <strong>{{ variable }}</strong>
"""
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: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
rendered = SimpleComponent2.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3f>test</strong>
""",
)
@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"}),
"""
<svg data-djc-id-ca1bc3e>Dynamic1</svg>
""",
)
assertHTMLEqual(
SvgComponent.render(kwargs={"name": "svg2"}),
"""
<svg data-djc-id-ca1bc3f>Dynamic2</svg>
""",
)
@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: <strong>{{ variable }}</strong>"
return Template(template_str)
rendered = TestComponent.render(kwargs={"variable": "test"})
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-ca1bc3e>test</strong>
""",
)
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: <strong>{{ variable }}</strong>
{% 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: <strong>{{ variable }}</strong>
{% slot 'my_slot' / %}
"""
return Template(template_str)
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),

View file

@ -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("<div class='variable-html'>{{ variable }}</div>")
rendered = VariableHTMLComponent.render(context=Context({"variable": "Dynamic Content"}))
assertHTMLEqual(
rendered,
'<div class="variable-html" data-djc-id-ca1bc3e>Dynamic Content</div>',
)
def test_html_variable_filtered(self):
class FilteredComponent(Component):
template: types.django_html = """
@ -851,6 +840,7 @@ class TestMediaStaticfiles:
@djc_test
class TestMediaRelativePath:
def _gen_parent_component(self):
class ParentComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -871,6 +861,9 @@ class TestMediaRelativePath:
def get_template_data(self, args, kwargs, slots, context):
return {"shadowing_variable": "NOT SHADOWED"}
return ParentComponent
def _gen_variable_display_component(self):
class VariableDisplay(Component):
template: types.django_html = """
{% load component_tags %}
@ -886,6 +879,8 @@ class TestMediaRelativePath:
context["unique_variable"] = kwargs["new_variable"]
return context
return VariableDisplay
# Settings required for autodiscover to work
@djc_test(
django_settings={
@ -896,8 +891,8 @@ class TestMediaRelativePath:
}
)
def test_component_with_relative_media_paths(self):
registry.register(name="parent_component", component=self.ParentComponent)
registry.register(name="variable_display", component=self.VariableDisplay)
registry.register(name="parent_component", component=self._gen_parent_component())
registry.register(name="variable_display", component=self._gen_variable_display_component())
# Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file.relative_file" in sys.modules:
@ -948,8 +943,8 @@ class TestMediaRelativePath:
}
)
def test_component_with_relative_media_paths_as_subcomponent(self):
registry.register(name="parent_component", component=self.ParentComponent)
registry.register(name="variable_display", component=self.VariableDisplay)
registry.register(name="parent_component", component=self._gen_parent_component())
registry.register(name="variable_display", component=self._gen_variable_display_component())
# Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file.relative_file" in sys.modules:
@ -995,8 +990,8 @@ class TestMediaRelativePath:
https://github.com/django-components/django-components/issues/522#issuecomment-2173577094
"""
registry.register(name="parent_component", component=self.ParentComponent)
registry.register(name="variable_display", component=self.VariableDisplay)
registry.register(name="parent_component", component=self._gen_parent_component())
registry.register(name="variable_display", component=self._gen_variable_display_component())
# Ensure that the module is executed again after import in autodiscovery
if "tests.components.relative_file_pathobj.relative_file_pathobj" in sys.modules:
@ -1066,7 +1061,7 @@ class TestSubclassingMedia:
js = "grandparent.js"
class ParentComponent(GrandParentComponent):
Media = None
Media = None # type: ignore[assignment]
class ChildComponent(ParentComponent):
class Media:
@ -1149,7 +1144,7 @@ class TestSubclassingMedia:
js = "parent1.js"
class Parent2Component(GrandParent3Component, GrandParent4Component):
Media = None
Media = None # type: ignore[assignment]
class ChildComponent(Parent1Component, Parent2Component):
template: types.django_html = """

View file

@ -24,7 +24,8 @@ def dummy_context_processor(request):
#########################
class SimpleComponent(Component):
def gen_simple_component():
class SimpleComponent(Component):
template: types.django_html = """
Variable: <strong>{{ variable }}</strong>
"""
@ -32,8 +33,11 @@ class SimpleComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
return {"variable": kwargs.get("variable", None)} if "variable" in kwargs else {}
return SimpleComponent
class VariableDisplay(Component):
def gen_variable_display_component():
class VariableDisplay(Component):
template: types.django_html = """
{% load component_tags %}
<h1>Shadowing variable = {{ shadowing_variable }}</h1>
@ -48,8 +52,11 @@ class VariableDisplay(Component):
context["unique_variable"] = kwargs["new_variable"]
return context
return VariableDisplay
class IncrementerComponent(Component):
def gen_incrementer_component():
class IncrementerComponent(Component):
template: types.django_html = """
{% load component_tags %}
<p class="incrementer">value={{ value }};calls={{ calls }}</p>
@ -68,14 +75,10 @@ class IncrementerComponent(Component):
self.call_count = 1
return {"value": value + 1, "calls": self.call_count}
#########################
# TESTS
#########################
return IncrementerComponent
@djc_test
class TestContext:
def gen_parent_component():
class ParentComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -96,129 +99,10 @@ class TestContext:
def get_template_data(self, args, kwargs, slots, context):
return {"shadowing_variable": "NOT SHADOWED"}
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
self, components_settings,
):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
self, components_settings,
):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc43>Uniquely named variable = unique_val</h1>", rendered)
assertInHTML(
"<h1 data-djc-id-ca1bc44>Uniquely named variable = slot_default_unique</h1>",
rendered,
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_parent_with_filled_slots(self, components_settings):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc45>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_instances_have_unique_context_with_filled_slots(self, components_settings):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc45>Uniquely named variable = unique_val</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc46>Uniquely named variable = unique_from_slot</h1>", rendered)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(
self, components_settings,
):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_outer_context_with_filled_slots(
self, components_settings,
):
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="parent_component", component=self.ParentComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
assertInHTML("<h1 data-djc-id-ca1bc45>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
return ParentComponent
@djc_test
class TestParentArgs:
def gen_parent_component_with_args():
class ParentComponentWithArgs(Component):
template: types.django_html = """
{% load component_tags %}
@ -239,11 +123,144 @@ class TestParentArgs:
def get_template_data(self, args, kwargs, slots, context):
return {"inner_parent_value": kwargs["parent_value"]}
return ParentComponentWithArgs
#########################
# TESTS
#########################
@djc_test
class TestContext:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
self, components_settings,
):
registry.register(name="variable_display", component=gen_variable_display_component())
registry.register(name="parent_component", component=gen_parent_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
self, components_settings,
):
registry.register(name="variable_display", component=gen_variable_display_component())
registry.register(name="parent_component", component=gen_parent_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc43>Uniquely named variable = unique_val</h1>", rendered)
assertInHTML(
"<h1 data-djc-id-ca1bc44>Uniquely named variable = slot_default_unique</h1>",
rendered,
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_parent_with_filled_slots(self, components_settings):
registry.register(name="variable_display", component=gen_variable_display_component())
registry.register(name="parent_component", component=gen_parent_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc45>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_instances_have_unique_context_with_filled_slots(self, components_settings):
registry.register(name="variable_display", component=gen_variable_display_component())
registry.register(name="parent_component", component=gen_parent_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc45>Uniquely named variable = unique_val</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc46>Uniquely named variable = unique_from_slot</h1>", rendered)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(
self, components_settings,
):
registry.register(name="variable_display", component=gen_variable_display_component())
registry.register(name="parent_component", component=gen_parent_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}{% endcomponent %}
"""
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_nested_component_context_shadows_outer_context_with_filled_slots(
self, components_settings,
):
registry.register(name="variable_display", component=gen_variable_display_component())
registry.register(name="parent_component", component=gen_parent_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'parent_component' %}
{% fill 'content' %}
{% component 'variable_display' shadowing_variable='shadow_from_slot' new_variable='unique_from_slot' %}
{% endcomponent %}
{% endfill %}
{% endcomponent %}
""" # NOQA
template = Template(template_str)
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
assertInHTML("<h1 data-djc-id-ca1bc45>Shadowing variable = override</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc46>Shadowing variable = shadow_from_slot</h1>", rendered)
assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test
class TestParentArgs:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_parent_args_can_be_drawn_from_context(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="incrementer", component=gen_incrementer_component())
registry.register(name="parent_with_args", component=gen_parent_component_with_args())
registry.register(name="variable_display", component=gen_variable_display_component())
template_str: types.django_html = """
{% load component_tags %}
@ -271,9 +288,9 @@ class TestParentArgs:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_parent_args_available_outside_slots(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="incrementer", component=gen_incrementer_component())
registry.register(name="parent_with_args", component=gen_parent_component_with_args())
registry.register(name="variable_display", component=gen_variable_display_component())
template_str: types.django_html = """
{% load component_tags %}
@ -282,8 +299,21 @@ class TestParentArgs:
template = Template(template_str)
rendered = template.render(Context())
assertInHTML("<h1 data-djc-id-ca1bc43>Shadowing variable = passed_in</h1>", rendered)
assertInHTML("<h1 data-djc-id-ca1bc44>Uniquely named variable = passed_in</h1>", rendered)
assertHTMLEqual(
rendered,
"""
<div data-djc-id-ca1bc3f>
<h1>Parent content</h1>
<h1 data-djc-id-ca1bc43>Shadowing variable = passed_in</h1>
<h1 data-djc-id-ca1bc43>Uniquely named variable = unique_val</h1>
</div>
<div data-djc-id-ca1bc3f>
<h2>Slot content</h2>
<h1 data-djc-id-ca1bc44>Shadowing variable = slot_default_override</h1>
<h1 data-djc-id-ca1bc44>Uniquely named variable = passed_in</h1>
</div>
""",
)
assert "Shadowing variable = NOT SHADOWED" not in rendered
@djc_test(
@ -297,9 +327,9 @@ class TestParentArgs:
)
)
def test_parent_args_available_in_slots(self, components_settings, first_val, second_val):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
registry.register(name="variable_display", component=VariableDisplay)
registry.register(name="incrementer", component=gen_incrementer_component())
registry.register(name="parent_with_args", component=gen_parent_component_with_args())
registry.register(name="variable_display", component=gen_variable_display_component())
template_str: types.django_html = """
{% load component_tags %}
@ -333,7 +363,7 @@ class TestParentArgs:
class TestContextCalledOnce:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_simple_component(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="incrementer", component=gen_incrementer_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' %}{% endcomponent %}
@ -347,7 +377,7 @@ class TestContextCalledOnce:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_simple_component_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="incrementer", component=gen_incrementer_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' value='2' %}{% endcomponent %}
@ -364,7 +394,7 @@ class TestContextCalledOnce:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_component(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="incrementer", component=gen_incrementer_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' %}{% endcomponent %}
@ -376,7 +406,7 @@ class TestContextCalledOnce:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_component_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="incrementer", component=gen_incrementer_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' value='3' %}{% endcomponent %}
@ -388,7 +418,7 @@ class TestContextCalledOnce:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_slot(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="incrementer", component=gen_incrementer_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' %}
@ -411,7 +441,7 @@ class TestContextCalledOnce:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_one_context_call_with_slot_and_arg(self, components_settings):
registry.register(name="incrementer", component=IncrementerComponent)
registry.register(name="incrementer", component=gen_incrementer_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'incrementer' value='3' %}
@ -446,7 +476,7 @@ class TestComponentsCanAccessOuterContext:
)
)
def test_simple_component_can_use_outer_context(self, components_settings, expected_value):
registry.register(name="simple_component", component=SimpleComponent)
registry.register(name="simple_component", component=gen_simple_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' %}{% endcomponent %}
@ -465,7 +495,7 @@ class TestComponentsCanAccessOuterContext:
class TestIsolatedContext:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_simple_component_can_pass_outer_context_in_args(self, components_settings):
registry.register(name="simple_component", component=SimpleComponent)
registry.register(name="simple_component", component=gen_simple_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' variable=variable only %}{% endcomponent %}
@ -476,7 +506,7 @@ class TestIsolatedContext:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_simple_component_cannot_use_outer_context(self, components_settings):
registry.register(name="simple_component", component=SimpleComponent)
registry.register(name="simple_component", component=gen_simple_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' only %}{% endcomponent %}
@ -492,7 +522,7 @@ class TestIsolatedContextSetting:
def test_component_tag_includes_variable_with_isolated_context_from_settings(
self,
):
registry.register(name="simple_component", component=SimpleComponent)
registry.register(name="simple_component", component=gen_simple_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' variable=variable %}{% endcomponent %}
@ -505,7 +535,7 @@ class TestIsolatedContextSetting:
def test_component_tag_excludes_variable_with_isolated_context_from_settings(
self,
):
registry.register(name="simple_component", component=SimpleComponent)
registry.register(name="simple_component", component=gen_simple_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' %}{% endcomponent %}
@ -518,7 +548,7 @@ class TestIsolatedContextSetting:
def test_component_includes_variable_with_isolated_context_from_settings(
self,
):
registry.register(name="simple_component", component=SimpleComponent)
registry.register(name="simple_component", component=gen_simple_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' variable=variable %}
@ -532,7 +562,7 @@ class TestIsolatedContextSetting:
def test_component_excludes_variable_with_isolated_context_from_settings(
self,
):
registry.register(name="simple_component", component=SimpleComponent)
registry.register(name="simple_component", component=gen_simple_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'simple_component' %}

View file

@ -919,11 +919,11 @@ class TestSignatureBasedValidation:
template3.render(Context({}))
params3, nodelist3, node_id3, contents3 = captured # type: ignore
assert len(params3) == 1
assert isinstance(params3[0], TagAttr)
assert len(nodelist3) == 0
assert contents3 is None
assert node_id3 == "a1bc40"
assert len(params3) == 1 # type: ignore
assert isinstance(params3[0], TagAttr) # type: ignore
assert len(nodelist3) == 0 # type: ignore
assert contents3 is None # type: ignore
assert node_id3 == "a1bc40" # type: ignore
# Cleanup
TestNodeWithEndTag.unregister(component_tags.register)

View file

@ -10,10 +10,6 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
class SlottedComponent(Component):
template_file = "slotted_template.html"
def _get_templates_used_to_render(subject_template, render_context=None):
"""Emulate django.test.client.Client (see request method)."""
from django.test.signals import template_rendered
@ -48,6 +44,13 @@ def with_template_signal(func):
@djc_test
class TestTemplateSignal:
def gen_slotted_component(self):
class SlottedComponent(Component):
template_file = "slotted_template.html"
return SlottedComponent
def gen_inner_component(self):
class InnerComponent(Component):
template_file = "simple_template.html"
@ -61,11 +64,13 @@ class TestTemplateSignal:
css = "style.css"
js = "script.js"
return InnerComponent
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
@with_template_signal
def test_template_rendered(self, components_settings):
registry.register("test_component", SlottedComponent)
registry.register("inner_component", self.InnerComponent)
registry.register("test_component", self.gen_slotted_component())
registry.register("inner_component", self.gen_inner_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'test_component' %}{% endcomponent %}
@ -77,8 +82,8 @@ class TestTemplateSignal:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
@with_template_signal
def test_template_rendered_nested_components(self, components_settings):
registry.register("test_component", SlottedComponent)
registry.register("inner_component", self.InnerComponent)
registry.register("test_component", self.gen_slotted_component())
registry.register("inner_component", self.gen_inner_component())
template_str: types.django_html = """
{% load component_tags %}
{% component 'test_component' %}

View file

@ -1,7 +1,8 @@
from django.template import Context, Template
from django.template import Template
from django_components import Component, cached_template, types
from django_components.template import _get_component_template
from django_components.testing import djc_test
from .testutils import setup_test_config
@ -10,6 +11,7 @@ setup_test_config({"autodiscover": False})
@djc_test
class TestTemplateCache:
# TODO_v1 - Remove
def test_cached_template(self):
template_1 = cached_template("Variable: <strong>{{ variable }}</strong>")
template_1._test_id = "123"
@ -18,6 +20,7 @@ class TestTemplateCache:
assert template_2._test_id == "123"
# TODO_v1 - Remove
def test_cached_template_accepts_class(self):
class MyTemplate(Template):
pass
@ -25,6 +28,8 @@ class TestTemplateCache:
template = cached_template("Variable: <strong>{{ variable }}</strong>", MyTemplate)
assert isinstance(template, MyTemplate)
# TODO_v1 - Move to `test_component.py`. While `cached_template()` will be removed,
# we will internally still cache templates by class, and we will want to test for that.
def test_component_template_is_cached(self):
class SimpleComponent(Component):
def get_template(self, context):
@ -38,9 +43,11 @@ class TestTemplateCache:
"variable": kwargs.get("variable", None),
}
comp = SimpleComponent()
template_1 = comp._get_template(Context({}), component_id="123")
template_1._test_id = "123"
comp = SimpleComponent(kwargs={"variable": "test"})
template_2 = comp._get_template(Context({}), component_id="123")
assert template_2._test_id == "123"
# Check that we get the same template instance
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]

View file

@ -10,10 +10,6 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
class SlottedComponent(Component):
template_file = "slotted_template.html"
#######################
# TESTS
#######################

View file

@ -12,11 +12,15 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
class SlottedComponent(Component):
def gen_slotted_component():
class SlottedComponent(Component):
template_file = "slotted_template.html"
return SlottedComponent
class SlottedComponentWithContext(Component):
def gen_slotted_component_with_context():
class SlottedComponentWithContext(Component):
template: types.django_html = """
{% load component_tags %}
<custom-template>
@ -29,6 +33,8 @@ class SlottedComponentWithContext(Component):
def get_template_data(self, args, kwargs, slots, context):
return {"variable": kwargs["variable"]}
return SlottedComponentWithContext
#######################
# TESTS
@ -522,8 +528,8 @@ class TestDynamicComponentTemplateTag:
class TestMultiComponent:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_with_no_slots(self, components_settings):
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
registry.register("first_component", gen_slotted_component())
registry.register("second_component", gen_slotted_component_with_context())
template_str: types.django_html = """
{% load component_tags %}
@ -557,8 +563,8 @@ class TestMultiComponent:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_with_slots(self, components_settings):
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
registry.register("first_component", gen_slotted_component())
registry.register("second_component", gen_slotted_component_with_context())
template_str: types.django_html = """
{% load component_tags %}
@ -594,8 +600,8 @@ class TestMultiComponent:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_when_only_first_has_slots(self, components_settings):
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
registry.register("first_component", gen_slotted_component())
registry.register("second_component", gen_slotted_component_with_context())
template_str: types.django_html = """
{% load component_tags %}
@ -630,8 +636,8 @@ class TestMultiComponent:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_both_components_render_correctly_when_only_second_has_slots(self, components_settings):
registry.register("first_component", SlottedComponent)
registry.register("second_component", SlottedComponentWithContext)
registry.register("first_component", gen_slotted_component())
registry.register("second_component", gen_slotted_component_with_context())
template_str: types.django_html = """
{% load component_tags %}
@ -667,6 +673,9 @@ class TestMultiComponent:
@djc_test
class TestComponentIsolation:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_instances_of_component_do_not_share_slots(self, components_settings):
@register("test")
class SlottedComponent(Component):
template: types.django_html = """
{% load component_tags %}
@ -677,9 +686,6 @@ class TestComponentIsolation:
</custom-template>
"""
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_instances_of_component_do_not_share_slots(self, components_settings):
registry.register("test", self.SlottedComponent)
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
@ -791,7 +797,7 @@ class TestRecursiveComponent:
class TestComponentTemplateSyntaxError:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_variable_outside_fill_tag_compiles_w_out_error(self, components_settings):
registry.register("test", SlottedComponent)
registry.register("test", gen_slotted_component())
# As of v0.28 this is valid, provided the component registered under "test"
# contains a slot tag marked as 'default'. This is verified outside
# template compilation time.
@ -805,7 +811,7 @@ class TestComponentTemplateSyntaxError:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_text_outside_fill_tag_is_not_error_when_no_fill_tags(self, components_settings):
registry.register("test", SlottedComponent)
registry.register("test", gen_slotted_component())
# As of v0.28 this is valid, provided the component registered under "test"
# contains a slot tag marked as 'default'. This is verified outside
# template compilation time.
@ -819,7 +825,7 @@ class TestComponentTemplateSyntaxError:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_text_outside_fill_tag_is_error_when_fill_tags(self, components_settings):
registry.register("test", SlottedComponent)
registry.register("test", gen_slotted_component())
template_str: types.django_html = """
{% load component_tags %}
{% component "test" %}
@ -837,7 +843,7 @@ class TestComponentTemplateSyntaxError:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_unclosed_component_is_error(self, components_settings):
registry.register("test", SlottedComponent)
registry.register("test", gen_slotted_component())
with pytest.raises(
TemplateSyntaxError,
match=re.escape("Unclosed tag on line 3: 'component'"),

View file

@ -11,21 +11,18 @@ from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
setup_test_config({"autodiscover": False})
class SlottedComponent(Component):
def gen_slotted_component():
class SlottedComponent(Component):
template_file = "slotted_template.html"
return SlottedComponent
class BlockedAndSlottedComponent(Component):
def gen_blocked_and_slotted_component():
class BlockedAndSlottedComponent(Component):
template_file = "blocked_and_slotted_template.html"
class RelativeFileComponentUsingTemplateFile(Component):
template_file = "relative_extends.html"
class RelativeFileComponentUsingGetTemplateName(Component):
def get_template_name(self, context):
return "relative_extends.html"
return BlockedAndSlottedComponent
#######################
@ -37,7 +34,7 @@ class RelativeFileComponentUsingGetTemplateName(Component):
class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_component_one_component(self, components_settings):
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component")
class _ExtendedComponent(Component):
@ -82,7 +79,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_component_two_identical_components(self, components_settings):
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component")
class _ExtendedComponent(Component):
@ -134,12 +131,11 @@ class TestExtendsCompat:
</body>
</html>
"""
assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_component_two_different_components_same_parent(self, components_settings): # noqa: E501
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component")
class _ExtendedComponent(Component):
@ -201,12 +197,11 @@ class TestExtendsCompat:
</body>
</html>
"""
assertHTMLEqual(rendered, expected)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_component_two_different_components_different_parent(self, components_settings): # noqa: E501
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component")
class _ExtendedComponent(Component):
@ -271,7 +266,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_extends_on_component_one_component(self, components_settings):
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component")
class _ExtendedComponent(Component):
@ -314,7 +309,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_extends_on_component_two_component(self, components_settings):
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component")
class _ExtendedComponent(Component):
@ -368,8 +363,8 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_nested_component(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
registry.register("slotted_component", gen_slotted_component())
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component")
class _ExtendedComponent(Component):
@ -425,8 +420,8 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_double_extends_on_main_template_and_nested_component_and_include(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("blocked_and_slotted_component", BlockedAndSlottedComponent)
registry.register("slotted_component", gen_slotted_component())
registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component())
@register("extended_component")
class _ExtendedComponent(Component):
@ -459,12 +454,25 @@ class TestExtendsCompat:
# second rendering after cache built
rendered_2 = Template(template).render(Context({"DJC_DEPS_STRATEGY": "ignore"}))
expected_2 = expected.replace("data-djc-id-ca1bc3f", "data-djc-id-ca1bc41")
expected_2 = """
<!DOCTYPE html>
<html lang="en">
<body>
<main role="main">
<div class='container main-container'>
Variable: <strong></strong>
Variable: <strong data-djc-id-ca1bc41></strong>
</div>
</main>
</body>
</html>
"""
assertHTMLEqual(rendered_2, expected_2)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slots_inside_extends(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("slot_inside_extends")
class SlotInsideExtendsComponent(Component):
@ -497,7 +505,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slots_inside_include(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("slot_inside_include")
class SlotInsideIncludeComponent(Component):
@ -530,7 +538,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_inside_block(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
template: types.django_html = """
{% extends "block.html" %}
{% load component_tags %}
@ -565,7 +573,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_block_inside_component(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
template: types.django_html = """
{% extends "block_in_component.html" %}
@ -594,7 +602,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_block_inside_component_parent(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("block_in_component_parent")
class BlockInCompParent(Component):
@ -627,7 +635,7 @@ class TestExtendsCompat:
Assert that when we call a component with `{% component %}`, that
the `{% block %}` will NOT affect the inner component.
"""
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("block_inside_slot_v1")
class BlockInSlotInComponent(Component):
@ -662,7 +670,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_inside_block__slot_default_block_default(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("slot_inside_block")
class _SlotInsideBlockComponent(Component):
@ -695,7 +703,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_inside_block__slot_default_block_override(self, components_settings):
registry.clear()
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("slot_inside_block")
class _SlotInsideBlockComponent(Component):
@ -730,7 +738,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_inside_block__slot_overriden_block_default(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("slot_inside_block")
class _SlotInsideBlockComponent(Component):
@ -766,7 +774,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_slot_inside_block__slot_overriden_block_overriden(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("slot_inside_block")
class _SlotInsideBlockComponent(Component):
@ -812,7 +820,7 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_inject_inside_block(self, components_settings):
registry.register("slotted_component", SlottedComponent)
registry.register("slotted_component", gen_slotted_component())
@register("injectee")
class InjectComponent(Component):
@ -851,7 +859,9 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_using_template_file_extends_relative_file(self, components_settings):
registry.register("relative_file_component_using_template_file", RelativeFileComponentUsingTemplateFile)
@register("relative_file_component_using_template_file")
class RelativeFileComponentUsingTemplateFile(Component):
template_file = "relative_extends.html"
template: types.django_html = """
{% load component_tags %}
@ -874,7 +884,10 @@ class TestExtendsCompat:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
def test_component_using_get_template_name_extends_relative_file(self, components_settings):
registry.register("relative_file_component_using_get_template_name", RelativeFileComponentUsingGetTemplateName)
@register("relative_file_component_using_get_template_name")
class RelativeFileComponentUsingGetTemplateName(Component):
def get_template_name(self, context):
return "relative_extends.html"
template: types.django_html = """
{% load component_tags %}

View file

@ -37,12 +37,19 @@ def setup_test_config(
"OPTIONS": {
"builtins": [
"django_components.templatetags.component_tags",
]
],
'loaders': [
# 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',
],
},
}
],
"COMPONENTS": {
"template_cache_size": 128,
**(components or {}),
},
"MIDDLEWARE": [],