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 # 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. 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. - 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. 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! 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 !!! warning
When [typing](../fundamentals/typing_and_validation.md) your components with [`Args`](../../../reference/api/#django_components.Component.Args), This works only when `Component.Kwargs` is a plain class, NamedTuple or dataclass.
[`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.
### Default factories ### Default factories
@ -124,30 +132,28 @@ class MyTable(Component):
### Accessing defaults ### 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 ```py
from django_components import Component, Default, register from django_components import Component, Default, get_component_defaults
@register("my_table")
class MyTable(Component): class MyTable(Component):
class Kwargs:
position: str
order: int
items: list[int]
variable: str = "from_kwargs"
class Defaults: class Defaults:
position = "left" position: str = "left"
selected_items = Default(lambda: [1, 2, 3]) items = Default(lambda: [1, 2, 3])
def get_template_data(self, args, kwargs, slots, context): defaults = get_component_defaults(MyTable)
return { # {
"position": kwargs["position"], # "position": "left",
"selected_items": kwargs["selected_items"], # "items": [1, 2, 3],
} # "variable": "from_kwargs",
``` # }
We can access individual defaults like this:
```py
print(MyTable.Defaults.position)
print(MyTable.Defaults.selected_items)
``` ```

View file

@ -17,10 +17,7 @@ Each method handles the data independently - you can define different data for t
class ProfileCard(Component): class ProfileCard(Component):
class Kwargs: class Kwargs:
user_id: int user_id: int
show_details: bool show_details: bool = True
class Defaults:
show_details = True
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):
user = User.objects.get(id=kwargs.user_id) user = User.objects.get(id=kwargs.user_id)
@ -304,7 +301,7 @@ class ProfileCard(Component):
## Default values ## 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: These defaults will be applied either when:
@ -321,12 +318,9 @@ from django_components import Component, Default, register
@register("profile_card") @register("profile_card")
class ProfileCard(Component): class ProfileCard(Component):
class Kwargs: 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): def get_template_data(self, args, kwargs: Kwargs, slots, context):
return { return {
"show_details": kwargs.show_details, "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 ## Accessing Render API
All three data methods have access to the Component's [Render API](../render_api), which includes: 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 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. 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. [`Component`](../../reference/api#django_components.Component) class.
Each attribute in the `Defaults` class represents a default value for a corresponding input. 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) Next, you will learn [how to use slots give your components even more flexibility ➡️](./adding_slots.md)

View file

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

View file

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

View file

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

View file

@ -527,6 +527,10 @@ class ComponentMeta(ComponentMediaMeta):
# class Kwargs: # 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"]: for data_class_name in ["Args", "Kwargs", "Slots", "TemplateData", "JsData", "CssData"]:
data_class = attrs.get(data_class_name) data_class = attrs.get(data_class_name)
# Not a class # Not a class
@ -698,7 +702,7 @@ class Component(metaclass=ComponentMeta):
class Table(Component): class Table(Component):
class Kwargs: class Kwargs:
color: str color: str
size: int size: int = 10
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):
assert isinstance(kwargs, Table.Kwargs) assert isinstance(kwargs, Table.Kwargs)
@ -714,6 +718,7 @@ class Component(metaclass=ComponentMeta):
- Validate the input at runtime. - Validate the input at runtime.
- Set type hints for the keyword arguments for data methods like - Set type hints for the keyword arguments for data methods like
[`get_template_data()`](../api#django_components.Component.get_template_data). [`get_template_data()`](../api#django_components.Component.get_template_data).
- Set defaults for individual fields
- Document the component inputs. - Document the component inputs.
You can also use `Kwargs` to validate the keyword arguments for 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 If you do not specify any bases, the `Kwargs` class will be automatically
converted to a `NamedTuple`: 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. 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). Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
**Example:** **Example:**
@ -2669,6 +2680,9 @@ class Component(metaclass=ComponentMeta):
then the `kwargs` property will return an instance of that `Kwargs` class. then the `kwargs` property will return an instance of that `Kwargs` class.
- Otherwise, `kwargs` will be a plain dict. - Otherwise, `kwargs` will be a plain dict.
Kwargs have the defaults applied to them.
Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
**Example:** **Example:**
With `Kwargs` class: 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 is not typed and will remain as plain dict even if you define the
[`Component.Kwargs`](../api/#django_components.Component.Kwargs) class. [`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:** **Example:**
```python ```python

View file

@ -1,6 +1,8 @@
import dataclasses
import sys import sys
from dataclasses import MISSING, Field, dataclass 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 weakref import WeakKeyDictionary
from django_components.extension import ( from django_components.extension import (
@ -57,55 +59,137 @@ class ComponentDefaultField(NamedTuple):
is_factory: bool 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, # Figure out which defaults are factories and which are not, at class creation,
# so that the actual creation of the defaults dictionary is simple. # so that the actual creation of the defaults dictionary is simple.
def _extract_defaults(defaults: Optional[Type]) -> List[ComponentDefaultField]: def _extract_defaults(defaults: Optional[Type], kwargs: Optional[Type]) -> List[ComponentDefaultField]:
defaults_fields: List[ComponentDefaultField] = [] """
if defaults is None: Given the `Defaults` and `Kwargs` classes from a component, this function extracts
return defaults_fields 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): default_field = getattr(defaults, default_field_key)
# 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) if isinstance(default_field, property):
continue
if isinstance(default_field, property): # If the field was defined with dataclass.field(), take the default / factory from there.
continue 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 the field was defined with our `Default` class, it defined a factory
if isinstance(default_field, Field): elif isinstance(default_field, Default):
if default_field.default is not MISSING: field_value = default_field.value
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 is_factory = True
# If the field was defined with a simple assignment, assume it's NOT a factory.
else: else:
field_value = None field_value = default_field
is_factory = False is_factory = False
# If the field was defined with our `Default` class, it defined a factory field_data = ComponentDefaultField(
elif isinstance(default_field, Default): key=default_field_key,
field_value = default_field.value value=field_value,
is_factory = True 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. # Next, extract defaults from the `Kwargs` class.
else: # We check for dataclasses and NamedTuple, as those are the supported ways to define defaults.
field_value = default_field # Support for other types of `Kwargs` classes, like Pydantic models, is left to extensions.
is_factory = False 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( field_data = ComponentDefaultField(
key=default_field_key, key=field.name,
value=field_value, value=field_value,
is_factory=is_factory, is_factory=is_factory,
) )
defaults_fields.append(field_data) 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: def _apply_defaults(kwargs: Dict, defaults: List[ComponentDefaultField]) -> None:
@ -177,7 +261,9 @@ class DefaultsExtension(ComponentExtension):
# each time a component is rendered. # each time a component is rendered.
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None: def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
defaults_cls = getattr(ctx.component_cls, "Defaults", 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` # Apply defaults to missing or `None` values in `kwargs`
def on_component_input(self, ctx: OnComponentInputContext) -> None: 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.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 django_components.testing import djc_test
from .testutils import setup_test_config from .testutils import setup_test_config
@ -169,3 +170,255 @@ class TestComponentDefaults:
) )
assert did_call_context 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: class Kwargs:
variable: str variable: str
variable2: str variable2: str = "default"
class Defaults:
variable2 = "default"
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):
return { return {