mirror of
https://github.com/django-components/django-components.git
synced 2025-11-13 20:25:20 +00:00
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
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:
parent
c37628dea0
commit
28ff1d072a
11 changed files with 561 additions and 119 deletions
62
CHANGELOG.md
62
CHANGELOG.md
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 {{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue