refactor: Add Node metadata (#1229)

* refactor: `Slot.source` replaced with `Slot.fill_node`, new `Component.node` property, and `slot_node` available in `on_slot_rendered()` hook.

* refactor: fix windows path error in tests
This commit is contained in:
Juro Oravec 2025-06-03 12:58:48 +02:00 committed by GitHub
parent abc6be343e
commit 46e524e37d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 728 additions and 103 deletions

View file

@ -18,6 +18,7 @@ from django_components.util.command import (
from django_components.component import (
Component,
ComponentInput,
ComponentNode,
ComponentVars,
all_components,
get_component_by_class_id,
@ -52,13 +53,16 @@ from django_components.extensions.debug_highlight import ComponentDebugHighlight
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.provide import ProvideNode
from django_components.slots import (
FillNode,
Slot,
SlotContent,
SlotContext,
SlotFallback,
SlotFunc,
SlotInput,
SlotNode,
SlotRef,
SlotResult,
)
@ -103,6 +107,7 @@ __all__ = [
"ComponentInput",
"ComponentMediaInput",
"ComponentMediaInputPath",
"ComponentNode",
"ComponentRegistry",
"ComponentVars",
"ComponentView",
@ -115,6 +120,7 @@ __all__ = [
"DynamicComponent",
"Empty",
"ExtensionComponentConfig",
"FillNode",
"format_attributes",
"get_component_by_class_id",
"get_component_dirs",
@ -131,6 +137,7 @@ __all__ = [
"OnComponentUnregisteredContext",
"OnRegistryCreatedContext",
"OnRegistryDeletedContext",
"ProvideNode",
"register",
"registry",
"RegistrySettings",
@ -142,6 +149,7 @@ __all__ = [
"SlotFallback",
"SlotFunc",
"SlotInput",
"SlotNode",
"SlotRef",
"SlotResult",
"TagFormatterABC",

View file

@ -1941,6 +1941,7 @@ class Component(metaclass=ComponentMeta):
slots: Optional[Any] = None,
deps_strategy: Optional[DependenciesStrategy] = None,
request: Optional[HttpRequest] = None,
node: Optional["ComponentNode"] = None,
id: Optional[str] = None,
):
# TODO_v1 - Remove this whole block in v1. This is for backwards compatibility with pre-v0.140
@ -2009,6 +2010,7 @@ class Component(metaclass=ComponentMeta):
self.request = request
self.outer_context: Optional[Context] = outer_context
self.registry = default(registry, registry_)
self.node = node
extensions._init_component_instance(self)
@ -2346,6 +2348,51 @@ class Component(metaclass=ComponentMeta):
that was used to render the component.
"""
node: Optional["ComponentNode"]
"""
The [`ComponentNode`](../api/#django_components.ComponentNode) instance
that was used to render the component.
This will be set only if the component was rendered with the
[`{% component %}`](../template_tags#component) tag.
Accessing the [`ComponentNode`](../api/#django_components.ComponentNode) is mostly useful for extensions,
which can modify their behaviour based on the source of the Component.
```py
class MyComponent(Component):
def get_template_data(self, context, template):
if self.node is not None:
assert self.node.name == "my_component"
```
For example, if `MyComponent` was used in another component - that is,
with a `{% component "my_component" %}` tag
in a template that belongs to another component - then you can use
[`self.node.template_component`](../api/#django_components.ComponentNode.template_component)
to access the owner [`Component`](../api/#django_components.Component) class.
```djc_py
class Parent(Component):
template: types.django_html = '''
<div>
{% component "my_component" / %}
</div>
'''
@register("my_component")
class MyComponent(Component):
def get_template_data(self, context, template):
if self.node is not None:
assert self.node.template_component == Parent
```
!!! info
`Component.node` is `None` if the component is created by
[`Component.render()`](../api/#django_components.Component.render)
(but you can pass in the `node` kwarg yourself).
"""
# TODO_v1 - Remove, superseded by `Component.slots`
is_filled: SlotIsFilled
"""
@ -2535,6 +2582,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional["ComponentNode"] = None,
**response_kwargs: Any,
) -> HttpResponse:
"""
@ -2603,6 +2651,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry=registry,
registered_name=registered_name,
node=node,
)
return cls.response_class(content, **response_kwargs)
@ -2623,6 +2672,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional["ComponentNode"] = None,
) -> str:
"""
Render the component into a string. This is the equivalent of calling
@ -2830,6 +2880,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry=registry,
registered_name=registered_name,
node=node,
)
# This is the internal entrypoint for the render function
@ -2846,6 +2897,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional["ComponentNode"] = None,
) -> str:
component_name = _get_component_name(cls, registered_name)
@ -2862,6 +2914,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry=registry,
registered_name=registered_name,
node=node,
)
@classmethod
@ -2877,6 +2930,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry: Optional[ComponentRegistry] = None,
registered_name: Optional[str] = None,
node: Optional["ComponentNode"] = None,
) -> str:
######################################
# 1. Handle inputs
@ -2929,6 +2983,7 @@ class Component(metaclass=ComponentMeta):
# TODO_v2 - Remove `registered_name` and `registry`
registry=registry,
registered_name=registered_name,
node=node,
)
# Allow plugins to modify or validate the inputs
@ -3316,7 +3371,7 @@ class ComponentNode(BaseNode):
[`@register()`](./api.md#django_components.register)
decorator.
The `{% component %}` tag takes:
The [`{% component %}`](../template_tags#component) tag takes:
- Component's registered name as the first positional argument,
- Followed by any number of positional and keyword arguments.
@ -3333,7 +3388,8 @@ class ComponentNode(BaseNode):
### Inserting slot fills
If the component defined any [slots](../concepts/fundamentals/slots.md), you can
"fill" these slots by placing the [`{% fill %}`](#fill) tags within the `{% component %}` tag:
"fill" these slots by placing the [`{% fill %}`](../template_tags#fill) tags
within the [`{% component %}`](../template_tags#component) tag:
```django
{% component "my_table" rows=rows headers=headers %}
@ -3343,7 +3399,7 @@ class ComponentNode(BaseNode):
{% endcomponent %}
```
You can even nest [`{% fill %}`](#fill) tags within
You can even nest [`{% fill %}`](../template_tags#fill) tags within
[`{% if %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#if),
[`{% for %}`](https://docs.djangoproject.com/en/5.2/ref/templates/builtins/#for)
and other tags:
@ -3382,7 +3438,7 @@ class ComponentNode(BaseNode):
}
```
### Omitting the `component` keyword
### Omitting the component keyword
If you would like to omit the `component` keyword, and simply refer to your
components by their registered names:
@ -3417,6 +3473,8 @@ class ComponentNode(BaseNode):
nodelist: Optional[NodeList] = None,
node_id: Optional[str] = None,
contents: Optional[str] = None,
template_name: Optional[str] = None,
template_component: Optional[Type["Component"]] = None,
) -> None:
super().__init__(
params=params,
@ -3424,6 +3482,8 @@ class ComponentNode(BaseNode):
nodelist=nodelist,
node_id=node_id,
contents=contents,
template_name=template_name,
template_component=template_component,
)
self.name = name
@ -3499,6 +3559,7 @@ class ComponentNode(BaseNode):
registered_name=self.name,
outer_context=context,
registry=self.registry,
node=self,
)
return output

View file

@ -28,7 +28,7 @@ from django_components.util.routing import URLRoute
if TYPE_CHECKING:
from django_components import Component
from django_components.component_registry import ComponentRegistry
from django_components.slots import Slot, SlotResult
from django_components.slots import Slot, SlotNode, SlotResult
TCallable = TypeVar("TCallable", bound=Callable)
@ -155,6 +155,8 @@ class OnSlotRenderedContext(NamedTuple):
"""The Slot instance that was rendered"""
slot_name: str
"""The name of the `{% slot %}` tag"""
slot_node: "SlotNode"
"""The node instance of the `{% slot %}` tag"""
slot_is_required: bool
"""Whether the slot is required"""
slot_is_default: bool
@ -744,6 +746,26 @@ class ComponentExtension(metaclass=ExtensionMeta):
# Append a comment to the slot's rendered output
return ctx.result + "<!-- MyExtension comment -->"
```
**Access slot metadata:**
You can access the [`{% slot %}` tag](../template_tags#slot)
node ([`SlotNode`](../api#django_components.SlotNode)) and its metadata using `ctx.slot_node`.
For example, to find the [`Component`](../api#django_components.Component) class to which
belongs the template where the [`{% slot %}`](../template_tags#slot) tag is defined, you can use
[`ctx.slot_node.template_component`](../api#django_components.SlotNode.template_component):
```python
from django_components import ComponentExtension, OnSlotRenderedContext
class MyExtension(ComponentExtension):
def on_slot_rendered(self, ctx: OnSlotRenderedContext) -> Optional[str]:
# Access slot metadata
slot_node = ctx.slot_node
slot_owner = slot_node.template_component
print(f"Slot owner: {slot_owner}")
```
"""
pass

View file

@ -1,7 +1,7 @@
import functools
import inspect
import keyword
from typing import Any, Callable, Dict, List, Optional, Tuple, Type, cast
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Optional, Tuple, Type, cast
from django.template import Context, Library
from django.template.base import Node, NodeList, Parser, Token
@ -15,6 +15,9 @@ from django_components.util.template_tag import (
validate_params,
)
if TYPE_CHECKING:
from django_components.component import Component
# Normally, when `Node.render()` is called, it receives only a single argument `context`.
#
@ -252,32 +255,77 @@ class BaseNode(Node, metaclass=NodeMeta):
# PUBLIC API (Configurable by users)
# #####################################
tag: str
tag: ClassVar[str]
"""
The tag name.
E.g. `"component"` or `"slot"` will make this class match
template tags `{% component %}` or `{% slot %}`.
```python
class SlotNode(BaseNode):
tag = "slot"
end_tag = "endslot"
```
This will allow the template tag `{% slot %}` to be used like this:
```django
{% slot %} ... {% endslot %}
```
"""
end_tag: Optional[str] = None
end_tag: ClassVar[Optional[str]] = None
"""
The end tag name.
E.g. `"endcomponent"` or `"endslot"` will make this class match
template tags `{% endcomponent %}` or `{% endslot %}`.
```python
class SlotNode(BaseNode):
tag = "slot"
end_tag = "endslot"
```
This will allow the template tag `{% slot %}` to be used like this:
```django
{% slot %} ... {% endslot %}
```
If not set, then this template tag has no end tag.
So instead of `{% component %} ... {% endcomponent %}`, you'd use only
`{% component %}`.
```python
class MyNode(BaseNode):
tag = "mytag"
end_tag = None
```
"""
allowed_flags: Optional[List[str]] = None
allowed_flags: ClassVar[Optional[List[str]]] = None
"""
The allowed flags for this tag.
The list of all *possible* flags for this tag.
E.g. `["required"]` will allow this tag to be used like `{% slot required %}`.
```python
class SlotNode(BaseNode):
tag = "slot"
end_tag = "endslot"
allowed_flags = ["required", "default"]
```
This will allow the template tag `{% slot %}` to be used like this:
```django
{% slot required %} ... {% endslot %}
{% slot default %} ... {% endslot %}
{% slot required default %} ... {% endslot %}
```
"""
def render(self, context: Context, *args: Any, **kwargs: Any) -> str:
@ -303,6 +351,133 @@ class BaseNode(Node, metaclass=NodeMeta):
"""
return self.nodelist.render(context)
# #####################################
# Attributes
# #####################################
params: List[TagAttr]
"""
The parameters to the tag in the template.
A single param represents an arg or kwarg of the template tag.
E.g. the following tag:
```django
{% component "my_comp" key=val key2='val2 two' %}
```
Has 3 params:
- Posiitonal arg `"my_comp"`
- Keyword arg `key=val`
- Keyword arg `key2='val2 two'`
"""
flags: Dict[str, bool]
"""
Dictionary of all [`allowed_flags`](../api#django_components.BaseNode.allowed_flags)
that were set on the tag.
Flags that were set are `True`, and the rest are `False`.
E.g. the following tag:
```python
class SlotNode(BaseNode):
tag = "slot"
end_tag = "endslot"
allowed_flags = ["default", "required"]
```
```django
{% slot "content" default %}
```
Has 2 flags, `default` and `required`, but only `default` was set.
The `flags` dictionary will be:
```python
{
"default": True,
"required": False,
}
```
You can check if a flag is set by doing:
```python
if node.flags["default"]:
...
```
"""
nodelist: NodeList
"""
The nodelist of the tag.
This is the text between the opening and closing tags, e.g.
```django
{% slot "content" default required %}
<div>
...
</div>
{% endslot %}
```
The `nodelist` will contain the `<div> ... </div>` part.
Unlike [`contents`](../api#django_components.BaseNode.contents),
the `nodelist` contains the actual Nodes, not just the text.
"""
contents: Optional[str]
"""
The contents of the tag.
This is the text between the opening and closing tags, e.g.
```django
{% slot "content" default required %}
<div>
...
</div>
{% endslot %}
```
The `contents` will be `"<div> ... </div>"`.
"""
node_id: str
"""
The unique ID of the node.
Extensions can use this ID to store additional information.
"""
template_name: Optional[str]
"""
The name of the [`Template`](https://docs.djangoproject.com/en/5.2/ref/templates/api/#django.template.Template)
that contains this node.
The template name is set by Django's
[template loaders](https://docs.djangoproject.com/en/5.2/topics/templates/#loaders).
For example, the filesystem template loader will set this to the absolute path of the template file.
```
"/home/user/project/templates/my_template.html"
```
"""
template_component: Optional[Type["Component"]]
"""
If the template that contains this node belongs to a [`Component`](../api#django_components.Component),
then this will be the [`Component`](../api#django_components.Component) class.
"""
# #####################################
# MISC
# #####################################
@ -314,12 +489,16 @@ class BaseNode(Node, metaclass=NodeMeta):
nodelist: Optional[NodeList] = None,
node_id: Optional[str] = None,
contents: Optional[str] = None,
template_name: Optional[str] = None,
template_component: Optional[Type["Component"]] = None,
):
self.params = params
self.flags = flags or {flag: False for flag in self.allowed_flags or []}
self.nodelist = nodelist or NodeList()
self.node_id = node_id or gen_id()
self.contents = contents
self.template_name = template_name
self.template_component = template_component
def __repr__(self) -> str:
return (
@ -329,7 +508,21 @@ class BaseNode(Node, metaclass=NodeMeta):
@property
def active_flags(self) -> List[str]:
"""Flags that were set for this specific instance."""
"""
Flags that were set for this specific instance as a list of strings.
E.g. the following tag:
```django
{% slot "content" default required / %}
```
Will have the following flags:
```python
["default", "required"]
```
"""
flags = []
for flag, value in self.flags.items():
if value:
@ -347,6 +540,9 @@ class BaseNode(Node, metaclass=NodeMeta):
To register the tag, you can use [`BaseNode.register()`](../api#django_components.BaseNode.register).
"""
# NOTE: Avoids circular import
from django_components.template import get_component_from_origin
tag_id = gen_id()
tag = parse_template_tag(cls.tag, cls.end_tag, cls.allowed_flags, parser, token)
@ -359,6 +555,8 @@ class BaseNode(Node, metaclass=NodeMeta):
params=tag.params,
flags=tag.flags,
contents=contents,
template_name=parser.origin.name if parser.origin else None,
template_component=get_component_from_origin(parser.origin) if parser.origin else None,
**kwargs,
)

View file

@ -12,8 +12,11 @@ from django_components.util.misc import gen_id
class ProvideNode(BaseNode):
"""
The "provider" part of the [provide / inject feature](../../concepts/advanced/provide_inject).
The [`{% provide %}`](../template_tags#provide) tag is part of the "provider" part of
the [provide / inject feature](../../concepts/advanced/provide_inject).
Pass kwargs to this tag to define the provider's data.
Any components defined within the `{% provide %}..{% endprovide %}` tags will be able to access this data
with [`Component.inject()`](../api#django_components.Component.inject).
@ -66,7 +69,7 @@ class ProvideNode(BaseNode):
}
```
Notice that the keys defined on the `{% provide %}` tag are then accessed as attributes
Notice that the keys defined on the [`{% provide %}`](../template_tags#provide) tag are then accessed as attributes
when accessing them with [`Component.inject()`](../api#django_components.Component.inject).
Do this

View file

@ -265,14 +265,33 @@ class Slot(Generic[TSlotData]):
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
"""
source: Literal["template", "python"] = "python"
fill_node: Optional[Union["FillNode", "ComponentNode"]] = None
"""
Whether the slot was created from a [`{% fill %}`](../template_tags#fill) tag (`'template'`),
or Python (`'python'`).
If the slot was created from a [`{% fill %}`](../template_tags#fill) tag,
this will be the [`FillNode`](../api/#django_components.FillNode) instance.
If the slot was a default slot created from a [`{% component %}`](../template_tags#component) tag,
this will be the [`ComponentNode`](../api/#django_components.ComponentNode) instance.
Otherwise, this will be `None`.
Extensions can use this info to handle slots differently based on their source.
See [Slot metadata](../../concepts/fundamentals/slots#slot-metadata).
**Example:**
You can use this to find the [`Component`](../api/#django_components.Component) in whose
template the [`{% fill %}`](../template_tags#fill) tag was defined:
```python
class MyTable(Component):
def get_template_data(self, args, kwargs, slots, context):
footer_slot = slots.get("footer")
if footer_slot is not None and footer_slot.fill_node is not None:
owner_component = footer_slot.fill_node.template_component
# ...
```
"""
extra: Dict[str, Any] = field(default_factory=dict)
"""
@ -494,7 +513,7 @@ class SlotIsFilled(dict):
class SlotNode(BaseNode):
"""
Slot tag marks a place inside a component where content can be inserted
[`{% slot %}`](../template_tags#slot) tag marks a place inside a component where content can be inserted
from outside.
[Learn more](../../concepts/fundamentals/slots) about using slots.
@ -508,7 +527,7 @@ class SlotNode(BaseNode):
- `name` (str, required): Registered name of the component to render
- `default`: Optional flag. If there is a default slot, you can pass the component slot content
without using the [`{% fill %}`](#fill) tag. See
without using the [`{% fill %}`](../template_tags#fill) tag. See
[Default slot](../../concepts/fundamentals/slots#default-slot)
- `required`: Optional flag. Will raise an error if a slot is required but not given.
- `**kwargs`: Any extra kwargs will be passed as the slot data.
@ -550,8 +569,8 @@ class SlotNode(BaseNode):
### Slot data
Any extra kwargs will be considered as slot data, and will be accessible in the [`{% fill %}`](#fill)
tag via fill's `data` kwarg:
Any extra kwargs will be considered as slot data, and will be accessible
in the [`{% fill %}`](../template_tags#fill) tag via fill's `data` kwarg:
Read more about [Slot data](../../concepts/fundamentals/slots#slot-data).
@ -588,8 +607,8 @@ class SlotNode(BaseNode):
The content between the `{% slot %}..{% endslot %}` tags is the fallback content that
will be rendered if no fill is given for the slot.
This fallback content can then be accessed from within the [`{% fill %}`](#fill) tag using
the fill's `fallback` kwarg.
This fallback content can then be accessed from within the [`{% fill %}`](../template_tags#fill) tag
using the fill's `fallback` kwarg.
This is useful if you need to wrap / prepend / append the original slot's content.
```djc_py
@ -926,6 +945,7 @@ class SlotNode(BaseNode):
component_id=component_id,
slot=slot,
slot_name=slot_name,
slot_node=self,
slot_is_required=is_required,
slot_is_default=is_default,
result=output,
@ -968,15 +988,16 @@ class SlotNode(BaseNode):
class FillNode(BaseNode):
"""
Use this tag to insert content into component's slots.
Use [`{% fill %}`](../template_tags#fill) tag to insert content into component's
[slots](../../concepts/fundamentals/slots).
`{% fill %}` tag may be used only within a `{% component %}..{% endcomponent %}` block,
[`{% fill %}`](../template_tags#fill) tag may be used only within a `{% component %}..{% endcomponent %}` block,
and raises a `TemplateSyntaxError` if used outside of a component.
**Args:**
- `name` (str, required): Name of the slot to insert this content into. Use `"default"` for
the default slot.
the [default slot](../../concepts/fundamentals/slots#default-slot).
- `data` (str, optional): This argument allows you to access the data passed to the slot
under the specified variable name. See [Slot data](../../concepts/fundamentals/slots#slot-data).
- `fallback` (str, optional): This argument allows you to access the original content of the slot
@ -1061,7 +1082,7 @@ class FillNode(BaseNode):
### Using default slot
To access slot data and the fallback slot content on the default slot,
use `{% fill %}` with `name` set to `"default"`:
use [`{% fill %}`](../template_tags#fill) with `name` set to `"default"`:
```django
{% component "button" %}
@ -1075,7 +1096,7 @@ class FillNode(BaseNode):
### Slot fills from Python
You can pass a slot fill from Python to a component by setting the `body` kwarg
on the `{% fill %}` tag.
on the [`{% fill %}`](../template_tags#fill) tag.
First pass a [`Slot`](../api#django_components.Slot) instance to the template
with the [`get_template_data()`](../api#django_components.Component.get_template_data)
@ -1091,7 +1112,7 @@ class FillNode(BaseNode):
}
```
Then pass the slot to the `{% fill %}` tag:
Then pass the slot to the [`{% fill %}`](../template_tags#fill) tag:
```django
{% component "table" %}
@ -1101,7 +1122,7 @@ class FillNode(BaseNode):
!!! warning
If you define both the `body` kwarg and the `{% fill %}` tag's body,
If you define both the `body` kwarg and the [`{% fill %}`](../template_tags#fill) tag's body,
an error will be raised.
```django
@ -1395,7 +1416,7 @@ def resolve_fills(
contents=contents,
data_var=None,
fallback_var=None,
source="template",
fill_node=component_node,
)
# The content has fills
@ -1405,14 +1426,15 @@ def resolve_fills(
for fill in maybe_fills:
# Case: Slot fill was explicitly defined as `{% fill body=... / %}`
if fill.body is not None:
# Set `Slot.fill_node` so the slot fill behaves the same as if it was defined inside
# a `{% fill %}` tag.
# This for example allows CSS scoping to work even on slots that are defined
# as `{% 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")
# Make a copy of the Slot instance and set its `fill_node`.
slot_fill = dataclass_replace(fill.body, fill_node=fill.fill)
else:
slot_fill = Slot(fill.body)
slot_fill = Slot(fill.body, fill_node=fill.fill)
# Case: Slot fill was defined as the body of `{% fill / %}...{% endfill %}`
else:
slot_fill = _nodelist_to_slot(
@ -1423,7 +1445,7 @@ def resolve_fills(
data_var=fill.data_var,
fallback_var=fill.fallback_var,
extra_context=fill.extra_context,
source="template",
fill_node=fill.fill,
)
slots[fill.name] = slot_fill
@ -1497,14 +1519,14 @@ def normalize_slot_fills(
used_slot_name = content.slot_name or slot_name
used_nodelist = content.nodelist
used_contents = content.contents if content.contents is not None else content_func
used_source = content.source
used_fill_node = content.fill_node
used_extra = content.extra.copy()
else:
used_component_name = component_name
used_slot_name = slot_name
used_nodelist = None
used_contents = content_func
used_source = "python"
used_fill_node = None
used_extra = {}
slot = Slot(
@ -1513,7 +1535,7 @@ def normalize_slot_fills(
component_name=used_component_name,
slot_name=used_slot_name,
nodelist=used_nodelist,
source=used_source,
fill_node=used_fill_node,
extra=used_extra,
)
@ -1544,7 +1566,7 @@ def _nodelist_to_slot(
data_var: Optional[str] = None,
fallback_var: Optional[str] = None,
extra_context: Optional[Dict[str, Any]] = None,
source: Optional[Literal["template", "python"]] = None,
fill_node: Optional[Union[FillNode, "ComponentNode"]] = None,
extra: Optional[Dict[str, Any]] = None,
) -> Slot:
if data_var:
@ -1638,7 +1660,7 @@ def _nodelist_to_slot(
# But `Slot(contents=None)` would result in `Slot.contents` being the render function.
# So we need to special-case this.
contents=default(contents, ""),
source=default(source, "python"),
fill_node=default(fill_node, None),
extra=default(extra, {}),
)