mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34: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
124
CHANGELOG.md
124
CHANGELOG.md
|
@ -186,6 +186,40 @@ Summary:
|
|||
)
|
||||
```
|
||||
|
||||
- `Component.template` no longer accepts a Template instance, only plain string.
|
||||
|
||||
Before:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
template = Template("{{ my_var }}")
|
||||
```
|
||||
|
||||
Instead, either:
|
||||
|
||||
1. Set `Component.template` to a plain string.
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
template = "{{ my_var }}"
|
||||
```
|
||||
|
||||
2. Move the template to it's own HTML file and set `Component.template_file`.
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
template_file = "my_template.html"
|
||||
```
|
||||
|
||||
3. Or, if you dynamically created the template, render the template inside `Component.on_render()`.
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
def on_render(self, context, template):
|
||||
dynamic_template = do_something_dynamic()
|
||||
return dynamic_template.render(context)
|
||||
```
|
||||
|
||||
- The `Component.Url` class was merged with `Component.View`.
|
||||
|
||||
Instead of `Component.Url.public`, use `Component.View.public`.
|
||||
|
@ -404,6 +438,51 @@ Summary:
|
|||
|
||||
Since `get_context_data()` is widely used, it will remain available until v2.
|
||||
|
||||
- `Component.get_template_name()` and `Component.get_template()` are now deprecated. Use `Component.template`,
|
||||
`Component.template_file` or `Component.on_render()` instead.
|
||||
|
||||
`Component.get_template_name()` and `Component.get_template()` will be removed in v1.
|
||||
|
||||
In v1, each Component will have at most one static template.
|
||||
This is needed to enable support for Markdown, Pug, or other pre-processing of templates by extensions.
|
||||
|
||||
If you are using the deprecated methods to point to different templates, there's 2 ways to migrate:
|
||||
|
||||
1. Split the single Component into multiple Components, each with its own template. Then switch between them in `Component.on_render()`:
|
||||
|
||||
```py
|
||||
class MyComponentA(Component):
|
||||
template_file = "a.html"
|
||||
|
||||
class MyComponentB(Component):
|
||||
template_file = "b.html"
|
||||
|
||||
class MyComponent(Component):
|
||||
def on_render(self, context, template):
|
||||
if context["a"]:
|
||||
return MyComponentA.render(context)
|
||||
else:
|
||||
return MyComponentB.render(context)
|
||||
```
|
||||
|
||||
2. Alternatively, use `Component.on_render()` with Django's `get_template()` to dynamically render different templates:
|
||||
|
||||
```py
|
||||
from django.template.loader import get_template
|
||||
|
||||
class MyComponent(Component):
|
||||
def on_render(self, context, template):
|
||||
if context["a"]:
|
||||
template_name = "a.html"
|
||||
else:
|
||||
template_name = "b.html"
|
||||
|
||||
actual_template = get_template(template_name)
|
||||
return actual_template.render(context)
|
||||
```
|
||||
|
||||
Read more in [django-components#1204](https://github.com/django-components/django-components/discussions/1204).
|
||||
|
||||
- The `type` kwarg in `Component.render()` and `Component.render_to_response()` is now deprecated. Use `deps_strategy` instead. The `type` kwarg will be removed in v1.
|
||||
|
||||
Before:
|
||||
|
@ -651,6 +730,22 @@ Summary:
|
|||
|
||||
**Miscellaneous**
|
||||
|
||||
- Template caching with `cached_template()` helper and `template_cache_size` setting is deprecated.
|
||||
These will be removed in v1.
|
||||
|
||||
This feature made sense if you were dynamically generating templates for components using
|
||||
`Component.get_template_string()` and `Component.get_template()`.
|
||||
|
||||
However, in v1, each Component will have at most one static template. This static template
|
||||
is cached internally per component class, and reused across renders.
|
||||
|
||||
This makes the template caching feature obsolete.
|
||||
|
||||
If you relied on `cached_template()`, you should either:
|
||||
|
||||
1. Wrap the templates as Components.
|
||||
2. Manage the cache of Templates yourself.
|
||||
|
||||
- The `debug_highlight_components` and `debug_highlight_slots` settings are deprecated.
|
||||
These will be removed in v1.
|
||||
|
||||
|
@ -1004,6 +1099,35 @@ Summary:
|
|||
can now be accessed also outside of the render call. So now its possible to take the component
|
||||
instance out of `get_template_data()` (although this is not recommended).
|
||||
|
||||
- Components can now be defined without a template.
|
||||
|
||||
Previously, the following would raise an error:
|
||||
|
||||
```py
|
||||
class MyComponent(Component):
|
||||
pass
|
||||
```
|
||||
|
||||
"Template-less" components can be used together with `Component.on_render()` to dynamically
|
||||
pick what to render:
|
||||
|
||||
```py
|
||||
class TableNew(Component):
|
||||
template_file = "table_new.html"
|
||||
|
||||
class TableOld(Component):
|
||||
template_file = "table_old.html"
|
||||
|
||||
class Table(Component):
|
||||
def on_render(self, context, template):
|
||||
if self.kwargs.get("feat_table_new_ui"):
|
||||
return TableNew.render(args=self.args, kwargs=self.kwargs, slots=self.slots)
|
||||
else:
|
||||
return TableOld.render(args=self.args, kwargs=self.kwargs, slots=self.slots)
|
||||
```
|
||||
|
||||
"Template-less" components can be also used as a base class for other components, or as mixins.
|
||||
|
||||
- Passing `Slot` instance to `Slot` constructor raises an error.
|
||||
|
||||
#### Fix
|
||||
|
|
|
@ -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]
|
||||
|
||||
```
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -94,7 +94,6 @@ COMPONENTS = ComponentsSettings(
|
|||
dirs=[BASE_DIR / "components"],
|
||||
# app_dirs=["components"],
|
||||
# libraries=[],
|
||||
# template_cache_size=128,
|
||||
# context_behavior="isolated", # "django" | "isolated"
|
||||
)
|
||||
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -66,7 +66,6 @@ if not settings.configured:
|
|||
}
|
||||
],
|
||||
COMPONENTS={
|
||||
"template_cache_size": 128,
|
||||
"autodiscover": False,
|
||||
"context_behavior": CONTEXT_MODE,
|
||||
},
|
||||
|
|
|
@ -37,7 +37,6 @@ if not settings.configured:
|
|||
}
|
||||
],
|
||||
COMPONENTS={
|
||||
"template_cache_size": 128,
|
||||
"autodiscover": False,
|
||||
"context_behavior": CONTEXT_MODE,
|
||||
},
|
||||
|
|
|
@ -66,7 +66,6 @@ if not settings.configured:
|
|||
}
|
||||
],
|
||||
COMPONENTS={
|
||||
"template_cache_size": 128,
|
||||
"autodiscover": False,
|
||||
"context_behavior": CONTEXT_MODE,
|
||||
},
|
||||
|
|
|
@ -37,7 +37,6 @@ if not settings.configured:
|
|||
}
|
||||
],
|
||||
COMPONENTS={
|
||||
"template_cache_size": 128,
|
||||
"autodiscover": False,
|
||||
"context_behavior": CONTEXT_MODE,
|
||||
},
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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 = """
|
||||
|
|
|
@ -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' %}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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' %}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
#######################
|
||||
|
|
|
@ -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'"),
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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": [],
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue