feat: allow to set comp defaults on Kwargs class (#1467)
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run

This commit is contained in:
Juro Oravec 2025-10-21 21:08:55 +02:00 committed by GitHub
parent c37628dea0
commit 28ff1d072a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 561 additions and 119 deletions

View file

@ -1,10 +1,64 @@
# Release notes
## v0.142.4
## v0.143.0
#### Refactor
#### Feat
- Simpler syntax for defining component inputs.
- You can now define component input defaults directly on `Component.Kwargs`.
Before, the defaults had to be defined on a separate `Component.Defaults` class:
```python
class ProfileCard(Component):
class Kwargs:
user_id: int
show_details: bool
class Defaults:
show_details = True
```
Now, django-components can detect the defaults from `Component.Kwargs` and apply
them. So you can merge `Component.Kwargs` with `Component.Defaults`:
```python
class ProfileCard(Component):
class Kwargs:
user_id: int
show_details: bool = True
```
NOTE: This applies only when `Component.Kwargs` is a NamedTuple or dataclass.
- New helper `get_component_defaults()`:
Now, the defaults may be defined on either `Component.Defaults` and `Component.Kwargs` classes.
To get a final, merged dictionary of all the component's defaults, use `get_component_defaults()`:
```py
from django_components import Component, Default, get_component_defaults
class MyTable(Component):
class Kwargs:
position: str
order: int
items: list[int]
variable: str = "from_kwargs"
class Defaults:
position: str = "left"
items = Default(lambda: [1, 2, 3])
defaults = get_component_defaults(MyTable)
# {
# "position": "left",
# "items": [1, 2, 3],
# "variable": "from_kwargs",
# }
```
- Simpler syntax for defining component inputs:
When defining `Args`, `Kwargs`, `Slots`, `JsData`, `CssData`, `TemplateData`, these data classes now don't have to subclass any other class.
@ -51,6 +105,8 @@
...
```
#### Refactor
- Extension authors: The `ExtensionComponentConfig` can be instantiated with `None` instead of a component instance.
This allows to call component-level extension methods outside of the normal rendering lifecycle.

View file

@ -65,25 +65,33 @@ and so `selected_items` will be set to `[1, 2, 3]`.
The defaults are aplied only to keyword arguments. They are NOT applied to positional arguments!
### Defaults from `Kwargs`
If you are using [`Component.Kwargs`](../fundamentals/typing_and_validation.md#typing-inputs) to specify the component input,
you can set the defaults directly on `Kwargs`:
```python
class ProfileCard(Component):
class Kwargs:
user_id: int
show_details: bool = True
```
Which is the same as:
```python
class ProfileCard(Component):
class Kwargs:
user_id: int
show_details: bool
class Defaults:
show_details = True
```
!!! warning
When [typing](../fundamentals/typing_and_validation.md) your components with [`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
you may be inclined to define the defaults in the classes.
```py
class ProfileCard(Component):
class Kwargs:
show_details: bool = True
```
This is **NOT recommended**, because:
- The defaults will NOT be applied to inputs when using [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs) property.
- The defaults will NOT be applied when a field is given but set to `None`.
Instead, define the defaults in the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class.
This works only when `Component.Kwargs` is a plain class, NamedTuple or dataclass.
### Default factories
@ -124,30 +132,28 @@ class MyTable(Component):
### Accessing defaults
Since the defaults are defined on the component class, you can access the defaults for a component with the [`Component.Defaults`](../../../reference/api#django_components.Component.Defaults) property.
The defaults may be defined on both [`Component.Defaults`](../../../reference/api#django_components.Component.Defaults) and [`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs) classes.
So if we have a component like this:
To get a final, merged dictionary of all the component's defaults, use [`get_component_defaults()`](../../../reference/api#django_components.get_component_defaults):
```py
from django_components import Component, Default, register
from django_components import Component, Default, get_component_defaults
@register("my_table")
class MyTable(Component):
class Kwargs:
position: str
order: int
items: list[int]
variable: str = "from_kwargs"
class Defaults:
position = "left"
selected_items = Default(lambda: [1, 2, 3])
position: str = "left"
items = Default(lambda: [1, 2, 3])
def get_template_data(self, args, kwargs, slots, context):
return {
"position": kwargs["position"],
"selected_items": kwargs["selected_items"],
}
```
We can access individual defaults like this:
```py
print(MyTable.Defaults.position)
print(MyTable.Defaults.selected_items)
defaults = get_component_defaults(MyTable)
# {
# "position": "left",
# "items": [1, 2, 3],
# "variable": "from_kwargs",
# }
```

View file

@ -17,10 +17,7 @@ Each method handles the data independently - you can define different data for t
class ProfileCard(Component):
class Kwargs:
user_id: int
show_details: bool
class Defaults:
show_details = True
show_details: bool = True
def get_template_data(self, args, kwargs: Kwargs, slots, context):
user = User.objects.get(id=kwargs.user_id)
@ -304,7 +301,7 @@ class ProfileCard(Component):
## Default values
You can use [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class to provide default values for your inputs.
You can use the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) and [`Kwargs`](../../../reference/api/#django_components.Component.Kwargs) classes to provide default values for your inputs.
These defaults will be applied either when:
@ -321,12 +318,9 @@ from django_components import Component, Default, register
@register("profile_card")
class ProfileCard(Component):
class Kwargs:
show_details: bool
# Will be set to True if `None` or missing
show_details: bool = True
class Defaults:
show_details = True
# show_details will be set to True if `None` or missing
def get_template_data(self, args, kwargs: Kwargs, slots, context):
return {
"show_details": kwargs.show_details,
@ -335,26 +329,6 @@ class ProfileCard(Component):
...
```
!!! warning
When typing your components with [`Args`](../../../reference/api/#django_components.Component.Args),
[`Kwargs`](../../../reference/api/#django_components.Component.Kwargs),
or [`Slots`](../../../reference/api/#django_components.Component.Slots) classes,
you may be inclined to define the defaults in the classes.
```py
class ProfileCard(Component):
class Kwargs:
show_details: bool = True
```
This is **NOT recommended**, because:
- The defaults will NOT be applied to inputs when using [`self.raw_kwargs`](../../../reference/api/#django_components.Component.raw_kwargs) property.
- The defaults will NOT be applied when a field is given but set to `None`.
Instead, define the defaults in the [`Defaults`](../../../reference/api/#django_components.Component.Defaults) class.
## Accessing Render API
All three data methods have access to the Component's [Render API](../render_api), which includes:

View file

@ -233,7 +233,7 @@ or [`get_css_data()`](../../reference/api#django_components.Component.get_css_da
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
To define defaults for a component, you create a nested [`Defaults`](../../reference/api#django_components.Component.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.
@ -255,6 +255,57 @@ class Calendar(Component):
}
```
### 6. Add input validation
Right now our `Calendar` component accepts any number of args and kwargs,
and we can't see which ones are being used.
*This is a maintenance nightmare!*
Let's be good colleagues and document the component inputs.
As a bonus, we will also get runtime validation of these inputs.
For defining component inputs, there's 3 options:
- [`Args`](../../reference/api#django_components.Component.Args) - For defining positional args passed to the component
- [`Kwargs`](../../reference/api#django_components.Component.Kwargs) - For keyword args
- [`Slots`](../../reference/api#django_components.Component.Slots) - For slots
Our calendar component is using only kwargs, so we can ignore `Args` and `Slots`.
The new `Kwargs` class defines fields that this component accepts:
```py
from django_components import Component, Default, register
@register("calendar")
class Calendar(Component):
template_file = "calendar.html"
class Kwargs: # <--- changed (replaced Defaults)
date: Date
extra_class: str = "text-blue"
def get_template_data(self, args, kwargs: Kwargs, slots, context): # <--- changed
workweek_date = to_workweek_date(kwargs.date) # <--- changed
return {
"date": workweek_date,
"extra_class": kwargs.extra_class, # <--- changed
}
```
Notice that:
- When we defined `Kwargs` class, the `kwargs` parameter to `get_template_data`
changed to an instance of `Kwargs`. Fields are now accessed as attributes.
- Since `kwargs` is of class `Kwargs`, we've added annotation to the `kwargs` parameter.
- `Kwargs` replaced `Defaults`, because defaults can be defined also on `Kwargs` class.
And that's it! Now you can sleep safe knowing you won't break anything when
adding or removing component inputs.
Read more about [Component defaults](../concepts/fundamentals/component_defaults.md)
and [Typing and validation](../concepts/fundamentals/typing_and_validation.md).
---
Next, you will learn [how to use slots give your components even more flexibility ➡️](./adding_slots.md)

View file

@ -171,6 +171,10 @@
options:
show_if_no_docstring: true
::: django_components.get_component_defaults
options:
show_if_no_docstring: true
::: django_components.get_component_dirs
options:
show_if_no_docstring: true

View file

@ -52,7 +52,7 @@ from django_components.extension import (
OnTemplateLoadedContext,
)
from django_components.extensions.cache import ComponentCache
from django_components.extensions.defaults import ComponentDefaults, Default
from django_components.extensions.defaults import ComponentDefaults, Default, get_component_defaults
from django_components.extensions.debug_highlight import ComponentDebugHighlight
from django_components.extensions.view import ComponentView, get_component_url
from django_components.library import TagProtectedError
@ -162,6 +162,7 @@ __all__ = [
"component_shorthand_formatter",
"format_attributes",
"get_component_by_class_id",
"get_component_defaults",
"get_component_dirs",
"get_component_files",
"get_component_url",

View file

@ -208,10 +208,7 @@ class CreateCommand(ComponentCommand):
css_file = "{css_filename}"
class Kwargs:
param: str
class Defaults:
param = "sample value"
param: str = "sample value"
def get_template_data(self, args, kwargs: Kwargs, slots, context):
return {{

View file

@ -527,6 +527,10 @@ class ComponentMeta(ComponentMediaMeta):
# class Kwargs:
# ...
# ```
# NOTE: Using dataclasses with `slots=True` could be faster than using NamedTuple,
# but in real world web pages that may load 1-2s, data access and instantiation
# is only on the order of milliseconds, or about 0.1% of the overall time.
# See https://github.com/django-components/django-components/pull/1467#discussion_r2449009201
for data_class_name in ["Args", "Kwargs", "Slots", "TemplateData", "JsData", "CssData"]:
data_class = attrs.get(data_class_name)
# Not a class
@ -698,7 +702,7 @@ class Component(metaclass=ComponentMeta):
class Table(Component):
class Kwargs:
color: str
size: int
size: int = 10
def get_template_data(self, args, kwargs: Kwargs, slots, context):
assert isinstance(kwargs, Table.Kwargs)
@ -714,6 +718,7 @@ class Component(metaclass=ComponentMeta):
- Validate the input at runtime.
- Set type hints for the keyword arguments for data methods like
[`get_template_data()`](../api#django_components.Component.get_template_data).
- Set defaults for individual fields
- Document the component inputs.
You can also use `Kwargs` to validate the keyword arguments for
@ -725,6 +730,10 @@ class Component(metaclass=ComponentMeta):
)
```
The defaults set on `Kwargs` will be merged with defaults from
[`Component.Defaults`](../api/#django_components.Component.Defaults) class.
`Kwargs` takes precendence. Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
If you do not specify any bases, the `Kwargs` class will be automatically
converted to a `NamedTuple`:
@ -2265,6 +2274,8 @@ class Component(metaclass=ComponentMeta):
"""
The fields of this class are used to set default values for the component's kwargs.
These defaults will be merged with defaults on [`Component.Kwargs`](../api/#django_components.Component.Kwargs).
Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
**Example:**
@ -2669,6 +2680,9 @@ class Component(metaclass=ComponentMeta):
then the `kwargs` property will return an instance of that `Kwargs` class.
- Otherwise, `kwargs` will be a plain dict.
Kwargs have the defaults applied to them.
Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
**Example:**
With `Kwargs` class:
@ -2715,6 +2729,9 @@ class Component(metaclass=ComponentMeta):
is not typed and will remain as plain dict even if you define the
[`Component.Kwargs`](../api/#django_components.Component.Kwargs) class.
`raw_kwargs` have the defaults applied to them.
Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
**Example:**
```python

View file

@ -1,6 +1,8 @@
import dataclasses
import sys
from dataclasses import MISSING, Field, dataclass
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Type
from inspect import isclass
from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Optional, Type, Union
from weakref import WeakKeyDictionary
from django_components.extension import (
@ -57,55 +59,137 @@ class ComponentDefaultField(NamedTuple):
is_factory: bool
def get_component_defaults(component: Union[Type["Component"], "Component"]) -> Dict[str, Any]:
"""
Generate a defaults dictionary for a [`Component`](../api#django_components.Component).
The defaults dictionary is generated from the [`Component.Defaults`](../api#django_components.Component.Defaults)
and [`Component.Kwargs`](../api#django_components.Component.Kwargs) classes.
`Kwargs` take precedence over `Defaults`.
Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
**Example:**
```py
from django_components import Component, Default, get_component_defaults
class MyTable(Component):
class Kwargs:
position: str
order: int
items: list[int]
variable: str = "from_kwargs"
class Defaults:
position: str = "left"
items = Default(lambda: [1, 2, 3])
# Get the defaults dictionary
defaults = get_component_defaults(MyTable)
# {
# "position": "left",
# "items": [1, 2, 3],
# "variable": "from_kwargs",
# }
```
"""
component_cls = component if isclass(component) else component.__class__
defaults_fields = defaults_by_component[component_cls] # type: ignore[index]
defaults: dict[str, Any] = {}
_apply_defaults(defaults, defaults_fields)
return defaults
# 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
def _extract_defaults(defaults: Optional[Type], kwargs: Optional[Type]) -> List[ComponentDefaultField]:
"""
Given the `Defaults` and `Kwargs` classes from a component, this function extracts
the default values from them.
"""
# First, extract defaults from the `Defaults` class
defaults_fields_map: Dict[str, ComponentDefaultField] = {}
if defaults is not None:
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.
# TODO_V1 - Remove `component_class`
if default_field_key.startswith("__") or default_field_key in {"component_class", "component_cls"}:
continue
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.
# TODO_V1 - Remove `component_class`
if default_field_key.startswith("__") or default_field_key in {"component_class", "component_cls"}:
continue
default_field = getattr(defaults, default_field_key)
default_field = getattr(defaults, default_field_key)
if isinstance(default_field, property):
continue
if isinstance(default_field, property):
continue
# 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 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
# 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 = None
field_value = default_field
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
field_data = ComponentDefaultField(
key=default_field_key,
value=field_value,
is_factory=is_factory,
)
defaults_fields_map[default_field_key] = field_data
# If the field was defined with a simple assignment, assume it's NOT a factory.
else:
field_value = default_field
is_factory = False
# Next, extract defaults from the `Kwargs` class.
# We check for dataclasses and NamedTuple, as those are the supported ways to define defaults.
# Support for other types of `Kwargs` classes, like Pydantic models, is left to extensions.
kwargs_fields_map: Dict[str, ComponentDefaultField] = {}
if kwargs is not None:
if dataclasses.is_dataclass(kwargs):
for field in dataclasses.fields(kwargs):
if field.default is not dataclasses.MISSING:
field_value = field.default
is_factory = False
elif field.default_factory is not dataclasses.MISSING:
field_value = field.default_factory
is_factory = True
else:
continue # No default value
field_data = ComponentDefaultField(
key=default_field_key,
value=field_value,
is_factory=is_factory,
)
defaults_fields.append(field_data)
field_data = ComponentDefaultField(
key=field.name,
value=field_value,
is_factory=is_factory,
)
kwargs_fields_map[field.name] = field_data
return defaults_fields
# Check for NamedTuple.
# Note that we check for `_fields` to avoid accidentally matching `tuple` subclasses.
elif issubclass(kwargs, tuple) and hasattr(kwargs, "_fields"):
# `_field_defaults` is a dict of {field_name: default_value}
for field_name, default_value in getattr(kwargs, "_field_defaults", {}).items():
field_data = ComponentDefaultField(
key=field_name,
value=default_value,
is_factory=False,
)
kwargs_fields_map[field_name] = field_data
# Merge the two, with `kwargs` overwriting `defaults`.
merged_fields_map = {**defaults_fields_map, **kwargs_fields_map}
return list(merged_fields_map.values())
def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None:
@ -177,7 +261,9 @@ class DefaultsExtension(ComponentExtension):
# 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)
# Allow to simply define `Component.Kwargs` with defaults instead of 2 separate classes
kwargs_cls = getattr(ctx.component_cls, "Kwargs", None)
defaults_by_component[ctx.component_cls] = _extract_defaults(defaults_cls, kwargs_cls)
# Apply defaults to missing or `None` values in `kwargs`
def on_component_input(self, ctx: OnComponentInputContext) -> None:

View file

@ -1,8 +1,9 @@
from dataclasses import field
from dataclasses import dataclass, field
from typing import NamedTuple
from django.template import Context
from django_components import Component, Default
from django_components import Component, Default, get_component_defaults
from django_components.testing import djc_test
from .testutils import setup_test_config
@ -169,3 +170,255 @@ class TestComponentDefaults:
)
assert did_call_context
def test_defaults_from_kwargs_namedtuple(self):
did_call_context = False
class TestComponent(Component):
template = ""
class Kwargs(NamedTuple):
another: int
variable: str = "default_from_kwargs"
def get_template_data(self, args, kwargs, slots, context):
nonlocal did_call_context
did_call_context = True
assert self.raw_kwargs == {
"variable": "default_from_kwargs",
"another": 123,
}
return {}
TestComponent.render(
kwargs={"another": 123},
)
assert did_call_context
def test_defaults_from_kwargs_dataclass(self):
did_call_context = False
class TestComponent(Component):
template = ""
@dataclass
class Kwargs:
another: int
variable: str = "default_from_kwargs"
def get_template_data(self, args, kwargs, slots, context):
nonlocal did_call_context
did_call_context = True
assert self.raw_kwargs == {
"variable": "default_from_kwargs",
"another": 123,
}
return {}
TestComponent.render(
kwargs={"another": 123},
)
assert did_call_context
def test_defaults_from_kwargs_other_class(self):
did_call_context = False
class CustomKwargs:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
self._kwargs = kwargs
def _asdict(self):
return self._kwargs
class TestComponent(Component):
template = ""
class Kwargs(CustomKwargs):
another: int
variable: str = "default_from_kwargs"
def get_template_data(self, args, kwargs, slots, context):
nonlocal did_call_context
did_call_context = True
# No defaults should be applied from a plain class
assert self.raw_kwargs == {
"another": 123,
}
return {}
TestComponent.render(
kwargs={"another": 123},
)
assert did_call_context
def test_defaults_from_defaults_and_kwargs_namedtuple(self):
did_call_context = False
class TestComponent(Component):
template = ""
class Kwargs(NamedTuple):
from_defaults_only: str
variable: str = "from_kwargs"
from_kwargs_only: str = "kwargs_default"
class Defaults:
variable = "from_defaults"
from_defaults_only = "defaults_default"
def get_template_data(self, args, kwargs, slots, context):
nonlocal did_call_context
did_call_context = True
assert self.raw_kwargs == {
"variable": "from_kwargs", # Overridden by Kwargs
"from_defaults_only": "defaults_default",
"from_kwargs_only": "kwargs_default",
}
return {}
TestComponent.render(kwargs={})
assert did_call_context
def test_defaults_from_defaults_and_kwargs_dataclass(self):
did_call_context = False
class TestComponent(Component):
template = ""
@dataclass
class Kwargs:
from_defaults_only: str
variable: str = "from_kwargs"
from_kwargs_only: str = "kwargs_default"
class Defaults:
variable = "from_defaults"
from_defaults_only = "defaults_default"
def get_template_data(self, args, kwargs, slots, context):
nonlocal did_call_context
did_call_context = True
assert self.raw_kwargs == {
"variable": "from_kwargs", # Overridden by Kwargs
"from_defaults_only": "defaults_default",
"from_kwargs_only": "kwargs_default",
}
return {}
TestComponent.render(kwargs={})
assert did_call_context
def test_defaults_from_defaults_and_kwargs_other_class(self):
did_call_context = False
class CustomKwargs:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
self._kwargs = kwargs
def _asdict(self):
return self._kwargs
class TestComponent(Component):
template = ""
class Kwargs(CustomKwargs):
variable: str = "from_kwargs"
class Defaults:
variable = "from_defaults"
from_defaults_only = "defaults_default"
def get_template_data(self, args, kwargs, slots, context):
nonlocal did_call_context
did_call_context = True
assert self.raw_kwargs == {
"variable": "from_defaults", # No override
"from_defaults_only": "defaults_default",
}
return {}
TestComponent.render(kwargs={})
assert did_call_context
@djc_test
class TestGetComponentDefaults:
def test_defaults_with_factory(self):
class MyComponent(Component):
template = ""
class Defaults:
val = "static"
factory_val = Default(lambda: "from_factory")
defaults = get_component_defaults(MyComponent)
assert defaults == {
"val": "static",
"factory_val": "from_factory",
}
def test_kwargs_dataclass_with_factory(self):
class MyComponent(Component):
template = ""
@dataclass
class Kwargs:
val: str = "static"
factory_val: str = field(default_factory=lambda: "from_factory")
defaults = get_component_defaults(MyComponent)
assert defaults == {
"val": "static",
"factory_val": "from_factory",
}
def test_defaults_and_kwargs_overrides_with_factories(self):
class MyComponent(Component):
template = ""
@dataclass
class Kwargs:
val_both: str = field(default_factory=lambda: "from_kwargs_factory")
val_kwargs: str = field(default_factory=lambda: "kwargs_only")
class Defaults:
val_both = Default(lambda: "from_defaults_factory")
val_defaults = Default(lambda: "defaults_only")
defaults = get_component_defaults(MyComponent)
assert defaults == {
"val_both": "from_kwargs_factory",
"val_kwargs": "kwargs_only",
"val_defaults": "defaults_only",
}
def test_kwargs_namedtuple_with_defaults(self):
class MyComponent(Component):
template = ""
class Kwargs(NamedTuple):
val_no_default: int
val_defaults: str
val_kwargs: str = "kwargs_default"
class Defaults:
val_defaults = "defaults_default"
defaults = get_component_defaults(component=MyComponent)
assert defaults == {
"val_kwargs": "kwargs_default",
"val_defaults": "defaults_default",
}

View file

@ -22,10 +22,7 @@ class TestDynamicComponent:
class Kwargs:
variable: str
variable2: str
class Defaults:
variable2 = "default"
variable2: str = "default"
def get_template_data(self, args, kwargs: Kwargs, slots, context):
return {