mirror of
https://github.com/django-components/django-components.git
synced 2025-08-31 11:17:21 +00:00
refactor: use typevar defaults + raise on conflicting extensions (#1125)
* refactor: use typevar defaults + raise on conflicting extensions * refactor: fix linter errors
This commit is contained in:
parent
61528ef0ad
commit
06cad2ec64
21 changed files with 321 additions and 93 deletions
|
@ -103,7 +103,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
```djc_py
|
||||
from typing import Dict, NotRequired, Optional, Tuple, TypedDict
|
||||
|
||||
from django_components import Component, SlotFunc, register, types
|
||||
from django_components import Component, SlotContent, register, types
|
||||
|
||||
from myapp.templatetags.mytags import comp_registry
|
||||
|
||||
|
@ -114,7 +114,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
type MyMenuArgs = Tuple[int, str]
|
||||
|
||||
class MyMenuSlots(TypedDict):
|
||||
default: NotRequired[Optional[SlotFunc[EmptyDict]]]
|
||||
default: NotRequired[Optional[SlotContent[EmptyDict]]]
|
||||
|
||||
class MyMenuProps(TypedDict):
|
||||
vertical: NotRequired[bool]
|
||||
|
@ -124,7 +124,7 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
|||
# Define the component
|
||||
# NOTE: Don't forget to set the `registry`!
|
||||
@register("my_menu", registry=comp_registry)
|
||||
class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots, Any, Any, Any]):
|
||||
class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots]):
|
||||
def get_context_data(
|
||||
self,
|
||||
*args,
|
||||
|
|
|
@ -169,6 +169,14 @@ class MyExtension(ComponentExtension):
|
|||
ctx.component_cls.my_attr = "my_value"
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
The `name` attribute MUST be unique across all extensions.
|
||||
|
||||
Moreover, the `name` attribute MUST NOT conflict with existing Component class API.
|
||||
|
||||
So if you name an extension `render`, it will conflict with the [`render()`](../../../reference/api/#django_components.Component.render) method of the `Component` class.
|
||||
|
||||
### Defining the extension class
|
||||
|
||||
In previous sections we've seen the `View` and `Storybook` extensions classes that were nested within the `Component` class:
|
||||
|
|
|
@ -10,7 +10,7 @@ Use this to add type hints to your components, or to validate component inputs.
|
|||
```py
|
||||
from django_components import Component
|
||||
|
||||
ButtonType = Component[Args, Kwargs, Slots, Data, JsData, CssData]
|
||||
ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData, CssData]
|
||||
|
||||
class Button(ButtonType):
|
||||
template_file = "button.html"
|
||||
|
@ -24,16 +24,34 @@ The generic parameters are:
|
|||
- `Args` - Positional arguments, must be a `Tuple` or `Any`
|
||||
- `Kwargs` - Keyword arguments, must be a `TypedDict` or `Any`
|
||||
- `Slots` - Slots, must be a `TypedDict` or `Any`
|
||||
- `Data` - Data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), must be a `TypedDict` or `Any`
|
||||
- `TemplateData` - Data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), must be a `TypedDict` or `Any`
|
||||
- `JsData` - Data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), must be a `TypedDict` or `Any`
|
||||
- `CssData` - Data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data), must be a `TypedDict` or `Any`
|
||||
|
||||
## Example
|
||||
You can specify as many or as few of the parameters as you want. The rest will default to `Any`.
|
||||
|
||||
All of these are valid:
|
||||
|
||||
```py
|
||||
ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData, CssData]
|
||||
ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData]
|
||||
ButtonType = Component[Args, Kwargs, Slots, TemplateData]
|
||||
ButtonType = Component[Args, Kwargs, Slots]
|
||||
ButtonType = Component[Args, Kwargs]
|
||||
ButtonType = Component[Args]
|
||||
```
|
||||
|
||||
!!! info
|
||||
|
||||
Setting a type parameter to `Any` will disable typing and validation for that part.
|
||||
|
||||
## Typing inputs
|
||||
|
||||
You can use the first 3 parameters to type inputs.
|
||||
|
||||
```python
|
||||
from typing import NotRequired, Tuple, TypedDict
|
||||
from pydantic import BaseModel
|
||||
from django_components import Component, SlotContent, SlotFunc
|
||||
from typing_extensions import NotRequired, Tuple, TypedDict
|
||||
from django_components import Component, Slot, SlotContent
|
||||
|
||||
###########################################
|
||||
# 1. Define the types
|
||||
|
@ -54,39 +72,17 @@ class ButtonFooterSlotData(TypedDict):
|
|||
|
||||
# Slots
|
||||
class ButtonSlots(TypedDict):
|
||||
# SlotContent == str or slot func
|
||||
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
||||
header: SlotContent
|
||||
# Use SlotFunc for slot functions.
|
||||
# Use `Slot` for slot functions.
|
||||
# The generic specifies the data available to the slot function
|
||||
footer: NotRequired[SlotFunc[ButtonFooterSlotData]]
|
||||
|
||||
# Data returned from `get_context_data`
|
||||
class ButtonData(BaseModel):
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
# Data returned from `get_js_data`
|
||||
class ButtonJsData(BaseModel):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
# Data returned from `get_css_data`
|
||||
class ButtonCssData(BaseModel):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||
|
||||
###########################################
|
||||
# 2. Define the component with those types
|
||||
###########################################
|
||||
|
||||
ButtonType = Component[
|
||||
ButtonArgs,
|
||||
ButtonKwargs,
|
||||
ButtonSlots,
|
||||
ButtonData,
|
||||
ButtonJsData,
|
||||
ButtonCssData,
|
||||
]
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
|
@ -109,23 +105,81 @@ Button.render(
|
|||
},
|
||||
slots={
|
||||
"header": "...",
|
||||
# ERROR: Expects key "footer"
|
||||
"footer": lambda ctx, slot_data, slot_ref: slot_data.value + 1,
|
||||
# ERROR: Unexpected key "foo"
|
||||
"foo": "invalid",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any).
|
||||
If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any) or omit them.
|
||||
|
||||
The following will validate only the keyword inputs:
|
||||
|
||||
```python
|
||||
ButtonType = Component[
|
||||
ButtonArgs,
|
||||
ButtonKwargs,
|
||||
ButtonSlots,
|
||||
Any,
|
||||
Any,
|
||||
Any,
|
||||
]
|
||||
ButtonType = Component[Any, ButtonKwargs]
|
||||
|
||||
class Button(ButtonType):
|
||||
...
|
||||
```
|
||||
|
||||
## Typing data
|
||||
|
||||
You can use the last 3 parameters to type the data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), and [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
|
||||
|
||||
Use this together with the [Pydantic integration](#runtime-input-validation-with-types) to have runtime validation of the data.
|
||||
|
||||
```python
|
||||
from typing_extensions import NotRequired, Tuple, TypedDict
|
||||
from django_components import Component, Slot, SlotContent
|
||||
|
||||
###########################################
|
||||
# 1. Define the types
|
||||
###########################################
|
||||
|
||||
# Data returned from `get_context_data`
|
||||
class ButtonData(TypedDict):
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
# Data returned from `get_js_data`
|
||||
class ButtonJsData(TypedDict):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
# Data returned from `get_css_data`
|
||||
class ButtonCssData(TypedDict):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
###########################################
|
||||
# 2. Define the component with those types
|
||||
###########################################
|
||||
|
||||
ButtonType = Component[Any, Any, Any, ButtonData, ButtonJsData, ButtonCssData]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
...
|
||||
|
||||
def get_js_data(self, *args, **kwargs):
|
||||
...
|
||||
|
||||
def get_css_data(self, *args, **kwargs):
|
||||
...
|
||||
```
|
||||
|
||||
When you then call
|
||||
[`Component.render`](../../../reference/api#django_components.Component.render)
|
||||
or [`Component.render_to_response`](../../../reference/api#django_components.Component.render_to_response),
|
||||
the component will raise an error if the data is not valid.
|
||||
|
||||
If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any).
|
||||
|
||||
The following will validate only the data returned from `get_js_data`:
|
||||
|
||||
```python
|
||||
ButtonType = Component[Any, Any, Any, Any, ButtonJsData]
|
||||
|
||||
class Button(ButtonType):
|
||||
...
|
||||
|
@ -182,6 +236,9 @@ class Button(Component[EmptyTuple, EmptyDict, EmptyDict, EmptyDict, EmptyDict, E
|
|||
|
||||
Since v0.136, input validation is available as a separate extension.
|
||||
|
||||
By defualt, when you add types to your component, this will only set up static type hints,
|
||||
but it will not validate the inputs.
|
||||
|
||||
To enable input validation, you need to install the [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic) extension:
|
||||
|
||||
```bash
|
||||
|
|
|
@ -47,7 +47,7 @@ Component.render(
|
|||
context: Mapping | django.template.Context | None = None,
|
||||
args: List[Any] | None = None,
|
||||
kwargs: Dict[str, Any] | None = None,
|
||||
slots: Dict[str, str | SafeString | SlotFunc] | None = None,
|
||||
slots: Dict[str, str | SafeString | SlotContent] | None = None,
|
||||
escape_slots_content: bool = True
|
||||
) -> str:
|
||||
```
|
||||
|
@ -60,7 +60,7 @@ Component.render(
|
|||
|
||||
- _`slots`_ - Component slot fills. This is the same as pasing `{% fill %}` tags to the component.
|
||||
Accepts a dictionary of `{ slot_name: slot_content }` where `slot_content` can be a string
|
||||
or [`SlotFunc`](#slotfunc).
|
||||
or `Slot`.
|
||||
|
||||
- _`escape_slots_content`_ - Whether the content from `slots` should be escaped. `True` by default to prevent XSS attacks. If you disable escaping, you should make sure that any content you pass to the slots is safe, especially if it comes from user input.
|
||||
|
||||
|
|
|
@ -386,7 +386,7 @@ Components API is fully typed, and supports [static type hints](https://django-c
|
|||
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
|
||||
|
||||
```py
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
|
||||
|
||||
from django_components import Component
|
||||
|
||||
|
@ -397,22 +397,19 @@ class ButtonKwargs(TypedDict):
|
|||
another: int
|
||||
maybe_var: NotRequired[int] # May be omitted
|
||||
|
||||
class ButtonData(TypedDict):
|
||||
variable: str
|
||||
|
||||
class ButtonSlots(TypedDict):
|
||||
my_slot: NotRequired[SlotFunc]
|
||||
# Use `Slot` for slot functions.
|
||||
my_slot: NotRequired[Slot]
|
||||
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
||||
another_slot: SlotContent
|
||||
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, JsData, CssData]
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
self.input.args[0] # int
|
||||
self.input.kwargs["variable"] # str
|
||||
self.input.slots["my_slot"] # SlotFunc[MySlotData]
|
||||
|
||||
return {} # Error: Key "variable" is missing
|
||||
self.input.slots["my_slot"] # Slot
|
||||
```
|
||||
|
||||
When you then call
|
||||
|
|
|
@ -19,10 +19,6 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentCommand
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentDefaults
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
@ -35,6 +31,10 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentInput
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.ComponentMediaInput
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
|
|
@ -462,8 +462,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
|
|||
## `upgradecomponent`
|
||||
|
||||
```txt
|
||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
|
||||
```
|
||||
|
||||
|
@ -507,9 +507,9 @@ Deprecated. Use `components upgrade` instead.
|
|||
## `startcomponent`
|
||||
|
||||
```txt
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
|
||||
[--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
|
||||
[--no-color] [--force-color] [--skip-checks]
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
name
|
||||
|
||||
```
|
||||
|
|
|
@ -109,6 +109,7 @@ name | type | description
|
|||
|
||||
::: django_components.extension.ComponentExtension.on_component_rendered
|
||||
options:
|
||||
heading_level: 3
|
||||
show_root_heading: true
|
||||
show_signature: true
|
||||
separate_signature: true
|
||||
|
|
|
@ -20,7 +20,7 @@ Import as
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1037" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1064" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -43,7 +43,7 @@ If you insert this tag multiple times, ALL CSS links will be duplicately inserte
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1059" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1086" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
|
|||
|
||||
|
||||
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1564" target="_blank">See source code</a>
|
||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1669" target="_blank">See source code</a>
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue