mirror of
https://github.com/django-components/django-components.git
synced 2025-08-31 11:17:21 +00:00
feat: slot caching (#1196)
* feat: slot caching Closes #1164 * refactor: fix linter
This commit is contained in:
parent
b6b574d875
commit
79c42da2f9
4 changed files with 306 additions and 12 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -486,6 +486,42 @@
|
||||||
- If `Slot` was created from string via `Slot("...")`, `Slot.contents` will contain that string.
|
- 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 a function, `Slot.contents` will contain that function.
|
||||||
|
|
||||||
|
- Component caching can now take slots into account, by setting `Component.Cache.include_slots` to `True`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Cache:
|
||||||
|
enabled = True
|
||||||
|
include_slots = True
|
||||||
|
```
|
||||||
|
|
||||||
|
In which case the following two calls will generate separate cache entries:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% component "my_component" position="left" %}
|
||||||
|
Hello, Alice
|
||||||
|
{% endcomponent %}
|
||||||
|
|
||||||
|
{% component "my_component" position="left" %}
|
||||||
|
Hello, Bob
|
||||||
|
{% endcomponent %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Same applies to `Component.render()` with string slots:
|
||||||
|
|
||||||
|
```py
|
||||||
|
MyComponent.render(
|
||||||
|
kwargs={"position": "left"},
|
||||||
|
slots={"content": "Hello, Alice"}
|
||||||
|
)
|
||||||
|
MyComponent.render(
|
||||||
|
kwargs={"position": "left"},
|
||||||
|
slots={"content": "Hello, Bob"}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Read more on [Component caching](https://django-components.github.io/django-components/0.140/concepts/advanced/component_caching/).
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
||||||
- Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)).
|
- Fix bug: Context processors data was being generated anew for each component. Now the data is correctly created once and reused across components with the same request ([#1165](https://github.com/django-components/django-components/issues/1165)).
|
||||||
|
|
|
@ -5,7 +5,8 @@ This is particularly useful for components that are expensive to render or do no
|
||||||
|
|
||||||
!!! info
|
!!! info
|
||||||
|
|
||||||
Component caching uses Django's cache framework, so you can use any cache backend that is supported by Django.
|
Component caching uses [Django's cache framework](https://docs.djangoproject.com/en/5.2/topics/cache/),
|
||||||
|
so you can use any cache backend that is supported by Django.
|
||||||
|
|
||||||
### Enabling caching
|
### Enabling caching
|
||||||
|
|
||||||
|
@ -83,6 +84,81 @@ For even more control, you can override other methods available on the [`Compone
|
||||||
The default implementation of `Cache.hash()` simply serializes the input into a string.
|
The default implementation of `Cache.hash()` simply serializes the input into a string.
|
||||||
As such, it might not be suitable if you need to hash complex objects like Models.
|
As such, it might not be suitable if you need to hash complex objects like Models.
|
||||||
|
|
||||||
|
### Caching slots
|
||||||
|
|
||||||
|
By default, the cache key is generated based ONLY on the args and kwargs.
|
||||||
|
|
||||||
|
To cache the component based on the slots, set [`Component.Cache.include_slots`](../../reference/api.md#django_components.ComponentCache.include_slots) to `True`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class MyComponent(Component):
|
||||||
|
class Cache:
|
||||||
|
enabled = True
|
||||||
|
include_slots = True
|
||||||
|
```
|
||||||
|
|
||||||
|
with `include_slots = True`, the cache key will be generated also based on the given slots.
|
||||||
|
|
||||||
|
As such, the following two calls would generate separate entries in the cache:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% component "my_component" position="left" %}
|
||||||
|
Hello, Alice
|
||||||
|
{% endcomponent %}
|
||||||
|
|
||||||
|
{% component "my_component" position="left" %}
|
||||||
|
Hello, Bob
|
||||||
|
{% endcomponent %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Same when using [`Component.render()`](../../reference/api.md#django_components.Component.render) with string slots:
|
||||||
|
|
||||||
|
```py
|
||||||
|
MyComponent.render(
|
||||||
|
kwargs={"position": "left"},
|
||||||
|
slots={"content": "Hello, Alice"}
|
||||||
|
)
|
||||||
|
MyComponent.render(
|
||||||
|
kwargs={"position": "left"},
|
||||||
|
slots={"content": "Hello, Bob"}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Passing slots as functions to cached components with `include_slots=True` will raise an error.
|
||||||
|
|
||||||
|
```py
|
||||||
|
MyComponent.render(
|
||||||
|
kwargs={"position": "left"},
|
||||||
|
slots={"content": lambda *a, **kwa: "Hello, Alice"}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Slot caching DOES NOT account for context variables within
|
||||||
|
the [`{% fill %}`](../../reference/template_tags.md#fill) tag.
|
||||||
|
|
||||||
|
For example, the following two cases will be treated as the same entry:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% with my_var="foo" %}
|
||||||
|
{% component "mycomponent" name="foo" %}
|
||||||
|
{{ my_var }}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with my_var="bar" %}
|
||||||
|
{% component "mycomponent" name="bar" %}
|
||||||
|
{{ my_var }}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Currently it's impossible to capture used variables. This will be addressed in v2.
|
||||||
|
Read more about it in [django-components/#1164](https://github.com/django-components/django-components/issues/1164).
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
Here's a complete example of a component with caching enabled:
|
Here's a complete example of a component with caching enabled:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from hashlib import md5
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from django.core.cache import BaseCache, caches
|
from django.core.cache import BaseCache, caches
|
||||||
|
@ -7,6 +8,7 @@ from django_components.extension import (
|
||||||
OnComponentInputContext,
|
OnComponentInputContext,
|
||||||
OnComponentRenderedContext,
|
OnComponentRenderedContext,
|
||||||
)
|
)
|
||||||
|
from django_components.slots import Slot
|
||||||
|
|
||||||
# NOTE: We allow users to override cache key generation, but then we internally
|
# NOTE: We allow users to override cache key generation, but then we internally
|
||||||
# still prefix their key with our own prefix, so it's clear where it comes from.
|
# still prefix their key with our own prefix, so it's clear where it comes from.
|
||||||
|
@ -38,6 +40,49 @@ class ComponentCache(ComponentExtension.ExtensionClass): # type: ignore
|
||||||
"""
|
"""
|
||||||
Whether this Component should be cached. Defaults to `False`.
|
Whether this Component should be cached. Defaults to `False`.
|
||||||
"""
|
"""
|
||||||
|
include_slots: bool = False
|
||||||
|
"""
|
||||||
|
Whether the slots should be hashed into the cache key.
|
||||||
|
|
||||||
|
If enabled, the following two cases will be treated as different entries:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% component "mycomponent" name="foo" %}
|
||||||
|
FILL ONE
|
||||||
|
{% endcomponent %}
|
||||||
|
|
||||||
|
{% component "mycomponent" name="foo" %}
|
||||||
|
FILL TWO
|
||||||
|
{% endcomponent %}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Passing slots as functions to cached components with `include_slots=True` will raise an error.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Slot caching DOES NOT account for context variables within the `{% fill %}` tag.
|
||||||
|
|
||||||
|
For example, the following two cases will be treated as the same entry:
|
||||||
|
|
||||||
|
```django
|
||||||
|
{% with my_var="foo" %}
|
||||||
|
{% component "mycomponent" name="foo" %}
|
||||||
|
{{ my_var }}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
{% with my_var="bar" %}
|
||||||
|
{% component "mycomponent" name="bar" %}
|
||||||
|
{{ my_var }}
|
||||||
|
{% endcomponent %}
|
||||||
|
{% endwith %}
|
||||||
|
```
|
||||||
|
|
||||||
|
Currently it's impossible to capture used variables. This will be addressed in v2.
|
||||||
|
Read more about it in https://github.com/django-components/django-components/issues/1164.
|
||||||
|
"""
|
||||||
|
|
||||||
ttl: Optional[int] = None
|
ttl: Optional[int] = None
|
||||||
"""
|
"""
|
||||||
|
@ -71,7 +116,10 @@ class ComponentCache(ComponentExtension.ExtensionClass): # type: ignore
|
||||||
# Allow user to override how the input is hashed into a cache key with `hash()`,
|
# Allow user to override how the input is hashed into a cache key with `hash()`,
|
||||||
# but then still prefix it wih our own prefix, so it's clear where it comes from.
|
# but then still prefix it wih our own prefix, so it's clear where it comes from.
|
||||||
cache_key = self.hash(args, kwargs)
|
cache_key = self.hash(args, kwargs)
|
||||||
cache_key = CACHE_KEY_PREFIX + self.component._class_hash + ":" + cache_key
|
if self.include_slots:
|
||||||
|
cache_key += ":" + self.hash_slots(slots)
|
||||||
|
cache_key = self.component._class_hash + ":" + cache_key
|
||||||
|
cache_key = CACHE_KEY_PREFIX + md5(cache_key.encode()).hexdigest()
|
||||||
return cache_key
|
return cache_key
|
||||||
|
|
||||||
def hash(self, args: List, kwargs: Dict) -> str:
|
def hash(self, args: List, kwargs: Dict) -> str:
|
||||||
|
@ -87,6 +135,19 @@ class ComponentCache(ComponentExtension.ExtensionClass): # type: ignore
|
||||||
kwargs_hash = ",".join(f"{k}-{v}" for k, v in sorted_items)
|
kwargs_hash = ",".join(f"{k}-{v}" for k, v in sorted_items)
|
||||||
return f"{args_hash}:{kwargs_hash}"
|
return f"{args_hash}:{kwargs_hash}"
|
||||||
|
|
||||||
|
def hash_slots(self, slots: Dict[str, Slot]) -> str:
|
||||||
|
sorted_items = sorted(slots.items())
|
||||||
|
hash_parts = []
|
||||||
|
for key, slot in sorted_items:
|
||||||
|
if callable(slot.contents):
|
||||||
|
raise ValueError(
|
||||||
|
f"Cannot hash slot '{key}' of component '{self.component.name}' - Slot functions are unhashable."
|
||||||
|
" Instead define the slot as a string or `{% fill %}` tag, or disable slot caching"
|
||||||
|
" with `Cache.include_slots=False`."
|
||||||
|
)
|
||||||
|
hash_parts.append(f"{key}-{slot.contents}")
|
||||||
|
return ",".join(hash_parts)
|
||||||
|
|
||||||
|
|
||||||
class CacheExtension(ComponentExtension):
|
class CacheExtension(ComponentExtension):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from django.core.cache import caches
|
from django.core.cache import caches
|
||||||
from django.template import Template
|
from django.template import Template
|
||||||
from django.template.context import Context
|
from django.template.context import Context
|
||||||
|
import pytest
|
||||||
|
|
||||||
from django_components import Component, register
|
from django_components import Component, register
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
|
@ -45,7 +47,7 @@ class TestComponentCache:
|
||||||
|
|
||||||
# Check if the cache entry is set
|
# Check if the cache entry is set
|
||||||
cache_key = component.cache.get_cache_key([], {}, {})
|
cache_key = component.cache.get_cache_key([], {}, {})
|
||||||
assert cache_key == "components:cache:TestComponent_c9770f::"
|
assert cache_key == "components:cache:c98bf483e9a1937732d4542c714462ac"
|
||||||
assert component.cache.get_entry(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello"
|
assert component.cache.get_entry(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello"
|
||||||
assert caches["default"].get(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello"
|
assert caches["default"].get(cache_key) == "<!-- _RENDERED TestComponent_c9770f,ca1bc3e,, -->Hello"
|
||||||
|
|
||||||
|
@ -137,9 +139,9 @@ class TestComponentCache:
|
||||||
|
|
||||||
assert component.cache.get_cache() is caches["custom"]
|
assert component.cache.get_cache() is caches["custom"]
|
||||||
assert (
|
assert (
|
||||||
component.cache.get_entry("components:cache:TestComponent_90ef7a::")
|
component.cache.get_entry("components:cache:bcb4b049d8556e06871b39e0e584e452")
|
||||||
== "<!-- _RENDERED TestComponent_90ef7a,ca1bc3e,, -->Hello"
|
== "<!-- _RENDERED TestComponent_90ef7a,ca1bc3e,, -->Hello"
|
||||||
) # noqa: E501
|
)
|
||||||
|
|
||||||
def test_cache_by_input(self):
|
def test_cache_by_input(self):
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
|
@ -162,15 +164,16 @@ class TestComponentCache:
|
||||||
|
|
||||||
# Check if the cache entry is set
|
# Check if the cache entry is set
|
||||||
cache = caches["default"]
|
cache = caches["default"]
|
||||||
|
|
||||||
assert len(cache._cache) == 2
|
assert len(cache._cache) == 2
|
||||||
assert (
|
assert (
|
||||||
component.cache.get_entry("components:cache:TestComponent_648b95::input-world")
|
component.cache.get_entry("components:cache:3535e1d1e5f6fa5bc521e7fe203a68d0")
|
||||||
== "<!-- _RENDERED TestComponent_648b95,ca1bc3e,, -->Hello world"
|
== "<!-- _RENDERED TestComponent_648b95,ca1bc3e,, -->Hello world"
|
||||||
) # noqa: E501
|
)
|
||||||
assert (
|
assert (
|
||||||
component.cache.get_entry("components:cache:TestComponent_648b95::input-cake")
|
component.cache.get_entry("components:cache:a98a8bd5e72a544d7601798d5e777a77")
|
||||||
== "<!-- _RENDERED TestComponent_648b95,ca1bc3f,, -->Hello cake"
|
== "<!-- _RENDERED TestComponent_648b95,ca1bc3f,, -->Hello cake"
|
||||||
) # noqa: E501
|
)
|
||||||
|
|
||||||
def test_cache_input_hashing(self):
|
def test_cache_input_hashing(self):
|
||||||
class TestComponent(Component):
|
class TestComponent(Component):
|
||||||
|
@ -201,8 +204,9 @@ class TestComponentCache:
|
||||||
component.render(args=(1, 2), kwargs={"key": "value"})
|
component.render(args=(1, 2), kwargs={"key": "value"})
|
||||||
|
|
||||||
# The key should use the custom hash methods
|
# The key should use the custom hash methods
|
||||||
expected_key = "components:cache:TestComponent_28880f:custom-args-and-kwargs"
|
expected_key = "components:cache:3d54974c467a578c509efec189b0d14b"
|
||||||
assert component.cache.get_cache_key([1, 2], {"key": "value"}, {}) == expected_key
|
assert component.cache.get_cache_key([1, 2], {"key": "value"}, {}) == expected_key
|
||||||
|
assert component.cache.get_entry(expected_key) == "<!-- _RENDERED TestComponent_28880f,ca1bc3e,, -->Hello"
|
||||||
|
|
||||||
def test_cached_component_inside_include(self):
|
def test_cached_component_inside_include(self):
|
||||||
|
|
||||||
|
@ -213,12 +217,14 @@ class TestComponentCache:
|
||||||
class Cache:
|
class Cache:
|
||||||
enabled = True
|
enabled = True
|
||||||
|
|
||||||
template = Template("""
|
template = Template(
|
||||||
|
"""
|
||||||
{% extends "test_cached_component_inside_include_base.html" %}
|
{% extends "test_cached_component_inside_include_base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
THIS_IS_IN_ACTUAL_TEMPLATE_SO_SHOULD_NOT_BE_OVERRIDDEN
|
THIS_IS_IN_ACTUAL_TEMPLATE_SO_SHOULD_NOT_BE_OVERRIDDEN
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
result = template.render(Context({}))
|
result = template.render(Context({}))
|
||||||
assert "THIS_IS_IN_BASE_TEMPLATE_SO_SHOULD_BE_OVERRIDDEN" not in result
|
assert "THIS_IS_IN_BASE_TEMPLATE_SO_SHOULD_BE_OVERRIDDEN" not in result
|
||||||
|
@ -227,3 +233,118 @@ class TestComponentCache:
|
||||||
result_cached = template.render(Context({}))
|
result_cached = template.render(Context({}))
|
||||||
assert "THIS_IS_IN_BASE_TEMPLATE_SO_SHOULD_BE_OVERRIDDEN" not in result_cached
|
assert "THIS_IS_IN_BASE_TEMPLATE_SO_SHOULD_BE_OVERRIDDEN" not in result_cached
|
||||||
assert "THIS_IS_IN_ACTUAL_TEMPLATE_SO_SHOULD_NOT_BE_OVERRIDDEN" in result_cached
|
assert "THIS_IS_IN_ACTUAL_TEMPLATE_SO_SHOULD_NOT_BE_OVERRIDDEN" in result_cached
|
||||||
|
|
||||||
|
def test_cache_slots__fills(self):
|
||||||
|
@register("test_component")
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "Hello {{ input }} <div>{% slot 'content' default / %}</div>"
|
||||||
|
|
||||||
|
class Cache:
|
||||||
|
enabled = True
|
||||||
|
include_slots = True
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {"input": kwargs["input"]}
|
||||||
|
|
||||||
|
Template(
|
||||||
|
"""
|
||||||
|
{% component "test_component" input="cake" %}
|
||||||
|
ONE
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
).render(Context({}))
|
||||||
|
|
||||||
|
Template(
|
||||||
|
"""
|
||||||
|
{% component "test_component" input="cake" %}
|
||||||
|
ONE
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
).render(Context({}))
|
||||||
|
|
||||||
|
# Check if the cache entry is set
|
||||||
|
component = TestComponent()
|
||||||
|
cache = caches["default"]
|
||||||
|
|
||||||
|
assert len(cache._cache) == 1
|
||||||
|
assert (
|
||||||
|
component.cache.get_entry("components:cache:87b9e27abdd3c6ef70982d065fc836a9")
|
||||||
|
== '<!-- _RENDERED TestComponent_dd1dee,ca1bc3f,, -->Hello cake <div data-djc-id-ca1bc3f="">\n ONE\n </div>' # noqa: E501
|
||||||
|
)
|
||||||
|
|
||||||
|
Template(
|
||||||
|
"""
|
||||||
|
{% component "test_component" input="cake" %}
|
||||||
|
TWO
|
||||||
|
{% endcomponent %}
|
||||||
|
"""
|
||||||
|
).render(Context({}))
|
||||||
|
|
||||||
|
assert len(cache._cache) == 2
|
||||||
|
assert (
|
||||||
|
component.cache.get_entry("components:cache:1d7e3a58972550cf9bec18f457fb1a61")
|
||||||
|
== '<!-- _RENDERED TestComponent_dd1dee,ca1bc44,, -->Hello cake <div data-djc-id-ca1bc44="">\n TWO\n </div>' # noqa: E501
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cache_slots__strings(self):
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "Hello {{ input }} <div>{% slot 'content' default / %}</div>"
|
||||||
|
|
||||||
|
class Cache:
|
||||||
|
enabled = True
|
||||||
|
include_slots = True
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {"input": kwargs["input"]}
|
||||||
|
|
||||||
|
TestComponent.render(
|
||||||
|
kwargs={"input": "cake"},
|
||||||
|
slots={"content": "ONE"},
|
||||||
|
)
|
||||||
|
TestComponent.render(
|
||||||
|
kwargs={"input": "cake"},
|
||||||
|
slots={"content": "ONE"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the cache entry is set
|
||||||
|
component = TestComponent()
|
||||||
|
cache = caches["default"]
|
||||||
|
|
||||||
|
assert len(cache._cache) == 1
|
||||||
|
assert (
|
||||||
|
component.cache.get_entry("components:cache:362766726cd0e991f33b0527ef8a513c")
|
||||||
|
== '<!-- _RENDERED TestComponent_34b6d1,ca1bc3e,, -->Hello cake <div data-djc-id-ca1bc3e="">ONE</div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
TestComponent.render(
|
||||||
|
kwargs={"input": "cake"},
|
||||||
|
slots={"content": "TWO"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(cache._cache) == 2
|
||||||
|
assert (
|
||||||
|
component.cache.get_entry("components:cache:468e3f122ac305cff5d9096a3c548faf")
|
||||||
|
== '<!-- _RENDERED TestComponent_34b6d1,ca1bc41,, -->Hello cake <div data-djc-id-ca1bc41="">TWO</div>'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_cache_slots_raises_on_func(self):
|
||||||
|
class TestComponent(Component):
|
||||||
|
template = "Hello {{ input }} <div>{% slot 'content' default / %}</div>"
|
||||||
|
|
||||||
|
class Cache:
|
||||||
|
enabled = True
|
||||||
|
include_slots = True
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return {"input": kwargs["input"]}
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
ValueError,
|
||||||
|
match=re.escape(
|
||||||
|
"Cannot hash slot 'content' of component 'TestComponent' - Slot functions are unhashable."
|
||||||
|
),
|
||||||
|
):
|
||||||
|
TestComponent.render(
|
||||||
|
kwargs={"input": "cake"},
|
||||||
|
slots={"content": lambda *a, **kwa: "ONE"},
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue