mirror of
https://github.com/django-components/django-components.git
synced 2025-08-08 08:17:59 +00:00
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:
parent
fa9ae9892f
commit
8677ee7941
28 changed files with 1548 additions and 652 deletions
|
@ -1,5 +1,4 @@
|
|||
import sys
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from types import MethodType
|
||||
from typing import (
|
||||
|
@ -7,7 +6,6 @@ from typing import (
|
|||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
Generator,
|
||||
List,
|
||||
Mapping,
|
||||
NamedTuple,
|
||||
|
@ -19,12 +17,10 @@ from typing import (
|
|||
)
|
||||
from weakref import ReferenceType, WeakValueDictionary, finalize
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.forms.widgets import Media as MediaCls
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template.base import NodeList, Origin, Parser, Template, Token
|
||||
from django.template.base import NodeList, Parser, Template, Token
|
||||
from django.template.context import Context, RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext
|
||||
from django.test.signals import template_rendered
|
||||
from django.views import View
|
||||
|
@ -72,12 +68,11 @@ from django_components.slots import (
|
|||
normalize_slot_fills,
|
||||
resolve_fills,
|
||||
)
|
||||
from django_components.template import cached_template
|
||||
from django_components.template import cache_component_template_file, prepare_component_template
|
||||
from django_components.util.context import gen_context_processors_data, snapshot_context
|
||||
from django_components.util.django_monkeypatch import is_template_cls_patched
|
||||
from django_components.util.exception import component_error_message
|
||||
from django_components.util.logger import trace_component_msg
|
||||
from django_components.util.misc import default, gen_id, get_import_path, hash_comp_cls, to_dict
|
||||
from django_components.util.misc import default, gen_id, hash_comp_cls, to_dict
|
||||
from django_components.util.template_tag import TagAttr
|
||||
from django_components.util.weakref import cached_ref
|
||||
|
||||
|
@ -412,14 +407,23 @@ class ComponentTemplateNameDescriptor:
|
|||
|
||||
|
||||
class ComponentMeta(ComponentMediaMeta):
|
||||
def __new__(mcs, name: Any, bases: Tuple, attrs: Dict) -> Any:
|
||||
def __new__(mcs, name: str, bases: Tuple[Type, ...], attrs: Dict) -> Type:
|
||||
# If user set `template_name` on the class, we instead set it to `template_file`,
|
||||
# because we want `template_name` to be the descriptor that proxies to `template_file`.
|
||||
if "template_name" in attrs:
|
||||
attrs["template_file"] = attrs.pop("template_name")
|
||||
attrs["template_name"] = ComponentTemplateNameDescriptor()
|
||||
|
||||
return super().__new__(mcs, name, bases, attrs)
|
||||
cls = super().__new__(mcs, name, bases, attrs)
|
||||
|
||||
# If the component defined `template_file`, then associate this Component class
|
||||
# with that template file path.
|
||||
# This way, when we will be instantiating `Template` in order to load the Component's template,
|
||||
# and its template_name matches this path, then we know that the template belongs to this Component class.
|
||||
if "template_file" in attrs and attrs["template_file"]:
|
||||
cache_component_template_file(cls)
|
||||
|
||||
return cls
|
||||
|
||||
# This runs when a Component class is being deleted
|
||||
def __del__(cls) -> None:
|
||||
|
@ -646,25 +650,47 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
- Relative to the directory where the Component's Python file is defined.
|
||||
- Relative to one of the component directories, as set by
|
||||
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
||||
[`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||
or
|
||||
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
[`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
(e.g. `<root>/components/`).
|
||||
- Relative to the template directories, as set by Django's `TEMPLATES` setting (e.g. `<root>/templates/`).
|
||||
|
||||
Only one of [`template_file`](../api#django_components.Component.template_file),
|
||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
||||
[`template`](../api#django_components.Component.template)
|
||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
||||
!!! warning
|
||||
|
||||
Only one of [`template_file`](../api#django_components.Component.template_file),
|
||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
||||
[`template`](../api#django_components.Component.template)
|
||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
template_file = "path/to/template.html"
|
||||
Assuming this project layout:
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"name": "World"}
|
||||
```txt
|
||||
|- components/
|
||||
|- table/
|
||||
|- table.html
|
||||
|- table.css
|
||||
|- table.js
|
||||
```
|
||||
|
||||
Template name can be either relative to the python file (`components/table/table.py`):
|
||||
|
||||
```python
|
||||
class Table(Component):
|
||||
template_file = "table.html"
|
||||
```
|
||||
|
||||
Or relative to one of the directories in
|
||||
[`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||
or
|
||||
[`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
(`components/`):
|
||||
|
||||
```python
|
||||
class Table(Component):
|
||||
template_file = "table/table.html"
|
||||
```
|
||||
"""
|
||||
|
||||
|
@ -677,53 +703,142 @@ class Component(metaclass=ComponentMeta):
|
|||
For historical reasons, django-components used `template_name` to align with Django's
|
||||
[TemplateView](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#django.views.generic.base.TemplateView).
|
||||
|
||||
`template_file` was introduced to align with `js/js_file` and `css/css_file`.
|
||||
`template_file` was introduced to align with
|
||||
[`js`](../api#django_components.Component.js)/[`js_file`](../api#django_components.Component.js_file)
|
||||
and [`css`](../api#django_components.Component.css)/[`css_file`](../api#django_components.Component.css_file).
|
||||
|
||||
Setting and accessing this attribute is proxied to `template_file`.
|
||||
Setting and accessing this attribute is proxied to
|
||||
[`template_file`](../api#django_components.Component.template_file).
|
||||
"""
|
||||
|
||||
# TODO_v1 - Remove
|
||||
def get_template_name(self, context: Context) -> Optional[str]:
|
||||
"""
|
||||
Filepath to the Django template associated with this component.
|
||||
DEPRECATED: Use instead [`Component.template_file`](../api#django_components.Component.template_file),
|
||||
[`Component.template`](../api#django_components.Component.template) or
|
||||
[`Component.on_render()`](../api#django_components.Component.on_render).
|
||||
Will be removed in v1.
|
||||
|
||||
The filepath must be relative to either the file where the component class was defined,
|
||||
or one of the roots of `STATIFILES_DIRS`.
|
||||
Same as [`Component.template_file`](../api#django_components.Component.template_file),
|
||||
but allows to dynamically resolve the template name at render time.
|
||||
|
||||
Only one of [`template_file`](../api#django_components.Component.template_file),
|
||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
||||
[`template`](../api#django_components.Component.template)
|
||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
||||
See [`Component.template_file`](../api#django_components.Component.template_file)
|
||||
for more info and examples.
|
||||
|
||||
!!! warning
|
||||
|
||||
The context is not fully populated at the point when this method is called.
|
||||
|
||||
If you need to access the context, either use
|
||||
[`Component.on_render_before()`](../api#django_components.Component.on_render_before) or
|
||||
[`Component.on_render()`](../api#django_components.Component.on_render).
|
||||
|
||||
!!! warning
|
||||
|
||||
Only one of
|
||||
[`template_file`](../api#django_components.Component.template_file),
|
||||
[`get_template_name()`](../api#django_components.Component.get_template_name),
|
||||
[`template`](../api#django_components.Component.template)
|
||||
or
|
||||
[`get_template()`](../api#django_components.Component.get_template)
|
||||
must be defined.
|
||||
|
||||
Args:
|
||||
context (Context): The Django template\
|
||||
[`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)\
|
||||
in which the component is rendered.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The filepath to the template.
|
||||
"""
|
||||
return None
|
||||
|
||||
template: Optional[Union[str, Template]] = None
|
||||
template: Optional[str] = None
|
||||
"""
|
||||
Inlined Django template associated with this component. Can be a plain string or a Template instance.
|
||||
Inlined Django template (as a plain string) associated with this component.
|
||||
|
||||
Only one of [`template_file`](../api#django_components.Component.template_file),
|
||||
[`get_template_name`](../api#django_components.Component.get_template_name),
|
||||
[`template`](../api#django_components.Component.template)
|
||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
||||
!!! warning
|
||||
|
||||
Only one of
|
||||
[`template_file`](../api#django_components.Component.template_file),
|
||||
[`template`](../api#django_components.Component.template),
|
||||
[`get_template_name()`](../api#django_components.Component.get_template_name),
|
||||
or
|
||||
[`get_template()`](../api#django_components.Component.get_template)
|
||||
must be defined.
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
template = "Hello, {{ name }}!"
|
||||
```python
|
||||
class Table(Component):
|
||||
template = '''
|
||||
<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),
|
||||
[`template`](../api#django_components.Component.template)
|
||||
or [`get_template`](../api#django_components.Component.get_template) must be defined.
|
||||
Same as [`Component.template`](../api#django_components.Component.template),
|
||||
but allows to dynamically resolve the template at render time.
|
||||
|
||||
The template can be either plain string or
|
||||
a [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) instance.
|
||||
|
||||
See [`Component.template`](../api#django_components.Component.template) for more info and examples.
|
||||
|
||||
!!! warning
|
||||
|
||||
Only one of
|
||||
[`template`](../api#django_components.Component.template)
|
||||
[`template_file`](../api#django_components.Component.template_file),
|
||||
[`get_template_name()`](../api#django_components.Component.get_template_name),
|
||||
or
|
||||
[`get_template()`](../api#django_components.Component.get_template)
|
||||
must be defined.
|
||||
|
||||
!!! warning
|
||||
|
||||
The context is not fully populated at the point when this method is called.
|
||||
|
||||
If you need to access the context, either use
|
||||
[`Component.on_render_before()`](../api#django_components.Component.on_render_before) or
|
||||
[`Component.on_render()`](../api#django_components.Component.on_render).
|
||||
|
||||
Args:
|
||||
context (Context): The Django template\
|
||||
[`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)\
|
||||
in which the component is rendered.
|
||||
|
||||
Returns:
|
||||
Optional[Union[str, Template]]: The inlined Django template string or\
|
||||
a [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) instance.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
@ -981,8 +1096,10 @@ class Component(metaclass=ComponentMeta):
|
|||
"""
|
||||
Main JS associated with this component inlined as string.
|
||||
|
||||
Only one of [`js`](../api#django_components.Component.js) or
|
||||
[`js_file`](../api#django_components.Component.js_file) must be defined.
|
||||
!!! warning
|
||||
|
||||
Only one of [`js`](../api#django_components.Component.js) or
|
||||
[`js_file`](../api#django_components.Component.js_file) must be defined.
|
||||
|
||||
**Example:**
|
||||
|
||||
|
@ -990,6 +1107,22 @@ class Component(metaclass=ComponentMeta):
|
|||
class MyComponent(Component):
|
||||
js = "console.log('Hello, World!');"
|
||||
```
|
||||
|
||||
**Syntax highlighting**
|
||||
|
||||
When using the inlined template, you can enable syntax highlighting
|
||||
with `django_components.types.js`.
|
||||
|
||||
Learn more about [syntax highlighting](../../concepts/fundamentals/single_file_components/#syntax-highlighting).
|
||||
|
||||
```djc_py
|
||||
from django_components import Component, types
|
||||
|
||||
class MyComponent(Component):
|
||||
js: types.js = '''
|
||||
console.log('Hello, World!');
|
||||
'''
|
||||
```
|
||||
"""
|
||||
|
||||
js_file: ClassVar[Optional[str]] = None
|
||||
|
@ -1000,9 +1133,9 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
- Relative to the directory where the Component's Python file is defined.
|
||||
- Relative to one of the component directories, as set by
|
||||
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
||||
[`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||
or
|
||||
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
[`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
(e.g. `<root>/components/`).
|
||||
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
|
||||
|
||||
|
@ -1012,8 +1145,10 @@ class Component(metaclass=ComponentMeta):
|
|||
the path is resolved.
|
||||
2. The file is read and its contents is set to [`Component.js`](../api#django_components.Component.js).
|
||||
|
||||
Only one of [`js`](../api#django_components.Component.js) or
|
||||
[`js_file`](../api#django_components.Component.js_file) must be defined.
|
||||
!!! warning
|
||||
|
||||
Only one of [`js`](../api#django_components.Component.js) or
|
||||
[`js_file`](../api#django_components.Component.js_file) must be defined.
|
||||
|
||||
**Example:**
|
||||
|
||||
|
@ -1244,19 +1379,39 @@ class Component(metaclass=ComponentMeta):
|
|||
"""
|
||||
Main CSS associated with this component inlined as string.
|
||||
|
||||
Only one of [`css`](../api#django_components.Component.css) or
|
||||
[`css_file`](../api#django_components.Component.css_file) must be defined.
|
||||
!!! warning
|
||||
|
||||
Only one of [`css`](../api#django_components.Component.css) or
|
||||
[`css_file`](../api#django_components.Component.css_file) must be defined.
|
||||
|
||||
**Example:**
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
css = \"\"\"
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
\"\"\"
|
||||
```
|
||||
|
||||
**Syntax highlighting**
|
||||
|
||||
When using the inlined template, you can enable syntax highlighting
|
||||
with `django_components.types.css`.
|
||||
|
||||
Learn more about [syntax highlighting](../../concepts/fundamentals/single_file_components/#syntax-highlighting).
|
||||
|
||||
```djc_py
|
||||
from django_components import Component, types
|
||||
|
||||
class MyComponent(Component):
|
||||
css: types.css = '''
|
||||
.my-class {
|
||||
color: red;
|
||||
}
|
||||
'''
|
||||
```
|
||||
"""
|
||||
|
||||
css_file: ClassVar[Optional[str]] = None
|
||||
|
@ -1267,9 +1422,9 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
- Relative to the directory where the Component's Python file is defined.
|
||||
- Relative to one of the component directories, as set by
|
||||
[`COMPONENTS.dirs`](../settings.md#django_components.app_settings.ComponentsSettings.dirs)
|
||||
[`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||
or
|
||||
[`COMPONENTS.app_dirs`](../settings.md#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
[`COMPONENTS.app_dirs`](../settings#django_components.app_settings.ComponentsSettings.app_dirs)
|
||||
(e.g. `<root>/components/`).
|
||||
- Relative to the staticfiles directories, as set by Django's `STATICFILES_DIRS` setting (e.g. `<root>/static/`).
|
||||
|
||||
|
@ -1279,8 +1434,10 @@ class Component(metaclass=ComponentMeta):
|
|||
the path is resolved.
|
||||
2. The file is read and its contents is set to [`Component.css`](../api#django_components.Component.css).
|
||||
|
||||
Only one of [`css`](../api#django_components.Component.css) or
|
||||
[`css_file`](../api#django_components.Component.css_file) must be defined.
|
||||
!!! warning
|
||||
|
||||
Only one of [`css`](../api#django_components.Component.css) or
|
||||
[`css_file`](../api#django_components.Component.css_file) must be defined.
|
||||
|
||||
**Example:**
|
||||
|
||||
|
@ -1632,7 +1789,7 @@ class Component(metaclass=ComponentMeta):
|
|||
# PUBLIC API - HOOKS (Configurable by users)
|
||||
# #####################################
|
||||
|
||||
def on_render_before(self, context: Context, template: Template) -> None:
|
||||
def on_render_before(self, context: Context, template: Optional[Template]) -> None:
|
||||
"""
|
||||
Hook that runs just before the component's template is rendered.
|
||||
|
||||
|
@ -1640,7 +1797,7 @@ class Component(metaclass=ComponentMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
def on_render_after(self, context: Context, template: Template, content: str) -> Optional[SlotResult]:
|
||||
def on_render_after(self, context: Context, template: Optional[Template], content: str) -> Optional[SlotResult]:
|
||||
"""
|
||||
Hook that runs just after the component's template was rendered.
|
||||
It receives the rendered output as the last argument.
|
||||
|
@ -1753,6 +1910,15 @@ class Component(metaclass=ComponentMeta):
|
|||
"""Deprecated. Use `Component.class_id` instead."""
|
||||
return self.class_id
|
||||
|
||||
_template: Optional[Template] = None
|
||||
"""
|
||||
Cached [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
|
||||
instance for the [`Component`](../api#django_components.Component),
|
||||
created from
|
||||
[`Component.template`](#django_components.Component.template) or
|
||||
[`Component.template_file`](#django_components.Component.template_file).
|
||||
"""
|
||||
|
||||
# TODO_v3 - Django-specific property to prevent calling the instance as a function.
|
||||
do_not_call_in_templates: ClassVar[bool] = True
|
||||
"""
|
||||
|
@ -1850,6 +2016,9 @@ class Component(metaclass=ComponentMeta):
|
|||
cls.class_id = hash_comp_cls(cls)
|
||||
comp_cls_id_mapping[cls.class_id] = cls
|
||||
|
||||
# Make sure that subclassed component will store it's own template, not the parent's.
|
||||
cls._template = None
|
||||
|
||||
ALL_COMPONENTS.append(cached_ref(cls)) # type: ignore[arg-type]
|
||||
extensions._init_component_class(cls)
|
||||
extensions.on_component_class_created(OnComponentClassCreatedContext(cls))
|
||||
|
@ -2276,64 +2445,6 @@ class Component(metaclass=ComponentMeta):
|
|||
# MISC
|
||||
# #####################################
|
||||
|
||||
# NOTE: We cache the Template instance. When the template is taken from a file
|
||||
# via `get_template_name`, then we leverage Django's template caching with `get_template()`.
|
||||
# Otherwise, we use our own `cached_template()` to cache the template.
|
||||
#
|
||||
# This is important to keep in mind, because the implication is that we should
|
||||
# treat Templates AND their nodelists as IMMUTABLE.
|
||||
def _get_template(self, context: Context, component_id: str) -> Template:
|
||||
template_name = self.get_template_name(context)
|
||||
# TODO_REMOVE_IN_V1 - Remove `self.get_template_string` in v1
|
||||
template_getter = getattr(self, "get_template_string", self.get_template)
|
||||
template_body = template_getter(context)
|
||||
|
||||
# `get_template_name()`, `get_template()`, and `template` are mutually exclusive
|
||||
#
|
||||
# Note that `template` and `template_name` are also mutually exclusive, but this
|
||||
# is checked when lazy-loading the template from `template_name`. So if user specified
|
||||
# `template_name`, then `template` will be populated with the content of that file.
|
||||
if self.template is not None and template_name is not None:
|
||||
raise ImproperlyConfigured(
|
||||
"Received non-null value from both 'template/template_name' and 'get_template_name' in"
|
||||
f" Component {type(self).__name__}. Only one of the two must be set."
|
||||
)
|
||||
if self.template is not None and template_body is not None:
|
||||
raise ImproperlyConfigured(
|
||||
"Received non-null value from both 'template/template_name' and 'get_template' in"
|
||||
f" Component {type(self).__name__}. Only one of the two must be set."
|
||||
)
|
||||
if template_name is not None and template_body is not None:
|
||||
raise ImproperlyConfigured(
|
||||
"Received non-null value from both 'get_template_name' and 'get_template' in"
|
||||
f" Component {type(self).__name__}. Only one of the two must be set."
|
||||
)
|
||||
|
||||
if template_name is not None:
|
||||
return get_template(template_name).template
|
||||
|
||||
template_body = template_body if template_body is not None else self.template
|
||||
if template_body is not None:
|
||||
# We got template string, so we convert it to Template
|
||||
if isinstance(template_body, str):
|
||||
trace_component_msg("COMP_LOAD", component_name=self.name, component_id=component_id, slot_name=None)
|
||||
template: Template = cached_template(
|
||||
template_string=template_body,
|
||||
name=self.template_file or self.name,
|
||||
origin=Origin(
|
||||
name=self.template_file or get_import_path(self.__class__),
|
||||
template_name=self.template_file or self.name,
|
||||
),
|
||||
)
|
||||
else:
|
||||
template = template_body
|
||||
|
||||
return template
|
||||
|
||||
raise ImproperlyConfigured(
|
||||
f"Either 'template_file' or 'template' must be set for Component {type(self).__name__}."
|
||||
)
|
||||
|
||||
def inject(self, key: str, default: Optional[Any] = None) -> Any:
|
||||
"""
|
||||
Use this method to retrieve the data that was passed to a [`{% provide %}`](../template_tags#provide) tag
|
||||
|
@ -2796,12 +2907,12 @@ class Component(metaclass=ComponentMeta):
|
|||
)
|
||||
# Use RequestContext if request is provided, so that child non-component template tags
|
||||
# can access the request object too.
|
||||
context = context or (RequestContext(request) if request else Context())
|
||||
context = context if context is not None else (RequestContext(request) if request else Context())
|
||||
|
||||
# Allow to provide a dict instead of Context
|
||||
# NOTE: This if/else is important to avoid nested Contexts,
|
||||
# See https://github.com/django-components/django-components/issues/414
|
||||
if not isinstance(context, Context):
|
||||
if not isinstance(context, (Context, RequestContext)):
|
||||
context = RequestContext(request, context) if request else Context(context)
|
||||
|
||||
render_id = _gen_component_id()
|
||||
|
@ -2850,7 +2961,9 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
# Required for compatibility with Django's {% extends %} tag
|
||||
# See https://github.com/django-components/django-components/pull/859
|
||||
context.render_context.push({BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())})
|
||||
context.render_context.push( # type: ignore[union-attr]
|
||||
{BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())} # type: ignore
|
||||
)
|
||||
|
||||
# We pass down the components the info about the component's parent.
|
||||
# This is used for correctly resolving slot fills, correct rendering order,
|
||||
|
@ -2886,7 +2999,7 @@ class Component(metaclass=ComponentMeta):
|
|||
template_name=None,
|
||||
# This field will be modified from within `SlotNodes.render()`:
|
||||
# - The `default_slot` will be set to the first slot that has the `default` attribute set.
|
||||
# If multiple slots have the `default` attribute set, yet have different name, then
|
||||
# - If multiple slots have the `default` attribute set, yet have different name, then
|
||||
# we will raise an error.
|
||||
default_slot=None,
|
||||
# NOTE: This is only a SNAPSHOT of the outer context.
|
||||
|
@ -2937,10 +3050,32 @@ class Component(metaclass=ComponentMeta):
|
|||
# but instead can render one component at a time.
|
||||
#############################################################################
|
||||
|
||||
with _prepare_template(component, template_data) as template:
|
||||
component_ctx.template_name = template.name
|
||||
# TODO_v1 - Currently we have to pass `template_data` to `prepare_component_template()`,
|
||||
# so that `get_template_string()`, `get_template_name()`, and `get_template()`
|
||||
# have access to the data from `get_template_data()`.
|
||||
#
|
||||
# Because of that there is one layer of `Context.update()` called inside `prepare_component_template()`.
|
||||
#
|
||||
# Once `get_template_string()`, `get_template_name()`, and `get_template()` are removed,
|
||||
# we can remove that layer of `Context.update()`, and NOT pass `template_data`
|
||||
# to `prepare_component_template()`.
|
||||
#
|
||||
# Then we can simply apply `template_data` to the context in the same layer
|
||||
# where we apply `context_processor_data` and `component_vars`.
|
||||
with prepare_component_template(component, template_data) as template:
|
||||
# Set `Template._djc_is_component_nested` based on whether we're currently INSIDE
|
||||
# the `{% extends %}` tag.
|
||||
# Part of fix for https://github.com/django-components/django-components/issues/508
|
||||
# See django_monkeypatch.py
|
||||
if template is not None:
|
||||
template._djc_is_component_nested = bool(
|
||||
context.render_context.get(BLOCK_CONTEXT_KEY) # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
with context.update(
|
||||
# Capture the template name so we can print better error messages (currently used in slots)
|
||||
component_ctx.template_name = template.name if template else None
|
||||
|
||||
with context.update( # type: ignore[union-attr]
|
||||
{
|
||||
# Make data from context processors available inside templates
|
||||
**component.context_processors_data,
|
||||
|
@ -2973,7 +3108,7 @@ class Component(metaclass=ComponentMeta):
|
|||
context_snapshot = snapshot_context(context)
|
||||
|
||||
# Cleanup
|
||||
context.render_context.pop()
|
||||
context.render_context.pop() # type: ignore[union-attr]
|
||||
|
||||
######################################
|
||||
# 5. Render component
|
||||
|
@ -3089,26 +3224,31 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
# Emit signal that the template is about to be rendered
|
||||
template_rendered.send(sender=template, template=template, context=context)
|
||||
# Get the component's HTML
|
||||
html_content = template.render(context)
|
||||
|
||||
# Add necessary HTML attributes to work with JS and CSS variables
|
||||
updated_html, child_components = set_component_attrs_for_js_and_css(
|
||||
html_content=html_content,
|
||||
component_id=render_id,
|
||||
css_input_hash=css_input_hash,
|
||||
css_scope_id=css_scope_id,
|
||||
root_attributes=root_attributes,
|
||||
)
|
||||
if template is not None:
|
||||
# Get the component's HTML
|
||||
html_content = template.render(context)
|
||||
|
||||
# Prepend an HTML comment to instructs how and what JS and CSS scripts are associated with it.
|
||||
updated_html = insert_component_dependencies_comment(
|
||||
updated_html,
|
||||
component_cls=component_cls,
|
||||
component_id=render_id,
|
||||
js_input_hash=js_input_hash,
|
||||
css_input_hash=css_input_hash,
|
||||
)
|
||||
# Add necessary HTML attributes to work with JS and CSS variables
|
||||
updated_html, child_components = set_component_attrs_for_js_and_css(
|
||||
html_content=html_content,
|
||||
component_id=render_id,
|
||||
css_input_hash=css_input_hash,
|
||||
css_scope_id=css_scope_id,
|
||||
root_attributes=root_attributes,
|
||||
)
|
||||
|
||||
# Prepend an HTML comment to instructs how and what JS and CSS scripts are associated with it.
|
||||
updated_html = insert_component_dependencies_comment(
|
||||
updated_html,
|
||||
component_cls=component_cls,
|
||||
component_id=render_id,
|
||||
js_input_hash=js_input_hash,
|
||||
css_input_hash=css_input_hash,
|
||||
)
|
||||
else:
|
||||
updated_html = ""
|
||||
child_components = {}
|
||||
|
||||
trace_component_msg(
|
||||
"COMP_RENDER_END",
|
||||
|
@ -3278,7 +3418,13 @@ class ComponentNode(BaseNode):
|
|||
node_id: Optional[str] = None,
|
||||
contents: Optional[str] = None,
|
||||
) -> None:
|
||||
super().__init__(params=params, flags=flags, nodelist=nodelist, node_id=node_id, contents=contents)
|
||||
super().__init__(
|
||||
params=params,
|
||||
flags=flags,
|
||||
nodelist=nodelist,
|
||||
node_id=node_id,
|
||||
contents=contents,
|
||||
)
|
||||
|
||||
self.name = name
|
||||
self.registry = registry
|
||||
|
@ -3370,43 +3516,3 @@ def _get_parent_component_context(context: Context) -> Union[Tuple[None, None],
|
|||
|
||||
parent_comp_ctx = component_context_cache[parent_id]
|
||||
return parent_id, parent_comp_ctx
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _maybe_bind_template(context: Context, template: Template) -> Generator[None, Any, None]:
|
||||
if context.template is None:
|
||||
with context.bind_template(template):
|
||||
yield
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _prepare_template(
|
||||
component: Component,
|
||||
template_data: Any,
|
||||
) -> Generator[Template, Any, None]:
|
||||
context = component.context
|
||||
with context.update(template_data):
|
||||
# Associate the newly-created Context with a Template, otherwise we get
|
||||
# an error when we try to use `{% include %}` tag inside the template?
|
||||
# See https://github.com/django-components/django-components/issues/580
|
||||
# And https://github.com/django-components/django-components/issues/634
|
||||
template = component._get_template(context, component_id=component.id)
|
||||
|
||||
if not is_template_cls_patched(template):
|
||||
raise RuntimeError(
|
||||
"Django-components received a Template instance which was not patched."
|
||||
"If you are using Django's Template class, check if you added django-components"
|
||||
"to INSTALLED_APPS. If you are using a custom template class, then you need to"
|
||||
"manually patch the class."
|
||||
)
|
||||
|
||||
# Set `Template._djc_is_component_nested` based on whether we're currently INSIDE
|
||||
# the `{% extends %}` tag.
|
||||
# Part of fix for https://github.com/django-components/django-components/issues/508
|
||||
# See django_monkeypatch.py
|
||||
template._djc_is_component_nested = bool(context.render_context.get(BLOCK_CONTEXT_KEY))
|
||||
|
||||
with _maybe_bind_template(context, template):
|
||||
yield template
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue