feat: allow to set defaults (#1072)

* feat: allow to set defaults

* docs: update changelog

* refactor: fix new linter errors
This commit is contained in:
Juro Oravec 2025-03-31 10:38:41 +02:00 committed by GitHub
parent 48dd3b7a5a
commit f07818fc7d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 553 additions and 36 deletions

View file

@ -4,6 +4,39 @@
#### Feat
- Add defaults for the component inputs with the `Component.Defaults` nested class. Defaults
are applied if the argument is not given, or if it set to `None`.
For lists, dictionaries, or other objects, wrap the value in `Default()` class to mark it as a factory
function:
```python
from django_components import Default
class Table(Component):
class Defaults:
position = "left"
width = "200px"
options = Default(lambda: ["left", "right", "center"])
def get_context_data(self, position, width, options):
return {
"position": position,
"width": width,
"options": options,
}
# `position` is used as given, `"right"`
# `width` uses default because it's `None`
# `options` uses default because it's missing
Table.render(
kwargs={
"position": "right",
"width": None,
}
)
```
- `{% html_attrs %}` now offers a Vue-like granular control over `class` and `style` HTML attributes,
where each class name or style property can be managed separately.

View file

@ -140,7 +140,6 @@ def prepare_templating_benchmark(
context_mode: DjcContextMode,
imports_only: bool = False,
):
global do_render
setup_script = _get_templating_script(renderer, size, context_mode, imports_only)
# If we're testing the startup time, then the setup is actually the tested code

View file

@ -3,6 +3,7 @@ nav:
- Single-file components: single_file_components.md
- Components in Python: components_in_python.md
- Accessing component inputs: access_component_input.md
- Component defaults: component_defaults.md
- Component context and scope: component_context_scope.md
- Template tag syntax: template_tag_syntax.md
- Slots: slots.md

View file

@ -0,0 +1,133 @@
When a component is being rendered, the component inputs are passed to various methods like
[`get_context_data()`](../../../reference/api#django_components.Component.get_context_data),
[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
or [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
It can be cumbersome to specify default values for each input in each method.
To make things easier, Components can specify their defaults. Defaults are used when
no value is provided, or when the value is set to `None` for a particular input.
### Defining defaults
To define defaults for a component, you create a nested `Defaults` class within your
[`Component`](../../../reference/api#django_components.Component) class.
Each attribute in the `Defaults` class represents a default value for a corresponding input.
```py
from django_components import Component, Default, register
@register("my_table")
class MyTable(Component):
class Defaults:
position = "left"
selected_items = Default(lambda: [1, 2, 3])
def get_context_data(self, position, selected_items):
return {
"position": position,
"selected_items": selected_items,
}
...
```
In this example, `position` is a simple default value, while `selected_items` uses a factory function wrapped in `Default` to ensure a new list is created each time the default is used.
Now, when we render the component, the defaults will be applied:
```django
{% component "my_table" position="right" / %}
```
In this case:
- `position` input is set to `right`, so no defaults applied
- `selected_items` is not set, so it will be set to `[1, 2, 3]`.
Same applies to rendering the Component in Python with the
[`render()`](../../../reference/api#django_components.Component.render) method:
```py
MyTable.render(
kwargs={
"position": "right",
"selected_items": None,
},
)
```
Notice that we've set `selected_items` to `None`. `None` values are treated as missing values,
and so `selected_items` will be set to `[1, 2, 3]`.
!!! warning
The defaults are aplied only to keyword arguments. They are NOT applied to positional arguments!
### Default factories
For objects such as lists, dictionaries or other instances, you have to be careful - if you simply set a default value, this instance will be shared across all instances of the component!
```py
from django_components import Component
class MyTable(Component):
class Defaults:
# All instances will share the same list!
selected_items = [1, 2, 3]
```
To avoid this, you can use a factory function wrapped in `Default`.
```py
from django_components import Component, Default
class MyTable(Component):
class Defaults:
# A new list is created for each instance
selected_items = Default(lambda: [1, 2, 3])
```
This is similar to how the dataclass fields work.
In fact, you can also use the dataclass's [`field`](https://docs.python.org/3/library/dataclasses.html#dataclasses.field) function to define the factories:
```py
from dataclasses import field
from django_components import Component
class MyTable(Component):
class Defaults:
selected_items = field(default_factory=lambda: [1, 2, 3])
```
### Accessing defaults
Since the defaults are defined on the component class, you can access the defaults for a component with the `Component.Defaults` property.
So if we have a component like this:
```py
from django_components import Component, Default, register
@register("my_table")
class MyTable(Component):
class Defaults:
position = "left"
selected_items = Default(lambda: [1, 2, 3])
def get_context_data(self, position, selected_items):
return {
"position": position,
"selected_items": selected_items,
}
```
We can access individual defaults like this:
```py
print(MyTable.Defaults.position)
print(MyTable.Defaults.selected_items)
```

View file

@ -68,7 +68,7 @@ from django_components import Component, register
class Calendar(Component):
template_file = "calendar.html"
...
def get_context_data(self, date: date, extra_class: str | None = None):
def get_context_data(self, date: date, extra_class: str = "text-blue"):
return {
"date": date,
"extra_class": extra_class,
@ -197,7 +197,7 @@ def to_workweek_date(d: date):
class Calendar(Component):
template_file = "calendar.html"
...
def get_context_data(self, date: date, extra_class: str | None = None):
def get_context_data(self, date: date, extra_class: str = "text-blue"):
workweek_date = to_workweek_date(date) # <--- new
return {
"date": workweek_date, # <--- changed
@ -220,3 +220,38 @@ the parametrized version of the component:
---
Next, you will learn [how to use slots give your components even more flexibility ➡️](./adding_slots.md)
### 5. Add defaults
In our example, we've set the `extra_class` to default to `"text-blue"` by setting it in the
[`get_context_data()`](../../reference/api#django_components.Component.get_context_data)
method.
However, you may want to use the same default value in multiple methods, like
[`get_js_data()`](../../reference/api#django_components.Component.get_js_data)
or [`get_css_data()`](../../reference/api#django_components.Component.get_css_data).
To make things easier, Components can specify their defaults. Defaults are used when
no value is provided, or when the value is set to `None` for a particular input.
To define defaults for a component, you create a nested `Defaults` class within your
[`Component`](../../reference/api#django_components.Component) class.
Each attribute in the `Defaults` class represents a default value for a corresponding input.
```py
from django_components import Component, Default, register
@register("calendar")
class Calendar(Component):
template_file = "calendar.html"
class Defaults: # <--- new
extra_class = "text-blue"
def get_context_data(self, date: date, extra_class: str): # <--- changed
workweek_date = to_workweek_date(date)
return {
"date": workweek_date,
"extra_class": extra_class,
}
```

View file

@ -75,6 +75,10 @@
options:
show_if_no_docstring: true
::: django_components.Default
options:
show_if_no_docstring: true
::: django_components.EmptyDict
options:
show_if_no_docstring: true

View file

@ -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#L1584" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1494" target="_blank">See source code</a>

View file

@ -40,6 +40,7 @@ from django_components.extension import (
OnComponentInputContext,
OnComponentDataContext,
)
from django_components.extensions.defaults import Default
from django_components.extensions.view import ComponentView
from django_components.library import TagProtectedError
from django_components.node import BaseNode, template_tag
@ -88,6 +89,7 @@ __all__ = [
"component_formatter",
"component_shorthand_formatter",
"ContextBehavior",
"Default",
"DynamicComponent",
"EmptyTuple",
"EmptyDict",

View file

@ -750,9 +750,10 @@ class InternalSettings:
)
# Prepend built-in extensions
from django_components.extensions.defaults import DefaultsExtension
from django_components.extensions.view import ViewExtension
extensions = [ViewExtension] + list(extensions)
extensions = [DefaultsExtension, ViewExtension] + list(extensions)
# Extensions may be passed in either as classes or import strings.
extension_instances: List["ComponentExtension"] = []

View file

@ -962,6 +962,8 @@ class Component(
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
or to the end of the `<body>` tag. CSS dependencies are inserted into
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
- `"fragment"` - `{% component_js_dependencies %}` and `{% component_css_dependencies %}`,
are ignored, and a script that loads the JS and CSS dependencies is appended to the HTML.
- `request` - The request object. This is only required when needing to use RequestContext,
e.g. to enable template `context_processors`.
@ -1030,6 +1032,8 @@ class Component(
- `"document"` (default) - JS dependencies are inserted into `{% component_js_dependencies %}`,
or to the end of the `<body>` tag. CSS dependencies are inserted into
`{% component_css_dependencies %}`, or the end of the `<head>` tag.
- `"fragment"` - `{% component_js_dependencies %}` and `{% component_css_dependencies %}`,
are ignored, and a script that loads the JS and CSS dependencies is appended to the HTML.
- `render_dependencies` - Set this to `False` if you want to insert the resulting HTML into another component.
- `request` - The request object. This is only required when needing to use RequestContext,
e.g. to enable template `context_processors`.
@ -1107,9 +1111,14 @@ class Component(
request = parent_comp_ctx.request
# Allow to provide no args/kwargs/slots/context
args = cast(ArgsType, args or ())
kwargs = cast(KwargsType, kwargs or {})
slots_untyped = self._normalize_slot_fills(slots or {}, escape_slots_content)
# NOTE: We make copies of args / kwargs / slots, so that plugins can modify them
# without affecting the original values.
args = cast(ArgsType, list(args) if args is not None else ())
kwargs = cast(KwargsType, dict(kwargs) if kwargs is not None else {})
slots_untyped = self._normalize_slot_fills(
dict(slots) if slots is not None else {},
escape_slots_content,
)
slots = cast(SlotsType, slots_untyped)
# Use RequestContext if request is provided, so that child non-component template tags
# can access the request object too.

View file

@ -0,0 +1,157 @@
import sys
from dataclasses import MISSING, Field, dataclass
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Type
from weakref import WeakKeyDictionary
from django_components.extension import ComponentExtension, OnComponentClassCreatedContext, OnComponentInputContext
if TYPE_CHECKING:
from django_components.component import Component
# NOTE: `WeakKeyDictionary` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
ComponentDefaultsCache = WeakKeyDictionary[Type["Component"], List["ComponentDefaultField"]]
else:
ComponentDefaultsCache = WeakKeyDictionary
defaults_by_component: ComponentDefaultsCache = WeakKeyDictionary()
@dataclass
class Default:
"""
Use this class to mark a field on the `Component.Defaults` class as a factory.
Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
**Example:**
```py
from django_components import Default
class MyComponent(Component):
class Defaults:
# Plain value doesn't need a factory
position = "left"
# Lists and dicts need to be wrapped in `Default`
# Otherwise all instances will share the same value
selected_items = Default(lambda: [1, 2, 3])
```
"""
value: Callable[[], Any]
class ComponentDefaultField(NamedTuple):
"""Internal representation of a field on the `Defaults` class."""
key: str
value: Any
is_factory: bool
# Figure out which defaults are factories and which are not, at class creation,
# so that the actual creation of the defaults dictionary is simple.
def _extract_defaults(defaults: Optional[Type]) -> List[ComponentDefaultField]:
defaults_fields: List[ComponentDefaultField] = []
if defaults is None:
return defaults_fields
for default_field_key in dir(defaults):
# Iterate only over fields set by the user (so non-dunder fields).
# Plus ignore `component_class` because that was set by the extension system.
if default_field_key.startswith("__") or default_field_key == "component_class":
continue
default_field = getattr(defaults, default_field_key)
# If the field was defined with dataclass.field(), take the default / factory from there.
if isinstance(default_field, Field):
if default_field.default is not MISSING:
field_value = default_field.default
is_factory = False
elif default_field.default_factory is not MISSING:
field_value = default_field.default_factory
is_factory = True
else:
field_value = None
is_factory = False
# If the field was defined with our `Default` class, it defined a factory
elif isinstance(default_field, Default):
field_value = default_field.value
is_factory = True
# If the field was defined with a simple assignment, assume it's NOT a factory.
else:
field_value = default_field
is_factory = False
field_data = ComponentDefaultField(
key=default_field_key,
value=field_value,
is_factory=is_factory,
)
defaults_fields.append(field_data)
return defaults_fields
def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None:
"""
Apply the defaults from `Component.Defaults` to the given `kwargs`.
Defaults are applied only to missing or `None` values.
"""
for default_field in defaults:
# Defaults are applied only to missing or `None` values
given_value = kwargs.get(default_field.key, None)
if given_value is not None:
continue
if default_field.is_factory:
default_value = default_field.value()
else:
default_value = default_field.value
kwargs[default_field.key] = default_value
class DefaultsExtension(ComponentExtension):
"""
This extension adds a nested `Defaults` class to each `Component`.
This nested `Defaults` class is used to set default values for the component's kwargs.
**Example:**
```py
from django_components import Component, Default
class MyComponent(Component):
class Defaults:
position = "left"
# Factory values need to be wrapped in `Default`
selected_items = Default(lambda: [1, 2, 3])
```
This extension is automatically added to all components.
"""
name = "defaults"
# Preprocess the `Component.Defaults` class, if given, so we don't have to do it
# each time a component is rendered.
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
defaults_cls = getattr(ctx.component_cls, "Defaults", None)
defaults_by_component[ctx.component_cls] = _extract_defaults(defaults_cls)
# Apply defaults to missing or `None` values in `kwargs`
def on_component_input(self, ctx: OnComponentInputContext) -> None:
defaults = defaults_by_component.get(ctx.component_cls, None)
if defaults is None:
return
_apply_defaults(ctx.kwargs, defaults)

View file

@ -447,7 +447,6 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
return False
def taken_n(n: int) -> str:
nonlocal index
result = text[index : index + n] # noqa: E203
add_token(result)
return result
@ -457,9 +456,6 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
tokens: Union[List[str], Tuple[str, ...]],
ignore: Optional[Sequence[str]] = None,
) -> str:
nonlocal index
nonlocal text
result = ""
while not is_at_end():
char = text[index]
@ -483,9 +479,6 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
# tag_name = take_while([" ", "\t", "\n", "\r", "\f"])
def take_while(tokens: Union[List[str], Tuple[str, ...]]) -> str:
nonlocal index
nonlocal text
result = ""
while not is_at_end():
char = text[index]

View file

@ -97,7 +97,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list")
output = out.getvalue()
assert output.strip() == "name\n====\nview"
assert output.strip() == "name \n========\ndefaults\nview"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
@ -108,7 +108,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list")
output = out.getvalue()
assert output.strip() == "name \n=====\nview \nempty\ndummy"
assert output.strip() == "name \n========\ndefaults\nview \nempty \ndummy"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
@ -119,7 +119,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list", "--all")
output = out.getvalue()
assert output.strip() == "name \n=====\nview \nempty\ndummy"
assert output.strip() == "name \n========\ndefaults\nview \nempty \ndummy"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
@ -130,7 +130,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list", "--columns", "name")
output = out.getvalue()
assert output.strip() == "name \n=====\nview \nempty\ndummy"
assert output.strip() == "name \n========\ndefaults\nview \nempty \ndummy"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
@ -141,7 +141,7 @@ class TestExtensionsListCommand:
call_command("components", "ext", "list", "--simple")
output = out.getvalue()
assert output.strip() == "view \nempty\ndummy"
assert output.strip() == "defaults\nview \nempty \ndummy"
@djc_test
@ -159,18 +159,19 @@ class TestExtensionsRunCommand:
output
== dedent(
f"""
usage: components ext run [-h] {{view,empty,dummy}} ...
usage: components ext run [-h] {{defaults,view,empty,dummy}} ...
Run a command added by an extension.
{OPTIONS_TITLE}:
-h, --help show this help message and exit
-h, --help show this help message and exit
subcommands:
{{view,empty,dummy}}
view Run commands added by the 'view' extension.
empty Run commands added by the 'empty' extension.
dummy Run commands added by the 'dummy' extension.
{{defaults,view,empty,dummy}}
defaults Run commands added by the 'defaults' extension.
view Run commands added by the 'view' extension.
empty Run commands added by the 'empty' extension.
dummy Run commands added by the 'dummy' extension.
"""
).lstrip()
)

View file

@ -293,7 +293,7 @@ class TestComponent:
class TestComponent(Component):
@no_type_check
def get_context_data(self, var1, var2, variable, another, **attrs):
assert self.input.args == (123, "str")
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
assert isinstance(self.input.context, Context)
assert list(self.input.slots.keys()) == ["my_slot"]
@ -305,7 +305,7 @@ class TestComponent:
@no_type_check
def get_template(self, context):
assert self.input.args == (123, "str")
assert self.input.args == [123, "str"]
assert self.input.kwargs == {"variable": "test", "another": 1}
assert isinstance(self.input.context, Context)
assert list(self.input.slots.keys()) == ["my_slot"]

View file

@ -0,0 +1,147 @@
from dataclasses import field
from typing import Any
from django.template import Context
from django_components import Component, Default
from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config({"autodiscover": False})
@djc_test
class TestComponentDefaults:
def test_input_defaults(self):
did_call_context = False
class TestComponent(Component):
template = ""
class Defaults:
variable = "test"
another = 1
extra = "extra"
fn = lambda: "fn_as_val" # noqa: E731
def get_context_data(self, arg1: Any, variable: Any, another: Any, **attrs: Any):
nonlocal did_call_context
did_call_context = True
# Check that args and slots are NOT affected by the defaults
assert self.input.args == [123]
assert [*self.input.slots.keys()] == ["my_slot"]
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
assert self.input.kwargs == {
"variable": "test", # User-given
"another": 1, # Default because missing
"extra": "extra", # Default because `None` was given
"fn": self.Defaults.fn, # Default because missing
}
assert isinstance(self.input.context, Context)
return {
"variable": variable,
}
TestComponent.render(
args=(123,),
kwargs={"variable": "test", "extra": None},
slots={"my_slot": "MY_SLOT"},
)
assert did_call_context
def test_factory_from_class(self):
did_call_context = False
class TestComponent(Component):
template = ""
class Defaults:
variable = "test"
fn = Default(lambda: "fn_as_factory")
def get_context_data(self, variable: Any, **attrs: Any):
nonlocal did_call_context
did_call_context = True
assert self.input.kwargs == {
"variable": "test", # User-given
"fn": "fn_as_factory", # Default because missing
}
assert isinstance(self.input.context, Context)
return {
"variable": variable,
}
TestComponent.render(
kwargs={"variable": "test"},
)
assert did_call_context
def test_factory_from_dataclass_field_value(self):
did_call_context = False
class TestComponent(Component):
template = ""
class Defaults:
variable = "test"
fn = field(default=lambda: "fn_as_factory")
def get_context_data(self, variable: Any, **attrs: Any):
nonlocal did_call_context
did_call_context = True
assert self.input.kwargs == {
"variable": "test", # User-given
# NOTE: NOT a factory, because it was set as `field(default=...)`
"fn": self.Defaults.fn.default, # type: ignore[attr-defined]
}
assert isinstance(self.input.context, Context)
return {
"variable": variable,
}
TestComponent.render(
kwargs={"variable": "test"},
)
assert did_call_context
def test_factory_from_dataclass_field_factory(self):
did_call_context = False
class TestComponent(Component):
template = ""
class Defaults:
variable = "test"
fn = field(default_factory=lambda: "fn_as_factory")
def get_context_data(self, variable: Any, **attrs: Any):
nonlocal did_call_context
did_call_context = True
assert self.input.kwargs == {
"variable": "test", # User-given
# NOTE: IS a factory, because it was set as `field(default_factory=...)`
"fn": "fn_as_factory", # Default because missing
}
assert isinstance(self.input.context, Context)
return {
"variable": variable,
}
TestComponent.render(
kwargs={"variable": "test"},
)
assert did_call_context

View file

@ -20,6 +20,7 @@ from django_components.extension import (
OnComponentInputContext,
OnComponentDataContext,
)
from django_components.extensions.defaults import DefaultsExtension
from django_components.extensions.view import ViewExtension
from django_components.testing import djc_test
@ -125,9 +126,10 @@ def with_registry(on_created: Callable):
class TestExtension:
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_extensions_setting(self):
assert len(app_settings.EXTENSIONS) == 2
assert isinstance(app_settings.EXTENSIONS[0], ViewExtension)
assert isinstance(app_settings.EXTENSIONS[1], DummyExtension)
assert len(app_settings.EXTENSIONS) == 3
assert isinstance(app_settings.EXTENSIONS[0], DefaultsExtension)
assert isinstance(app_settings.EXTENSIONS[1], ViewExtension)
assert isinstance(app_settings.EXTENSIONS[2], DummyExtension)
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_access_component_from_extension(self):
@ -150,7 +152,7 @@ class TestExtension:
class TestExtensionHooks:
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_component_class_lifecycle_hooks(self):
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
extension = cast(DummyExtension, app_settings.EXTENSIONS[2])
assert len(extension.calls["on_component_class_created"]) == 0
assert len(extension.calls["on_component_class_deleted"]) == 0
@ -182,7 +184,7 @@ class TestExtensionHooks:
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_registry_lifecycle_hooks(self):
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
extension = cast(DummyExtension, app_settings.EXTENSIONS[2])
assert len(extension.calls["on_registry_created"]) == 0
assert len(extension.calls["on_registry_deleted"]) == 0
@ -219,7 +221,7 @@ class TestExtensionHooks:
return {"name": name}
registry.register("test_comp", TestComponent)
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
extension = cast(DummyExtension, app_settings.EXTENSIONS[2])
# Verify on_component_registered was called
assert len(extension.calls["on_component_registered"]) == 1
@ -257,14 +259,14 @@ class TestExtensionHooks:
test_slots = {"content": "Some content"}
TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots)
extension = cast(DummyExtension, app_settings.EXTENSIONS[1])
extension = cast(DummyExtension, app_settings.EXTENSIONS[2])
# Verify on_component_input was called with correct args
assert len(extension.calls["on_component_input"]) == 1
input_call: OnComponentInputContext = extension.calls["on_component_input"][0]
assert input_call.component_cls == TestComponent
assert isinstance(input_call.component_id, str)
assert input_call.args == ("arg1", "arg2")
assert input_call.args == ["arg1", "arg2"]
assert input_call.kwargs == {"name": "Test"}
assert len(input_call.slots) == 1
assert isinstance(input_call.slots["content"], Slot)