refactor: simplify slot API (#1202)

Closes #1096
This commit is contained in:
Juro Oravec 2025-05-20 09:48:45 +02:00 committed by GitHub
parent 7a49a7806c
commit f069255b64
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 404 additions and 72 deletions

View file

@ -236,15 +236,103 @@
After:
```py
slot = Slot(contents=lambda *a, **kw: "CONTENT")
slot = Slot(contents=lambda ctx: "CONTENT")
```
Alternatively, pass the function / content as first positional argument:
```py
slot = Slot(lambda *a, **kw: "CONTENT")
slot = Slot(lambda ctx: "CONTENT")
```
- Slot functions behavior has changed. See the new [Slots](https://django-components.github.io/django-components/latest/concepts/fundamentals/slots/) docs for more info.
- Function signature:
1. All parameters are now passed under a single `ctx` argument.
You can still access all the same parameters via `ctx.context`, `ctx.data`, and `ctx.fallback`.
2. `context` and `fallback` now may be `None` if the slot function was called outside of `{% slot %}` tag.
Before:
```py
def slot_fn(context: Context, data: Dict, slot_ref: SlotRef):
isinstance(context, Context)
isinstance(data, Dict)
isinstance(slot_ref, SlotRef)
return "CONTENT"
```
After:
```py
def slot_fn(ctx: SlotContext):
assert isinstance(ctx.context, Context) # May be None
assert isinstance(ctx.data, Dict)
assert isinstance(ctx.fallback, SlotFallback) # May be None
return "CONTENT"
```
- Calling slot functions:
1. Rather than calling the slot functions directly, you should now call the `Slot` instances.
2. All parameters are now optional.
3. The order of parameters has changed.
Before:
```py
def slot_fn(context: Context, data: Dict, slot_ref: SlotRef):
return "CONTENT"
html = slot_fn(context, data, slot_ref)
```
After:
```py
def slot_fn(ctx: SlotContext):
return "CONTENT"
slot = Slot(slot_fn)
html = slot()
html = slot({"data1": "abc", "data2": "hello"})
html = slot({"data1": "abc", "data2": "hello"}, fallback="FALLBACK")
```
- Usage in components:
Before:
```python
class MyComponent(Component):
def get_context_data(self, *args, **kwargs):
slots = self.input.slots
slot_fn = slots["my_slot"]
html = slot_fn(context, data, slot_ref)
return {
"html": html,
}
```
After:
```python
class MyComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
slot_fn = slots["my_slot"]
html = slot_fn(data)
return {
"html": html,
}
```
**Miscellaneous**
- The second argument to `render_dependencies()` is now `strategy` instead of `type`.

View file

@ -131,7 +131,7 @@ MyComponent.render(
```py
MyComponent.render(
kwargs={"position": "left"},
slots={"content": lambda *a, **kwa: "Hello, Alice"}
slots={"content": lambda ctx: "Hello, Alice"}
)
```

View file

@ -513,7 +513,7 @@ Button.render(
age=30,
),
slots=Button.Slots(
footer=Slot(lambda *a, **kwa: "Click me!"),
footer=Slot(lambda ctx: "Click me!"),
),
)
```

View file

@ -128,7 +128,7 @@ Button.render(
age=30,
),
slots=Button.Slots(
footer=Slot(lambda *a, **kwa: "Click me!"),
footer=Slot(lambda ctx: "Click me!"),
),
)
```
@ -601,7 +601,7 @@ Button.render(
},
slots={
"my_slot": "...",
"another_slot": Slot(lambda: ...),
"another_slot": Slot(lambda ctx: ...),
},
)
```
@ -662,7 +662,7 @@ Button.render(
),
slots=Button.Slots(
my_slot="...",
another_slot=Slot(lambda: ...),
another_slot=Slot(lambda ctx: ...),
),
)
```

View file

@ -50,7 +50,16 @@ from django_components.extensions.defaults import ComponentDefaults, Default
from django_components.extensions.view import ComponentView, get_component_url
from django_components.library import TagProtectedError
from django_components.node import BaseNode, template_tag
from django_components.slots import Slot, SlotContent, SlotFallback, SlotFunc, SlotInput, SlotRef, SlotResult
from django_components.slots import (
Slot,
SlotContent,
SlotContext,
SlotFallback,
SlotFunc,
SlotInput,
SlotRef,
SlotResult,
)
from django_components.tag_formatter import (
ComponentFormatter,
ShorthandComponentFormatter,
@ -125,6 +134,7 @@ __all__ = [
"ShorthandComponentFormatter",
"Slot",
"SlotContent",
"SlotContext",
"SlotFallback",
"SlotFunc",
"SlotInput",

View file

@ -458,7 +458,7 @@ class Component(metaclass=ComponentMeta):
Table.render(
slots=Table.Slots(
header="HELLO IM HEADER",
footer=Slot(lambda: ...),
footer=Slot(lambda ctx: ...),
),
)
```
@ -2132,8 +2132,8 @@ class Component(metaclass=ComponentMeta):
Button.render(
slots={
"content": "Click me!"
"content2": lambda *a, **kwa: "Click me!",
"content3": Slot(lambda *a, **kwa: "Click me!"),
"content2": lambda ctx: "Click me!",
"content3": Slot(lambda ctx: "Click me!"),
},
)
```
@ -2260,7 +2260,7 @@ class Component(metaclass=ComponentMeta):
age=30,
),
slots=Button.Slots(
footer=Slot(lambda *a, **kwa: "Click me!"),
footer=Slot(lambda ctx: "Click me!"),
),
)
```

View file

@ -38,7 +38,7 @@ from django_components.util.misc import get_index, get_last_index, is_identifier
if TYPE_CHECKING:
from django_components.component import ComponentContext, ComponentNode
TSlotData = TypeVar("TSlotData", bound=Mapping, contravariant=True)
TSlotData = TypeVar("TSlotData", bound=Mapping)
DEFAULT_SLOT_KEY = "default"
FILL_GEN_CONTEXT_KEY = "_DJANGO_COMPONENTS_GEN_FILL"
@ -54,32 +54,193 @@ SlotResult = Union[str, SafeString]
"""Type representing the result of a slot render function."""
@dataclass(frozen=True)
class SlotContext(Generic[TSlotData]):
"""
Metadata available inside slot functions.
Read more about [Slot functions](../../concepts/fundamentals/slots#slot-class).
**Example:**
```python
from django_components import SlotContext
def my_slot(ctx: SlotContext):
return f"Hello, {ctx.data['name']}!"
```
You can pass a type parameter to the `SlotContext` to specify the type of the data passed to the slot:
```python
class MySlotData(TypedDict):
name: str
def my_slot(ctx: SlotContext[MySlotData]):
return f"Hello, {ctx.data['name']}!"
```
"""
data: TSlotData
"""
Data passed to the slot.
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
**Example:**
```python
def my_slot(ctx: SlotContext):
return f"Hello, {ctx.data['name']}!"
```
"""
fallback: Optional[Union[str, "SlotFallback"]] = None
"""
Slot's fallback content. Lazily-rendered - coerce this value to string to force it to render.
Read more about [Slot fallback](../../concepts/fundamentals/slots#slot-fallback).
**Example:**
```python
def my_slot(ctx: SlotContext):
return f"Hello, {ctx.fallback}!"
```
May be `None` if you call the slot fill directly, without using [`{% slot %}`](../template_tags#slot) tags.
"""
context: Optional[Context] = None
"""
Django template [`Context`](https://docs.djangoproject.com/en/5.1/ref/templates/api/#django.template.Context)
available inside the [`{% fill %}`](../template_tags#fill) tag.
May be `None` if you call the slot fill directly, without using [`{% slot %}`](../template_tags#slot) tags.
"""
@runtime_checkable
class SlotFunc(Protocol, Generic[TSlotData]):
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult: ... # noqa E704
"""
When rendering components with
[`Component.render()`](../api#django_components.Component.render)
or
[`Component.render_to_response()`](../api#django_components.Component.render_to_response),
the slots can be given either as strings or as functions.
If a slot is given as a function, it will have the signature of `SlotFunc`.
Read more about [Slot functions](../../concepts/fundamentals/slots#slot-functions).
Args:
ctx (SlotContext): Single named tuple that holds the slot data and metadata.
Returns:
(str | SafeString): The rendered slot content.
**Example:**
```python
from django_components import SlotContext, SlotResult
def header(ctx: SlotContext) -> SlotResult:
if ctx.data.get("name"):
return f"Hello, {ctx.data['name']}!"
else:
return ctx.fallback
html = MyTable.render(
slots={
"header": header,
},
)
```
"""
def __call__(self, ctx: SlotContext[TSlotData]) -> SlotResult: ... # noqa E704
@dataclass
class Slot(Generic[TSlotData]):
"""This class holds the slot content function along with related metadata."""
"""
This class is the main way for defining and handling slots.
It holds the slot content function along with related metadata.
Read more about [Slot class](../../concepts/fundamentals/slots#slot-class).
**Example:**
Passing slots to components:
```python
from django_components import Slot
slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
MyComponent.render(
slots={
"my_slot": slot,
},
)
```
Accessing slots inside the components:
```python
from django_components import Component
class MyComponent(Component):
def get_template_data(self, args, kwargs, slots, context):
my_slot = slots["my_slot"]
return {
"my_slot": my_slot,
}
```
Rendering slots:
```python
from django_components import Slot
slot = Slot(lambda ctx: f"Hello, {ctx.data['name']}!")
html = slot({"name": "John"}) # Output: Hello, John!
```
"""
contents: Any
"""
The original value that was passed to the `Slot` constructor.
If the slot was defined with `{% fill %}` tag, this will be the raw string contents of the slot.
- If Slot was created from [`{% fill %}`](../template_tags#fill) tag, `Slot.contents` will contain
the body (string) of that `{% fill %}` tag.
- If Slot was created from string as `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 another `Slot` as `Slot(slot)`, `Slot.contents` will contain the inner
slot's `Slot.contents`.
Read more about [Slot contents](../../concepts/fundamentals/slots#slot-contents).
"""
content_func: SlotFunc[TSlotData] = cast(SlotFunc[TSlotData], None)
"""
The actual slot function.
Do NOT call this function directly, instead call the `Slot` instance as a function.
Read more about [Rendering slot functions](../../concepts/fundamentals/slots#rendering-slots).
"""
escaped: bool = False
"""Whether the slot content has been escaped."""
# Following fields are only for debugging
component_name: Optional[str] = None
"""Name of the component that originally defined or accepted this slot fill."""
"""Name of the component that originally received this slot fill."""
slot_name: Optional[str] = None
"""Name of the slot that originally defined or accepted this slot fill."""
"""Slot name to which this Slot was initially assigned."""
nodelist: Optional[NodeList] = None
"""If the slot was defined with `{% fill %} tag, this will be the Nodelist of the slot content."""
"""
If the slot was defined with [`{% fill %}`](../template_tags#fill) tag,
this will be the Nodelist of the fill's content.
"""
def __post_init__(self) -> None:
# Since the `Slot` instance is treated as a function, it may be passed as `contents`
@ -104,13 +265,23 @@ class Slot(Generic[TSlotData]):
raise ValueError(f"Slot content must be a callable, got: {self.content_func}")
# Allow to treat the instances as functions
def __call__(self, ctx: Context, slot_data: TSlotData, slot_ref: "SlotFallback") -> SlotResult:
return self.content_func(ctx, slot_data, slot_ref)
def __call__(
self,
data: Optional[TSlotData] = None,
fallback: Optional[Union[str, "SlotFallback"]] = None,
context: Optional[Context] = None,
) -> SlotResult:
slot_ctx: SlotContext = SlotContext(context=context, data=data or {}, fallback=fallback)
return self.content_func(slot_ctx)
# Make Django pass the instances of this class within the templates without calling
# the instances as a function.
@property
def do_not_call_in_templates(self) -> bool:
"""
Django special property to prevent calling the instance as a function
inside Django templates.
"""
return True
def __repr__(self) -> str:
@ -177,10 +348,10 @@ html = Table.render(
"header": mark_safe("<i><am><safe>"),
# Function
"footer": lambda ctx, slot_data, slot_ref: f"Page: {slot_data['page_number']}!",
"footer": lambda ctx: f"Page: {ctx.data['page_number']}!",
# Slot instance
"footer": Slot(lambda ctx, slot_data, slot_ref: f"Page: {slot_data['page_number']}!"),
"footer": Slot(lambda ctx: f"Page: {ctx.data['page_number']}!"),
# None (Same as no slot)
"header": None,
@ -211,8 +382,8 @@ class SlotFallback:
Usage in slot functions:
```py
def slot_function(self, ctx: Context, slot_data: TSlotData, fallback: SlotFallback):
return f"Hello, {fallback}!"
def slot_function(self, ctx: SlotContext):
return f"Hello, {ctx.fallback}!"
```
"""
@ -637,7 +808,7 @@ class SlotNode(BaseNode):
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
extra_context[key] = value
slot_ref = SlotFallback(self, context)
fallback = SlotFallback(self, context)
# For the user-provided slot fill, we want to use the context of where the slot
# came from (or current context if configured so)
@ -658,7 +829,7 @@ class SlotNode(BaseNode):
# Render slot as a function
# NOTE: While `{% fill %}` tag has to opt in for the `fallback` and `data` variables,
# the render function ALWAYS receives them.
output = slot(used_ctx, kwargs, slot_ref)
output = slot(data=kwargs, fallback=fallback, context=used_ctx)
if app_settings.DEBUG_HIGHLIGHT_SLOTS:
output = apply_component_highlight("slot", output, f"{component_name} - {slot_name}")
@ -1119,7 +1290,7 @@ def normalize_slot_fills(
# NOTE: `gen_escaped_content_func` is defined as a separate function, instead of being inlined within
# the forloop, because the value the forloop variable points to changes with each loop iteration.
def gen_escaped_content_func(content: SlotFunc, slot_name: str) -> Slot:
def gen_escaped_content_func(content: Union[SlotFunc, Slot], slot_name: str) -> Slot:
# Case: Already Slot, already escaped, and names assigned, so nothing to do.
if isinstance(content, Slot) and content.escaped and content.slot_name and content.component_name:
return content
@ -1128,8 +1299,8 @@ def normalize_slot_fills(
# so we can assign metadata to our internal copies.
if not isinstance(content, Slot) or not content.escaped:
# We wrap the original function so we post-process it by escaping the result.
def content_fn(ctx: Context, slot_data: Dict, fallback: SlotFallback) -> SlotResult:
rendered = content(ctx, slot_data, fallback)
def content_fn(ctx: SlotContext) -> SlotResult:
rendered = content(ctx)
return conditional_escape(rendered) if escape_content else rendered
content_func = cast(SlotFunc, content_fn)
@ -1227,17 +1398,19 @@ def _nodelist_to_slot(
# This allows the template to access current RenderContext layer.
template._djc_is_component_nested = True
def render_func(ctx: Context, slot_data: Dict[str, Any], slot_ref: SlotFallback) -> SlotResult:
def render_func(ctx: SlotContext) -> SlotResult:
context = ctx.context or Context()
# Expose the kwargs that were passed to the `{% slot %}` tag. These kwargs
# are made available through a variable name that was set on the `{% fill %}`
# tag.
if data_var:
ctx[data_var] = slot_data
context[data_var] = ctx.data
# If slot fill is using `{% fill "myslot" fallback="abc" %}`, then set the "abc" to
# the context, so users can refer to the fallback slot from within the fill content.
if fallback_var:
ctx[fallback_var] = slot_ref
context[fallback_var] = ctx.fallback
# NOTE: If a `{% fill %}` tag inside a `{% component %}` tag is inside a forloop,
# the `extra_context` contains the forloop variables. We want to make these available
@ -1260,7 +1433,7 @@ def _nodelist_to_slot(
# HOWEVER, the layer with `_COMPONENT_CONTEXT_KEY` also contains user-defined data from `get_template_data()`.
# Data from `get_template_data()` should take precedence over `extra_context`. So we have to insert
# the forloop variables BEFORE that.
index_of_last_component_layer = get_last_index(ctx.dicts, lambda d: _COMPONENT_CONTEXT_KEY in d)
index_of_last_component_layer = get_last_index(context.dicts, lambda d: _COMPONENT_CONTEXT_KEY in d)
if index_of_last_component_layer is None:
index_of_last_component_layer = 0
@ -1272,18 +1445,18 @@ def _nodelist_to_slot(
# Insert the `extra_context` layer BEFORE the layer that defines the variables from get_template_data.
# Thus, get_template_data will overshadow these on conflict.
ctx.dicts.insert(index_of_last_component_layer, extra_context or {})
context.dicts.insert(index_of_last_component_layer, extra_context or {})
trace_component_msg("RENDER_NODELIST", component_name, component_id=None, slot_name=slot_name)
# We wrap the slot nodelist in Template. However, we also override Django's `Template.render()`
# to call `render_dependencies()` on the results. So we need to set the strategy to `ignore`
# so that the dependencies are processed only once the whole component tree is rendered.
with ctx.push({"DJC_DEPS_STRATEGY": "ignore"}):
rendered = template.render(ctx)
with context.push({"DJC_DEPS_STRATEGY": "ignore"}):
rendered = template.render(context)
# After the rendering is done, remove the `extra_context` from the context stack
ctx.dicts.pop(index_of_last_component_layer)
context.dicts.pop(index_of_last_component_layer)
return rendered

View file

@ -322,7 +322,8 @@ class TestComponentRenderAPI:
assert self.input.kwargs == {"variable": "test", "another": 1}
assert isinstance(self.input.context, Context)
assert list(self.input.slots.keys()) == ["my_slot"]
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
my_slot = self.input.slots["my_slot"]
assert my_slot() == "MY_SLOT"
return {
"variable": kwargs["variable"],
@ -334,7 +335,8 @@ class TestComponentRenderAPI:
assert self.input.kwargs == {"variable": "test", "another": 1}
assert isinstance(self.input.context, Context)
assert list(self.input.slots.keys()) == ["my_slot"]
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
my_slot = self.input.slots["my_slot"]
assert my_slot() == "MY_SLOT"
template_str: types.django_html = """
{% load component_tags %}

View file

@ -346,5 +346,5 @@ class TestComponentCache:
):
TestComponent.render(
kwargs={"input": "cake"},
slots={"content": lambda *a, **kwa: "ONE"},
slots={"content": lambda ctx: "ONE"},
)

View file

@ -76,7 +76,7 @@ class TestComponentTyping:
kwargs=Button.Kwargs(name="name", age=123),
slots=Button.Slots(
header="HEADER",
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
footer=Slot(lambda ctx: "FOOTER"),
),
)
@ -124,7 +124,7 @@ class TestComponentTyping:
kwargs={"name": "name", "age": 123},
slots={
"header": "HEADER",
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
"footer": Slot(lambda ctx: "FOOTER"),
},
)
@ -206,7 +206,7 @@ class TestComponentTyping:
kwargs={"name": "name", "age": 123},
slots={
"header": "HEADER",
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
"footer": Slot(lambda ctx: "FOOTER"),
},
)
@ -314,7 +314,7 @@ class TestComponentTyping:
kwargs={"name": "name", "age": 123},
slots={
"header": "HEADER",
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
"footer": Slot(lambda ctx: "FOOTER"),
},
)
@ -397,7 +397,7 @@ class TestComponentTyping:
kwargs=Button.Kwargs(name="name", age=123),
slots=Button.Slots(
header="HEADER",
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
footer=Slot(lambda ctx: "FOOTER"),
),
)
@ -412,7 +412,7 @@ class TestComponentTyping:
kwargs=Button.Kwargs(name="name", age=123),
slots=Button.Slots(
header="HEADER",
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
footer=Slot(lambda ctx: "FOOTER"),
),
)
@ -427,7 +427,7 @@ class TestComponentTyping:
kwargs=Button.Kwargs(age=123), # type: ignore[call-arg]
slots=Button.Slots(
header="HEADER",
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
footer=Slot(lambda ctx: "FOOTER"),
),
)
@ -438,7 +438,7 @@ class TestComponentTyping:
args=Button.Args(arg1="arg1", arg2="arg2"),
kwargs=Button.Kwargs(name="name", age=123),
slots=Button.Slots( # type: ignore[typeddict-item]
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"), # Missing header
footer=Slot(lambda ctx: "FOOTER"), # Missing header
),
)

View file

@ -4,7 +4,6 @@ For tests focusing on the `{% slot %}` tag, see `test_templatetags_slot_fill.py`
"""
import re
from typing import Dict
import pytest
from django.template import Context, Template, TemplateSyntaxError
@ -12,7 +11,7 @@ from django.template.base import NodeList, TextNode
from pytest_django.asserts import assertHTMLEqual
from django_components import Component, register, types
from django_components.slots import Slot, SlotFallback
from django_components.slots import Slot, SlotContext, SlotFallback
from django_components.testing import djc_test
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
@ -49,43 +48,44 @@ class TestSlot:
"kwargs": kwargs,
}
def first_slot(ctx: Context, slot_data: Dict, slot_ref: SlotFallback):
assert isinstance(ctx, Context)
def slot_fn(ctx: SlotContext):
context = ctx.context
assert isinstance(context, Context)
# NOTE: Since the slot has access to the Context object, it should behave
# the same way as it does in templates - when in "isolated" mode, then the
# slot fill has access only to the "root" context, but not to the data of
# get_template_data() of SimpleComponent.
if is_isolated:
assert ctx.get("the_arg") is None
assert ctx.get("the_kwarg") is None
assert ctx.get("kwargs") is None
assert ctx.get("abc") is None
assert context.get("the_arg") is None
assert context.get("the_kwarg") is None
assert context.get("kwargs") is None
assert context.get("abc") is None
else:
assert ctx["the_arg"] == "1"
assert ctx["the_kwarg"] == 3
assert ctx["kwargs"] == {}
assert ctx["abc"] == "def"
assert context["the_arg"] == "1"
assert context["the_kwarg"] == 3
assert context["kwargs"] == {}
assert context["abc"] == "def"
slot_data_expected = {
"data1": "abc",
"data2": {"hello": "world", "one": 123},
}
assert slot_data_expected == slot_data
assert slot_data_expected == ctx.data
assert isinstance(slot_ref, SlotFallback)
assert "SLOT_DEFAULT" == str(slot_ref).strip()
assert isinstance(ctx.fallback, SlotFallback)
assert "SLOT_DEFAULT" == str(ctx.fallback).strip()
return f"FROM_INSIDE_FIRST_SLOT | {slot_ref}"
return f"FROM_INSIDE_SLOT_FN | {ctx.fallback}"
rendered = SimpleComponent.render(
context={"abc": "def"},
args=["1"],
kwargs={"the_kwarg": 3},
slots={"first": first_slot},
slots={"first": slot_fn},
)
assertHTMLEqual(
rendered,
"FROM_INSIDE_FIRST_SLOT | SLOT_DEFAULT",
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
)
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
@ -116,7 +116,66 @@ class TestSlot:
)
SimpleComponent.render(
slots={"first": "FIRST_SLOT"},
slots={"first": "SLOT_FN"},
)
def test_render_slot_in_python__minimal(self):
def slot_fn(ctx: SlotContext):
assert ctx.context is None
assert ctx.data == {}
assert ctx.fallback is None
return "FROM_INSIDE_SLOT_FN"
slot: Slot = Slot(slot_fn)
rendered = slot()
assertHTMLEqual(
rendered,
"FROM_INSIDE_SLOT_FN",
)
def test_render_slot_in_python__with_data(self):
def slot_fn(ctx: SlotContext):
assert ctx.context is not None
assert ctx.context["the_arg"] == "1"
assert ctx.context["the_kwarg"] == 3
assert ctx.context["kwargs"] == {}
assert ctx.context["abc"] == "def"
slot_data_expected = {
"data1": "abc",
"data2": {"hello": "world", "one": 123},
}
assert slot_data_expected == ctx.data
assert isinstance(ctx.fallback, str)
assert "SLOT_DEFAULT" == ctx.fallback
return f"FROM_INSIDE_SLOT_FN | {ctx.fallback}"
slot: Slot = Slot(slot_fn)
context = Context({"the_arg": "1", "the_kwarg": 3, "kwargs": {}, "abc": "def"})
# Test positional arguments
rendered = slot(
{"data1": "abc", "data2": {"hello": "world", "one": 123}},
"SLOT_DEFAULT",
context,
)
assertHTMLEqual(
rendered,
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
)
# Test keyword arguments
rendered2 = slot(
data={"data1": "abc", "data2": {"hello": "world", "one": 123}},
fallback="SLOT_DEFAULT",
context=context,
)
assertHTMLEqual(
rendered2,
"FROM_INSIDE_SLOT_FN | SLOT_DEFAULT",
)
# Part of the slot caching feature - test that static content slots reuse the slot function.
@ -180,7 +239,7 @@ class TestSlot:
nonlocal captured_slots
captured_slots = slots
slot_func = lambda ctx, slot_data, slot_ref: "FROM_INSIDE_SLOT" # noqa: E731
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
SimpleComponent.render(
slots={"first": slot_func},
@ -223,7 +282,7 @@ class TestSlot:
nonlocal captured_slots
captured_slots = slots
slot_func = lambda ctx, slot_data, slot_ref: "FROM_INSIDE_SLOT" # noqa: E731
slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731
SimpleComponent.render(
slots={"first": Slot(slot_func)},

View file

@ -2440,9 +2440,9 @@ class TestSlotInput:
assert seen_slots == {}
header_slot: Slot = Slot(lambda *a, **kw: "HEADER_SLOT")
header_slot: Slot = Slot(lambda ctx: "HEADER_SLOT")
main_slot_str = "MAIN_SLOT"
footer_slot_fn = lambda *a, **kw: "FOOTER_SLOT" # noqa: E731
footer_slot_fn = lambda ctx: "FOOTER_SLOT" # noqa: E731
SlottedComponent.render(
slots={