mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 06:18:17 +00:00
feat: Slot.extra and Slot.source metadata (#1221)
This commit is contained in:
parent
bb129aefab
commit
fa9ae9892f
5 changed files with 356 additions and 49 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -875,17 +875,27 @@ Summary:
|
||||||
|
|
||||||
Then, the `contents` attribute of the `BaseNode` instance will contain the string `"Hello, world!"`.
|
Then, the `contents` attribute of the `BaseNode` instance will contain the string `"Hello, world!"`.
|
||||||
|
|
||||||
- `Slot` class now has a `Slot.contents` attribute, which contains the original contents:
|
- `Slot` class now has 3 new metadata fields:
|
||||||
|
|
||||||
- If `Slot` was created from `{% fill %}` tag, `Slot.contents` will contain the body of the `{% fill %}` tag.
|
1. `Slot.contents` attribute contains the original contents:
|
||||||
- If `Slot` was created from string via `Slot("...")`, `Slot.contents` will contain that string.
|
|
||||||
- If `Slot` was created from a function, `Slot.contents` will contain that function.
|
- If `Slot` was created from `{% fill %}` tag, `Slot.contents` will contain the body of the `{% fill %}` tag.
|
||||||
|
- If `Slot` was created from string via `Slot("...")`, `Slot.contents` will contain that string.
|
||||||
|
- If `Slot` was created from a function, `Slot.contents` will contain that function.
|
||||||
|
|
||||||
|
2. `Slot.extra` attribute where you can put arbitrary metadata about the slot.
|
||||||
|
|
||||||
|
3. `Slot.source` attribute tells where the slot comes from:
|
||||||
|
|
||||||
|
- `'template'` if the slot was created from `{% fill %}` tag.
|
||||||
|
- `'python'` if the slot was created from string, function, or `Slot` instance.
|
||||||
|
|
||||||
|
See [Slot metadata](https://django-components.github.io/django-components/0.140/concepts/fundamentals/slots/#slot-metadata).
|
||||||
|
|
||||||
- `{% fill %}` tag now accepts `body` kwarg to pass a Slot instance to fill.
|
- `{% fill %}` tag now accepts `body` kwarg to pass a Slot instance to fill.
|
||||||
|
|
||||||
First pass a [`Slot`](../api#django_components.Slot) instance to the template
|
First pass a `Slot` instance to the template
|
||||||
with the [`get_template_data()`](../api#django_components.Component.get_template_data)
|
with the `get_template_data()` method:
|
||||||
method:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import component, Slot
|
from django_components import component, Slot
|
||||||
|
|
|
@ -453,6 +453,45 @@ class ColorLoggerExtension(ComponentExtension):
|
||||||
ComponentConfig = ColorLoggerComponentConfig
|
ComponentConfig = ColorLoggerComponentConfig
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Pass slot metadata
|
||||||
|
|
||||||
|
When a slot is passed to a component, it is copied, so that the original slot is not modified
|
||||||
|
with rendering metadata.
|
||||||
|
|
||||||
|
Therefore, don't use slot's identity to associate metadata with the slot:
|
||||||
|
|
||||||
|
```py
|
||||||
|
# ❌ Don't do this:
|
||||||
|
slots_cache = {}
|
||||||
|
|
||||||
|
class LoggerExtension(ComponentExtension):
|
||||||
|
name = "logger"
|
||||||
|
|
||||||
|
def on_component_input(self, ctx: OnComponentInputContext):
|
||||||
|
for slot in ctx.component.slots.values():
|
||||||
|
slots_cache[id(slot)] = {"some": "metadata"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, use the [`Slot.extra`](../../../reference/api#django_components.Slot.extra) attribute,
|
||||||
|
which is copied from the original slot:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# ✅ Do this:
|
||||||
|
class LoggerExtension(ComponentExtension):
|
||||||
|
name = "logger"
|
||||||
|
|
||||||
|
# Save component-level logger settings for each slot when a component is rendered.
|
||||||
|
def on_component_input(self, ctx: OnComponentInputContext):
|
||||||
|
for slot in ctx.component.slots.values():
|
||||||
|
slot.extra["logger"] = ctx.component.logger
|
||||||
|
|
||||||
|
# Then, when a fill is rendered with `{% slot %}`, we can access the logger settings
|
||||||
|
# from the slot's metadata.
|
||||||
|
def on_slot_rendered(self, ctx: OnSlotRenderedContext):
|
||||||
|
logger = ctx.slot.extra["logger"]
|
||||||
|
logger.log(...)
|
||||||
|
```
|
||||||
|
|
||||||
## Extension commands
|
## Extension commands
|
||||||
|
|
||||||
Extensions in django-components can define custom commands that can be executed via the Django management command interface. This allows for powerful automation and customization capabilities.
|
Extensions in django-components can define custom commands that can be executed via the Django management command interface. This allows for powerful automation and customization capabilities.
|
||||||
|
|
|
@ -723,25 +723,52 @@ html = slot()
|
||||||
|
|
||||||
When accessing slots from within [`Component`](../../../reference/api#django_components.Component) methods,
|
When accessing slots from within [`Component`](../../../reference/api#django_components.Component) methods,
|
||||||
the [`Slot`](../../../reference/api#django_components.Slot) instances are populated
|
the [`Slot`](../../../reference/api#django_components.Slot) instances are populated
|
||||||
with extra metadata [`component_name`](../../../reference/api#django_components.Slot.component_name),
|
with extra metadata:
|
||||||
[`slot_name`](../../../reference/api#django_components.Slot.slot_name), and
|
|
||||||
[`nodelist`](../../../reference/api#django_components.Slot.nodelist).
|
|
||||||
|
|
||||||
These are used for debugging, such as printing the path to the slot in the component tree.
|
- [`component_name`](../../../reference/api#django_components.Slot.component_name)
|
||||||
|
- [`slot_name`](../../../reference/api#django_components.Slot.slot_name)
|
||||||
|
- [`nodelist`](../../../reference/api#django_components.Slot.nodelist)
|
||||||
|
- [`source`](../../../reference/api#django_components.Slot.source)
|
||||||
|
- [`extra`](../../../reference/api#django_components.Slot.extra)
|
||||||
|
|
||||||
When you create a slot, you can set these fields too:
|
These are populated the first time a slot is passed to a component.
|
||||||
|
|
||||||
|
So if you pass the same slot through multiple nested components, the metadata will
|
||||||
|
still point to the first component that received the slot.
|
||||||
|
|
||||||
|
You can use these for debugging, such as printing out the slot's component name and slot name.
|
||||||
|
|
||||||
|
Extensions can use [`Slot.source`](../../../reference/api#django_components.Slot.source)
|
||||||
|
to handle slots differently based on whether the slot
|
||||||
|
was defined in the template with [`{% fill %}`](../../../reference/template_tags#fill) tag
|
||||||
|
or in the component's Python code. See an example in [Pass slot metadata](../../advanced/extensions#pass-slot-metadata).
|
||||||
|
|
||||||
|
You can also pass any additional data along with the slot by setting it in [`Slot.extra`](../../../reference/api#django_components.Slot.extra):
|
||||||
|
|
||||||
|
```py
|
||||||
|
slot = Slot(
|
||||||
|
lambda ctx: f"Hello, {ctx.data['name']}!",
|
||||||
|
extra={"foo": "bar"},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
When you create a slot, you can set any of these fields too:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# Either at slot creation
|
# Either at slot creation
|
||||||
slot = Slot(
|
slot = Slot(
|
||||||
lambda ctx: f"Hello, {ctx.data['name']}!",
|
lambda ctx: f"Hello, {ctx.data['name']}!",
|
||||||
|
# Optional
|
||||||
component_name="table",
|
component_name="table",
|
||||||
slot_name="name",
|
slot_name="name",
|
||||||
|
source="python",
|
||||||
|
extra={},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Or later
|
# Or later
|
||||||
slot.component_name = "table"
|
slot.component_name = "table"
|
||||||
slot.slot_name = "name"
|
slot.slot_name = "name"
|
||||||
|
slot.extra["foo"] = "bar"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Slot contents
|
### Slot contents
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import difflib
|
import difflib
|
||||||
import re
|
import re
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
|
from dataclasses import replace as dataclass_replace
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
|
@ -33,7 +34,7 @@ from django_components.node import BaseNode
|
||||||
from django_components.perfutil.component import component_context_cache
|
from django_components.perfutil.component import component_context_cache
|
||||||
from django_components.util.exception import add_slot_to_error_message
|
from django_components.util.exception import add_slot_to_error_message
|
||||||
from django_components.util.logger import trace_component_msg
|
from django_components.util.logger import trace_component_msg
|
||||||
from django_components.util.misc import get_index, get_last_index, is_identifier
|
from django_components.util.misc import default, get_index, get_last_index, is_identifier
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components.component import Component, ComponentNode
|
from django_components.component import Component, ComponentNode
|
||||||
|
@ -264,6 +265,34 @@ class Slot(Generic[TSlotData]):
|
||||||
|
|
||||||
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
||||||
"""
|
"""
|
||||||
|
source: Literal["template", "python"] = "python"
|
||||||
|
"""
|
||||||
|
Whether the slot was created from a [`{% fill %}`](../template_tags#fill) tag (`'template'`),
|
||||||
|
or Python (`'python'`).
|
||||||
|
|
||||||
|
Extensions can use this info to handle slots differently based on their source.
|
||||||
|
|
||||||
|
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
||||||
|
"""
|
||||||
|
extra: Dict[str, Any] = field(default_factory=dict)
|
||||||
|
"""
|
||||||
|
Dictionary that can be used to store arbitrary metadata about the slot.
|
||||||
|
|
||||||
|
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
|
||||||
|
|
||||||
|
See [Pass slot metadata](../../concepts/advanced/extensions#pass-slot-metadata)
|
||||||
|
for usage for extensions.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Either at slot creation
|
||||||
|
slot = Slot(lambda ctx: "Hello, world!", extra={"foo": "bar"})
|
||||||
|
|
||||||
|
# Or later
|
||||||
|
slot.extra["baz"] = "qux"
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
# Raise if Slot received another Slot instance as `contents`,
|
# Raise if Slot received another Slot instance as `contents`,
|
||||||
|
@ -1366,6 +1395,7 @@ def resolve_fills(
|
||||||
contents=contents,
|
contents=contents,
|
||||||
data_var=None,
|
data_var=None,
|
||||||
fallback_var=None,
|
fallback_var=None,
|
||||||
|
source="template",
|
||||||
)
|
)
|
||||||
|
|
||||||
# The content has fills
|
# The content has fills
|
||||||
|
@ -1375,7 +1405,14 @@ def resolve_fills(
|
||||||
for fill in maybe_fills:
|
for fill in maybe_fills:
|
||||||
# Case: Slot fill was explicitly defined as `{% fill body=... / %}`
|
# Case: Slot fill was explicitly defined as `{% fill body=... / %}`
|
||||||
if fill.body is not None:
|
if fill.body is not None:
|
||||||
slot_fill = fill.body if isinstance(fill.body, Slot) else Slot(fill.body)
|
if isinstance(fill.body, Slot):
|
||||||
|
# Make a copy of the Slot instance and set it to `source="template"`,
|
||||||
|
# so it behaves the same as if the content was written inside the `{% fill %}` tag.
|
||||||
|
# This for example allows CSS scoping to work even on slots that are defined
|
||||||
|
# as `{% fill ... body=... / %}`
|
||||||
|
slot_fill = dataclass_replace(fill.body, source="template")
|
||||||
|
else:
|
||||||
|
slot_fill = Slot(fill.body)
|
||||||
# Case: Slot fill was defined as the body of `{% fill / %}...{% endfill %}`
|
# Case: Slot fill was defined as the body of `{% fill / %}...{% endfill %}`
|
||||||
else:
|
else:
|
||||||
slot_fill = _nodelist_to_slot(
|
slot_fill = _nodelist_to_slot(
|
||||||
|
@ -1386,6 +1423,7 @@ def resolve_fills(
|
||||||
data_var=fill.data_var,
|
data_var=fill.data_var,
|
||||||
fallback_var=fill.fallback_var,
|
fallback_var=fill.fallback_var,
|
||||||
extra_context=fill.extra_context,
|
extra_context=fill.extra_context,
|
||||||
|
source="template",
|
||||||
)
|
)
|
||||||
slots[fill.name] = slot_fill
|
slots[fill.name] = slot_fill
|
||||||
|
|
||||||
|
@ -1459,11 +1497,15 @@ def normalize_slot_fills(
|
||||||
used_slot_name = content.slot_name or slot_name
|
used_slot_name = content.slot_name or slot_name
|
||||||
used_nodelist = content.nodelist
|
used_nodelist = content.nodelist
|
||||||
used_contents = content.contents if content.contents is not None else content_func
|
used_contents = content.contents if content.contents is not None else content_func
|
||||||
|
used_source = content.source
|
||||||
|
used_extra = content.extra.copy()
|
||||||
else:
|
else:
|
||||||
used_component_name = component_name
|
used_component_name = component_name
|
||||||
used_slot_name = slot_name
|
used_slot_name = slot_name
|
||||||
used_nodelist = None
|
used_nodelist = None
|
||||||
used_contents = content_func
|
used_contents = content_func
|
||||||
|
used_source = "python"
|
||||||
|
used_extra = {}
|
||||||
|
|
||||||
slot = Slot(
|
slot = Slot(
|
||||||
contents=used_contents,
|
contents=used_contents,
|
||||||
|
@ -1471,6 +1513,8 @@ def normalize_slot_fills(
|
||||||
component_name=used_component_name,
|
component_name=used_component_name,
|
||||||
slot_name=used_slot_name,
|
slot_name=used_slot_name,
|
||||||
nodelist=used_nodelist,
|
nodelist=used_nodelist,
|
||||||
|
source=used_source,
|
||||||
|
extra=used_extra,
|
||||||
)
|
)
|
||||||
|
|
||||||
return slot
|
return slot
|
||||||
|
@ -1500,6 +1544,8 @@ def _nodelist_to_slot(
|
||||||
data_var: Optional[str] = None,
|
data_var: Optional[str] = None,
|
||||||
fallback_var: Optional[str] = None,
|
fallback_var: Optional[str] = None,
|
||||||
extra_context: Optional[Dict[str, Any]] = None,
|
extra_context: Optional[Dict[str, Any]] = None,
|
||||||
|
source: Optional[Literal["template", "python"]] = None,
|
||||||
|
extra: Optional[Dict[str, Any]] = None,
|
||||||
) -> Slot:
|
) -> Slot:
|
||||||
if data_var:
|
if data_var:
|
||||||
if not data_var.isidentifier():
|
if not data_var.isidentifier():
|
||||||
|
@ -1591,7 +1637,9 @@ def _nodelist_to_slot(
|
||||||
# `BaseNode.contents` which is `None` for self-closing tags like `{% fill "footer" / %}`.
|
# `BaseNode.contents` which is `None` for self-closing tags like `{% fill "footer" / %}`.
|
||||||
# But `Slot(contents=None)` would result in `Slot.contents` being the render function.
|
# But `Slot(contents=None)` would result in `Slot.contents` being the render function.
|
||||||
# So we need to special-case this.
|
# So we need to special-case this.
|
||||||
contents=contents if contents is not None else "",
|
contents=default(contents, ""),
|
||||||
|
source=default(source, "python"),
|
||||||
|
extra=default(extra, {}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -210,7 +210,7 @@ class TestSlot:
|
||||||
|
|
||||||
# Part of the slot caching feature - test that static content slots reuse the slot function.
|
# Part of the slot caching feature - test that static content slots reuse the slot function.
|
||||||
# See https://github.com/django-components/django-components/issues/1164#issuecomment-2854682354
|
# See https://github.com/django-components/django-components/issues/1164#issuecomment-2854682354
|
||||||
def test_slots_reuse_functions__string(self):
|
def test_slots_same_contents__string(self):
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
|
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
@ -229,13 +229,9 @@ class TestSlot:
|
||||||
)
|
)
|
||||||
|
|
||||||
first_slot_func = captured_slots["first"]
|
first_slot_func = captured_slots["first"]
|
||||||
first_nodelist: NodeList = first_slot_func.nodelist
|
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert first_slot_func.content_func is not None
|
assert first_slot_func.content_func is not None
|
||||||
assert first_slot_func.contents == "FIRST_SLOT"
|
assert first_slot_func.contents == "FIRST_SLOT"
|
||||||
assert len(first_nodelist) == 1
|
|
||||||
assert isinstance(first_nodelist[0], TextNode)
|
|
||||||
assert first_nodelist[0].s == "FIRST_SLOT"
|
|
||||||
|
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
SimpleComponent.render(
|
SimpleComponent.render(
|
||||||
|
@ -243,19 +239,15 @@ class TestSlot:
|
||||||
)
|
)
|
||||||
|
|
||||||
second_slot_func = captured_slots["first"]
|
second_slot_func = captured_slots["first"]
|
||||||
second_nodelist: NodeList = second_slot_func.nodelist
|
|
||||||
assert isinstance(second_slot_func, Slot)
|
assert isinstance(second_slot_func, Slot)
|
||||||
assert second_slot_func.content_func is not None
|
assert second_slot_func.content_func is not None
|
||||||
assert second_slot_func.contents == "FIRST_SLOT"
|
assert second_slot_func.contents == "FIRST_SLOT"
|
||||||
assert len(second_nodelist) == 1
|
|
||||||
assert isinstance(second_nodelist[0], TextNode)
|
|
||||||
assert second_nodelist[0].s == "FIRST_SLOT"
|
|
||||||
|
|
||||||
assert first_slot_func.contents == second_slot_func.contents
|
assert first_slot_func.contents == second_slot_func.contents
|
||||||
|
|
||||||
# Part of the slot caching feature - test that consistent functions passed as slots
|
# Part of the slot caching feature - test that consistent functions passed as slots
|
||||||
# reuse the slot function.
|
# reuse the slot function.
|
||||||
def test_slots_reuse_functions__func(self):
|
def test_slots_same_contents__func(self):
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
|
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
@ -279,7 +271,6 @@ class TestSlot:
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert callable(first_slot_func.content_func)
|
assert callable(first_slot_func.content_func)
|
||||||
assert callable(first_slot_func.contents)
|
assert callable(first_slot_func.contents)
|
||||||
assert first_slot_func.nodelist is None
|
|
||||||
|
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
SimpleComponent.render(
|
SimpleComponent.render(
|
||||||
|
@ -290,13 +281,12 @@ class TestSlot:
|
||||||
assert isinstance(second_slot_func, Slot)
|
assert isinstance(second_slot_func, Slot)
|
||||||
assert callable(second_slot_func.content_func)
|
assert callable(second_slot_func.content_func)
|
||||||
assert callable(second_slot_func.contents)
|
assert callable(second_slot_func.contents)
|
||||||
assert second_slot_func.nodelist is None
|
|
||||||
|
|
||||||
assert first_slot_func.contents is second_slot_func.contents
|
assert first_slot_func.contents is second_slot_func.contents
|
||||||
|
|
||||||
# Part of the slot caching feature - test that `Slot` instances with identical function
|
# Part of the slot caching feature - test that `Slot` instances with identical function
|
||||||
# passed as slots reuse the slot function.
|
# passed as slots reuse the slot function.
|
||||||
def test_slots_reuse_functions__slot(self):
|
def test_slots_same_contents__slot(self):
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
|
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
@ -320,7 +310,6 @@ class TestSlot:
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert callable(first_slot_func.content_func)
|
assert callable(first_slot_func.content_func)
|
||||||
assert callable(first_slot_func.contents)
|
assert callable(first_slot_func.contents)
|
||||||
assert first_slot_func.nodelist is None
|
|
||||||
|
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
SimpleComponent.render(
|
SimpleComponent.render(
|
||||||
|
@ -331,13 +320,12 @@ class TestSlot:
|
||||||
assert isinstance(second_slot_func, Slot)
|
assert isinstance(second_slot_func, Slot)
|
||||||
assert callable(second_slot_func.content_func)
|
assert callable(second_slot_func.content_func)
|
||||||
assert callable(second_slot_func.contents)
|
assert callable(second_slot_func.contents)
|
||||||
assert second_slot_func.nodelist is None
|
|
||||||
|
|
||||||
assert first_slot_func.contents == second_slot_func.contents
|
assert first_slot_func.contents == second_slot_func.contents
|
||||||
|
|
||||||
# Part of the slot caching feature - test that identical slot fill content
|
# Part of the slot caching feature - test that identical slot fill content
|
||||||
# slots reuse the slot function.
|
# slots reuse the slot function.
|
||||||
def test_slots_reuse_functions__fill_tag_default(self):
|
def test_slots_same_contents__fill_tag_default(self):
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
|
|
||||||
@register("test")
|
@register("test")
|
||||||
|
@ -363,31 +351,23 @@ class TestSlot:
|
||||||
template.render(Context())
|
template.render(Context())
|
||||||
|
|
||||||
first_slot_func = captured_slots["default"]
|
first_slot_func = captured_slots["default"]
|
||||||
first_nodelist: NodeList = first_slot_func.nodelist
|
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert callable(first_slot_func.content_func)
|
assert callable(first_slot_func.content_func)
|
||||||
assert first_slot_func.contents == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
assert first_slot_func.contents == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
||||||
assert len(first_nodelist) == 1
|
|
||||||
assert isinstance(first_nodelist[0], TextNode)
|
|
||||||
assert first_nodelist[0].s == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
|
||||||
|
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
template.render(Context())
|
template.render(Context())
|
||||||
|
|
||||||
second_slot_func = captured_slots["default"]
|
second_slot_func = captured_slots["default"]
|
||||||
second_nodelist: NodeList = second_slot_func.nodelist
|
|
||||||
assert isinstance(second_slot_func, Slot)
|
assert isinstance(second_slot_func, Slot)
|
||||||
assert callable(second_slot_func.content_func)
|
assert callable(second_slot_func.content_func)
|
||||||
assert second_slot_func.contents == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
assert second_slot_func.contents == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
||||||
assert len(second_nodelist) == 1
|
|
||||||
assert isinstance(second_nodelist[0], TextNode)
|
|
||||||
assert second_nodelist[0].s == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
|
||||||
|
|
||||||
assert first_slot_func.contents == second_slot_func.contents
|
assert first_slot_func.contents == second_slot_func.contents
|
||||||
|
|
||||||
# Part of the slot caching feature - test that identical slot fill content
|
# Part of the slot caching feature - test that identical slot fill content
|
||||||
# slots reuse the slot function.
|
# slots reuse the slot function.
|
||||||
def test_slots_reuse_functions__fill_tag_named(self):
|
def test_slots_same_contents__fill_tag_named(self):
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
|
|
||||||
@register("test")
|
@register("test")
|
||||||
|
@ -415,28 +395,231 @@ class TestSlot:
|
||||||
template.render(Context())
|
template.render(Context())
|
||||||
|
|
||||||
first_slot_func = captured_slots["first"]
|
first_slot_func = captured_slots["first"]
|
||||||
first_nodelist: NodeList = first_slot_func.nodelist
|
|
||||||
assert isinstance(first_slot_func, Slot)
|
assert isinstance(first_slot_func, Slot)
|
||||||
assert callable(first_slot_func.content_func)
|
assert callable(first_slot_func.content_func)
|
||||||
assert first_slot_func.contents == "\n FROM_INSIDE_NAMED_SLOT\n "
|
assert first_slot_func.contents == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||||
assert len(first_nodelist) == 1
|
|
||||||
assert isinstance(first_nodelist[0], TextNode)
|
|
||||||
assert first_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
|
||||||
|
|
||||||
captured_slots = {}
|
captured_slots = {}
|
||||||
template.render(Context())
|
template.render(Context())
|
||||||
|
|
||||||
second_slot_func = captured_slots["first"]
|
second_slot_func = captured_slots["first"]
|
||||||
second_nodelist: NodeList = second_slot_func.nodelist
|
|
||||||
assert isinstance(second_slot_func, Slot)
|
assert isinstance(second_slot_func, Slot)
|
||||||
assert callable(second_slot_func.content_func)
|
assert callable(second_slot_func.content_func)
|
||||||
assert second_slot_func.contents == "\n FROM_INSIDE_NAMED_SLOT\n "
|
assert second_slot_func.contents == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||||
assert len(second_nodelist) == 1
|
|
||||||
assert isinstance(second_nodelist[0], TextNode)
|
|
||||||
assert second_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
|
||||||
|
|
||||||
assert first_slot_func.contents == second_slot_func.contents
|
assert first_slot_func.contents == second_slot_func.contents
|
||||||
|
|
||||||
|
def test_slot_metadata__string(self):
|
||||||
|
captured_slots = {}
|
||||||
|
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" required %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal captured_slots
|
||||||
|
captured_slots = slots
|
||||||
|
|
||||||
|
SimpleComponent.render(
|
||||||
|
slots={"first": "FIRST_SLOT"},
|
||||||
|
)
|
||||||
|
|
||||||
|
first_slot_func = captured_slots["first"]
|
||||||
|
assert isinstance(first_slot_func, Slot)
|
||||||
|
assert first_slot_func.component_name == "SimpleComponent"
|
||||||
|
assert first_slot_func.slot_name == "first"
|
||||||
|
assert first_slot_func.source == "python"
|
||||||
|
assert first_slot_func.extra == {}
|
||||||
|
|
||||||
|
first_nodelist: NodeList = first_slot_func.nodelist
|
||||||
|
assert len(first_nodelist) == 1
|
||||||
|
assert isinstance(first_nodelist[0], TextNode)
|
||||||
|
assert first_nodelist[0].s == "FIRST_SLOT"
|
||||||
|
|
||||||
|
# Part of the slot caching feature - test that consistent functions passed as slots
|
||||||
|
# reuse the slot function.
|
||||||
|
def test_slot_metadata__func(self):
|
||||||
|
captured_slots = {}
|
||||||
|
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" required %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal captured_slots
|
||||||
|
captured_slots = slots
|
||||||
|
|
||||||
|
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
||||||
|
|
||||||
|
SimpleComponent.render(
|
||||||
|
slots={"first": slot_func},
|
||||||
|
)
|
||||||
|
|
||||||
|
first_slot_func = captured_slots["first"]
|
||||||
|
assert isinstance(first_slot_func, Slot)
|
||||||
|
assert first_slot_func.component_name == "SimpleComponent"
|
||||||
|
assert first_slot_func.slot_name == "first"
|
||||||
|
assert first_slot_func.source == "python"
|
||||||
|
assert first_slot_func.extra == {}
|
||||||
|
assert first_slot_func.nodelist is None
|
||||||
|
|
||||||
|
# Part of the slot caching feature - test that `Slot` instances with identical function
|
||||||
|
# passed as slots reuse the slot function.
|
||||||
|
def test_slot_metadata__slot(self):
|
||||||
|
captured_slots = {}
|
||||||
|
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" required %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal captured_slots
|
||||||
|
captured_slots = slots
|
||||||
|
|
||||||
|
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
|
||||||
|
|
||||||
|
SimpleComponent.render(
|
||||||
|
slots={"first": Slot(slot_func, extra={"foo": "bar"}, slot_name="whoop")},
|
||||||
|
)
|
||||||
|
|
||||||
|
first_slot_func = captured_slots["first"]
|
||||||
|
assert isinstance(first_slot_func, Slot)
|
||||||
|
assert first_slot_func.component_name == "SimpleComponent"
|
||||||
|
assert first_slot_func.slot_name == "whoop"
|
||||||
|
assert first_slot_func.source == "python"
|
||||||
|
assert first_slot_func.extra == {"foo": "bar"}
|
||||||
|
assert first_slot_func.nodelist is None
|
||||||
|
|
||||||
|
# Part of the slot caching feature - test that identical slot fill content
|
||||||
|
# slots reuse the slot function.
|
||||||
|
def test_slot_metadata__fill_tag_default(self):
|
||||||
|
captured_slots = {}
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" default %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal captured_slots
|
||||||
|
captured_slots = slots
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
FROM_INSIDE_DEFAULT_SLOT
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
|
||||||
|
template.render(Context())
|
||||||
|
|
||||||
|
first_slot_func = captured_slots["default"]
|
||||||
|
assert isinstance(first_slot_func, Slot)
|
||||||
|
assert first_slot_func.component_name == "test"
|
||||||
|
assert first_slot_func.slot_name == "default"
|
||||||
|
assert first_slot_func.source == "template"
|
||||||
|
assert first_slot_func.extra == {}
|
||||||
|
|
||||||
|
first_nodelist: NodeList = first_slot_func.nodelist
|
||||||
|
assert len(first_nodelist) == 1
|
||||||
|
assert isinstance(first_nodelist[0], TextNode)
|
||||||
|
assert first_nodelist[0].s == "\n FROM_INSIDE_DEFAULT_SLOT\n "
|
||||||
|
|
||||||
|
# Part of the slot caching feature - test that identical slot fill content
|
||||||
|
# slots reuse the slot function.
|
||||||
|
def test_slot_metadata__fill_tag_named(self):
|
||||||
|
captured_slots = {}
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" default %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal captured_slots
|
||||||
|
captured_slots = slots
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
{% fill "first" %}
|
||||||
|
FROM_INSIDE_NAMED_SLOT
|
||||||
|
{% endfill %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
|
||||||
|
template.render(Context())
|
||||||
|
|
||||||
|
first_slot_func = captured_slots["first"]
|
||||||
|
assert isinstance(first_slot_func, Slot)
|
||||||
|
assert first_slot_func.component_name == "test"
|
||||||
|
assert first_slot_func.slot_name == "first"
|
||||||
|
assert first_slot_func.source == "template"
|
||||||
|
assert first_slot_func.extra == {}
|
||||||
|
|
||||||
|
first_nodelist: NodeList = first_slot_func.nodelist
|
||||||
|
assert len(first_nodelist) == 1
|
||||||
|
assert isinstance(first_nodelist[0], TextNode)
|
||||||
|
assert first_nodelist[0].s == "\n FROM_INSIDE_NAMED_SLOT\n "
|
||||||
|
|
||||||
|
# Part of the slot caching feature - test that identical slot fill content
|
||||||
|
# slots reuse the slot function.
|
||||||
|
def test_slot_metadata__fill_tag_body(self):
|
||||||
|
captured_slots = {}
|
||||||
|
|
||||||
|
@register("test")
|
||||||
|
class SimpleComponent(Component):
|
||||||
|
template: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% slot "first" default %}
|
||||||
|
{% endslot %}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal captured_slots
|
||||||
|
captured_slots = slots
|
||||||
|
|
||||||
|
template_str: types.django_html = """
|
||||||
|
{% load component_tags %}
|
||||||
|
{% component "test" %}
|
||||||
|
{% fill "first" body=my_slot / %}
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
template = Template(template_str)
|
||||||
|
|
||||||
|
template.render(
|
||||||
|
Context(
|
||||||
|
{
|
||||||
|
"my_slot": Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT", extra={"foo": "bar"}, slot_name="whoop"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
first_slot_func = captured_slots["first"]
|
||||||
|
assert isinstance(first_slot_func, Slot)
|
||||||
|
assert first_slot_func.component_name == "test"
|
||||||
|
assert first_slot_func.slot_name == "whoop"
|
||||||
|
assert first_slot_func.source == "template"
|
||||||
|
assert first_slot_func.extra == {"foo": "bar"}
|
||||||
|
assert first_slot_func.nodelist is None
|
||||||
|
|
||||||
def test_pass_body_to_fill__slot(self):
|
def test_pass_body_to_fill__slot(self):
|
||||||
@register("test")
|
@register("test")
|
||||||
class SimpleComponent(Component):
|
class SimpleComponent(Component):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue