mirror of
https://github.com/django-components/django-components.git
synced 2025-09-20 12:49:45 +00:00
refactor: change component typing from generics to class attributes (#1138)
This commit is contained in:
parent
912d8e8074
commit
b49002b545
25 changed files with 2451 additions and 610 deletions
118
CHANGELOG.md
118
CHANGELOG.md
|
@ -1,5 +1,123 @@
|
||||||
# Release notes
|
# Release notes
|
||||||
|
|
||||||
|
## 🚨📢 v0.140.0
|
||||||
|
|
||||||
|
#### 🚨📢 BREAKING CHANGES
|
||||||
|
|
||||||
|
- Component typing no longer uses generics. Instead, the types are now defined as class attributes of the component class.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```py
|
||||||
|
Args = Tuple[float, str]
|
||||||
|
|
||||||
|
class Button(Component[Args]):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class Button(Component):
|
||||||
|
class Args(NamedTuple):
|
||||||
|
size: float
|
||||||
|
text: str
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Migrating from generics to class attributes](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/#migrating-from-generics-to-class-attributes) for more info.
|
||||||
|
|
||||||
|
- The interface of the not-yet-released `get_js_data()` and `get_css_data()` methods has changed to
|
||||||
|
match `get_template_data()`.
|
||||||
|
|
||||||
|
```py
|
||||||
|
def get_js_data(self, *args, **kwargs):
|
||||||
|
def get_css_data(self, *args, **kwargs):
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
|
```
|
||||||
|
|
||||||
|
- Removed `EmptyTuple` and `EmptyDict` types. Instead, there is now a single `Empty` type.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import Component, Empty
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
Args = Empty
|
||||||
|
Kwargs = Empty
|
||||||
|
```
|
||||||
|
|
||||||
|
- The order of arguments in `Component.render_to_response()` has changed
|
||||||
|
to match that of `Component.render()`.
|
||||||
|
|
||||||
|
Please ensure that you pass the parameters as kwargs, not as positional arguments,
|
||||||
|
to avoid breaking changes.
|
||||||
|
|
||||||
|
The signature changed, moving the `args` and `kwargs` parameters to 2nd and 3rd position.
|
||||||
|
|
||||||
|
Before:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def render_to_response(
|
||||||
|
cls,
|
||||||
|
context: Optional[Union[Dict[str, Any], Context]] = None,
|
||||||
|
slots: Optional[SlotsType] = None,
|
||||||
|
escape_slots_content: bool = True,
|
||||||
|
args: Optional[ArgsType] = None,
|
||||||
|
kwargs: Optional[KwargsType] = None,
|
||||||
|
type: RenderType = "document",
|
||||||
|
request: Optional[HttpRequest] = None,
|
||||||
|
*response_args: Any,
|
||||||
|
**response_kwargs: Any,
|
||||||
|
) -> HttpResponse:
|
||||||
|
```
|
||||||
|
|
||||||
|
After:
|
||||||
|
|
||||||
|
```py
|
||||||
|
def render_to_response(
|
||||||
|
context: Optional[Union[Dict[str, Any], Context]] = None,
|
||||||
|
args: Optional[Tuple[Any, ...]] = None,
|
||||||
|
kwargs: Optional[Mapping] = None,
|
||||||
|
slots: Optional[Mapping] = None,
|
||||||
|
escape_slots_content: bool = True,
|
||||||
|
type: RenderType = "document",
|
||||||
|
render_dependencies: bool = True,
|
||||||
|
request: Optional[HttpRequest] = None,
|
||||||
|
*response_args: Any,
|
||||||
|
**response_kwargs: Any,
|
||||||
|
) -> HttpResponse:
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 🚨📢 Deprecation
|
||||||
|
|
||||||
|
- `get_context_data()` is now deprecated. Use `get_template_data()` instead.
|
||||||
|
|
||||||
|
`get_template_data()` behaves the same way, but has a different function signature
|
||||||
|
to accept also slots and context.
|
||||||
|
|
||||||
|
Since `get_context_data()` is widely used, it will remain available until v2.
|
||||||
|
|
||||||
|
- `SlotContent` was renamed to `SlotInput`. The old name is deprecated and will be removed in v1.
|
||||||
|
|
||||||
|
#### Feat
|
||||||
|
|
||||||
|
- Input validation is now part of the render process.
|
||||||
|
|
||||||
|
When you specify the input types (such as `Component.Args`, `Component.Kwargs`, etc),
|
||||||
|
the actual inputs to data methods (`Component.get_template_data()`, etc) will be instances of the types you specified.
|
||||||
|
|
||||||
|
This practically brings back input validation, because the instantiation of the types
|
||||||
|
will raise an error if the inputs are not valid.
|
||||||
|
|
||||||
|
Read more on [Typing and validation](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/)
|
||||||
|
|
||||||
## v0.139.1
|
## v0.139.1
|
||||||
|
|
||||||
#### Fix
|
#### Fix
|
||||||
|
|
59
README.md
59
README.md
|
@ -389,52 +389,53 @@ class Header(Component):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Static type hints
|
### Input validation and static type hints
|
||||||
|
|
||||||
Components API is fully typed, and supports [static type hints](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/).
|
Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/).
|
||||||
|
|
||||||
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
|
To opt-in to input validation, define types for component's args, kwargs, slots, and more:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
|
from typing import NamedTuple, Optional
|
||||||
|
from django.template import Context
|
||||||
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
from django_components import Component
|
class Button(Component):
|
||||||
|
class Args(NamedTuple):
|
||||||
|
size: int
|
||||||
|
text: str
|
||||||
|
|
||||||
ButtonArgs = Tuple[int, str]
|
class Kwargs(NamedTuple):
|
||||||
|
variable: str
|
||||||
|
another: int
|
||||||
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
class ButtonKwargs(TypedDict):
|
class Slots(NamedTuple):
|
||||||
variable: str
|
my_slot: Optional[SlotInput] = None
|
||||||
another: int
|
another_slot: SlotInput
|
||||||
maybe_var: NotRequired[int] # May be omitted
|
|
||||||
|
|
||||||
class ButtonSlots(TypedDict):
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
# Use `Slot` for slot functions.
|
args.size # int
|
||||||
my_slot: NotRequired[Slot]
|
kwargs.variable # str
|
||||||
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
slots.my_slot # Slot[MySlotData]
|
||||||
another_slot: SlotContent
|
|
||||||
|
|
||||||
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"] # Slot[MySlotData]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When you then call
|
To have type hints when calling
|
||||||
[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or
|
[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or
|
||||||
[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response),
|
[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response),
|
||||||
you will get type hints:
|
wrap the inputs in their respective `Args`, `Kwargs`, and `Slots` classes:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
Button.render(
|
Button.render(
|
||||||
# Error: First arg must be `int`, got `float`
|
# Error: First arg must be `int`, got `float`
|
||||||
args=(1.25, "abc"),
|
args=Button.Args(
|
||||||
|
size=1.25,
|
||||||
|
text="abc",
|
||||||
|
),
|
||||||
# Error: Key "another" is missing
|
# Error: Key "another" is missing
|
||||||
kwargs={
|
kwargs=Button.Kwargs(
|
||||||
"variable": "text",
|
variable="text",
|
||||||
},
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -101,35 +101,30 @@ For live examples, see the [Community examples](../../overview/community.md#comm
|
||||||
It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`.
|
It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`.
|
||||||
|
|
||||||
```djc_py
|
```djc_py
|
||||||
from typing import Dict, NotRequired, Optional, Tuple, TypedDict
|
from typing import NamedTuple, Optional
|
||||||
|
from django_components import Component, SlotInput, register, types
|
||||||
from django_components import Component, SlotContent, register, types
|
|
||||||
|
|
||||||
from myapp.templatetags.mytags import comp_registry
|
from myapp.templatetags.mytags import comp_registry
|
||||||
|
|
||||||
# Define the types
|
|
||||||
class EmptyDict(TypedDict):
|
|
||||||
pass
|
|
||||||
|
|
||||||
type MyMenuArgs = Tuple[int, str]
|
|
||||||
|
|
||||||
class MyMenuSlots(TypedDict):
|
|
||||||
default: NotRequired[Optional[SlotContent[EmptyDict]]]
|
|
||||||
|
|
||||||
class MyMenuProps(TypedDict):
|
|
||||||
vertical: NotRequired[bool]
|
|
||||||
klass: NotRequired[str]
|
|
||||||
style: NotRequired[str]
|
|
||||||
|
|
||||||
# Define the component
|
# Define the component
|
||||||
# NOTE: Don't forget to set the `registry`!
|
# NOTE: Don't forget to set the `registry`!
|
||||||
@register("my_menu", registry=comp_registry)
|
@register("my_menu", registry=comp_registry)
|
||||||
class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots]):
|
class MyMenu(Component):
|
||||||
def get_context_data(
|
# Define the types
|
||||||
self,
|
class Args(NamedTuple):
|
||||||
*args,
|
size: int
|
||||||
attrs: Optional[Dict] = None,
|
text: str
|
||||||
):
|
|
||||||
|
class Kwargs(NamedTuple):
|
||||||
|
vertical: Optional[bool] = None
|
||||||
|
klass: Optional[str] = None
|
||||||
|
style: Optional[str] = None
|
||||||
|
|
||||||
|
class Slots(NamedTuple):
|
||||||
|
default: Optional[SlotInput] = None
|
||||||
|
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
attrs = ...
|
||||||
return {
|
return {
|
||||||
"attrs": attrs,
|
"attrs": attrs,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,251 +1,470 @@
|
||||||
## Adding type hints with Generics
|
## Typing overview
|
||||||
|
|
||||||
_New in version 0.92_
|
<!-- TODO_V1 - REMOVE IN v1 -->
|
||||||
|
|
||||||
The [`Component`](../../../reference/api#django_components.Component) class optionally accepts type parameters
|
!!! warning
|
||||||
that allow you to specify the types of args, kwargs, slots, and data.
|
|
||||||
|
|
||||||
Use this to add type hints to your components, or to validate component inputs.
|
In versions 0.92 to 0.139 (inclusive), the component typing was specified through generics.
|
||||||
|
|
||||||
|
Since v0.140, the types must be specified as class attributes of the Component class - `Args`, `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData`.
|
||||||
|
|
||||||
|
See [Migrating from generics to class attributes](#migrating-from-generics-to-class-attributes) for more info.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
Input validation was NOT part of Django Components between versions 0.136 and 0.139 (inclusive).
|
||||||
|
|
||||||
|
The [`Component`](../../../reference/api#django_components.Component) class optionally accepts class attributes
|
||||||
|
that allow you to define the types of args, kwargs, slots, as well as the data returned from the data methods.
|
||||||
|
|
||||||
|
Use this to add type hints to your components, to validate the inputs at runtime, and to document them.
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from django_components import Component
|
from typing import NamedTuple, Optional
|
||||||
|
from django.template import Context
|
||||||
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
ButtonType = Component[Args, Kwargs, Slots, TemplateData, JsData, CssData]
|
class Button(Component):
|
||||||
|
class Args(NamedTuple):
|
||||||
|
size: int
|
||||||
|
text: str
|
||||||
|
|
||||||
class Button(ButtonType):
|
class Kwargs(NamedTuple):
|
||||||
template_file = "button.html"
|
variable: str
|
||||||
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
class Slots(NamedTuple):
|
||||||
|
my_slot: Optional[SlotInput] = None
|
||||||
|
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
template_file = "button.html"
|
||||||
```
|
```
|
||||||
|
|
||||||
The generic parameters are:
|
The class attributes are:
|
||||||
|
|
||||||
- `Args` - Positional arguments, must be a `Tuple` or `Any`
|
- [`Args`](../../../reference/api#django_components.Component.Args) - Type for positional arguments.
|
||||||
- `Kwargs` - Keyword arguments, must be a `TypedDict` or `Any`
|
- [`Kwargs`](../../../reference/api#django_components.Component.Kwargs) - Type for keyword arguments.
|
||||||
- `Slots` - Slots, must be a `TypedDict` or `Any`
|
- [`Slots`](../../../reference/api#django_components.Component.Slots) - Type for slots.
|
||||||
- `TemplateData` - Data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), must be a `TypedDict` or `Any`
|
- [`TemplateData`](../../../reference/api#django_components.Component.TemplateData) - Type for data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data).
|
||||||
- `JsData` - Data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), must be a `TypedDict` or `Any`
|
- [`JsData`](../../../reference/api#django_components.Component.JsData) - Type for data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data).
|
||||||
- `CssData` - Data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data), must be a `TypedDict` or `Any`
|
- [`CssData`](../../../reference/api#django_components.Component.CssData) - Type for data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data).
|
||||||
|
|
||||||
You can specify as many or as few of the parameters as you want. The rest will default to `Any`.
|
You can specify as many or as few of these as you want, the rest will default to `None`.
|
||||||
|
|
||||||
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
|
## Typing inputs
|
||||||
|
|
||||||
You can use the first 3 parameters to type inputs.
|
You can use [`Component.Args`](../../../reference/api#django_components.Component.Args),
|
||||||
|
[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||||
|
and [`Component.Slots`](../../../reference/api#django_components.Component.Slots) to type the component inputs.
|
||||||
|
|
||||||
|
When you set these classes, at render time the `args`, `kwargs`, and `slots` parameters of the data methods
|
||||||
|
([`get_template_data()`](../../../reference/api#django_components.Component.get_template_data),
|
||||||
|
[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
|
||||||
|
[`get_css_data()`](../../../reference/api#django_components.Component.get_css_data))
|
||||||
|
will be instances of these classes.
|
||||||
|
|
||||||
|
This way, each component can have runtime validation of the inputs:
|
||||||
|
|
||||||
|
- When you use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
|
or [`@dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
|
||||||
|
instantiating these classes will check ONLY for the presence of the attributes.
|
||||||
|
- When you use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/),
|
||||||
|
instantiating these classes will check for the presence AND type of the attributes.
|
||||||
|
|
||||||
|
If you omit the [`Args`](../../../reference/api#django_components.Component.Args),
|
||||||
|
[`Kwargs`](../../../reference/api#django_components.Component.Kwargs), or
|
||||||
|
[`Slots`](../../../reference/api#django_components.Component.Slots) classes,
|
||||||
|
or set them to `None`, the inputs will be passed as plain lists or dictionaries,
|
||||||
|
and will not be validated.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing_extensions import NotRequired, Tuple, TypedDict
|
from typing_extensions import NamedTuple, TypedDict
|
||||||
from django_components import Component, Slot, SlotContent
|
from django.template import Context
|
||||||
|
from django_components import Component, Slot, SlotInput
|
||||||
###########################################
|
|
||||||
# 1. Define the types
|
|
||||||
###########################################
|
|
||||||
|
|
||||||
# Positional inputs
|
|
||||||
ButtonArgs = Tuple[str, ...]
|
|
||||||
|
|
||||||
# Keyword inputs
|
|
||||||
class ButtonKwargs(TypedDict):
|
|
||||||
name: str
|
|
||||||
age: int
|
|
||||||
maybe_var: NotRequired[int] # May be ommited
|
|
||||||
|
|
||||||
# The data available to the `footer` scoped slot
|
# The data available to the `footer` scoped slot
|
||||||
class ButtonFooterSlotData(TypedDict):
|
class ButtonFooterSlotData(TypedDict):
|
||||||
value: int
|
value: int
|
||||||
|
|
||||||
# Slots
|
# Define the component with the types
|
||||||
class ButtonSlots(TypedDict):
|
class Button(Component):
|
||||||
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
class Args(NamedTuple):
|
||||||
header: SlotContent
|
name: str
|
||||||
# Use `Slot` for slot functions.
|
|
||||||
# The generic specifies the data available to the slot function
|
|
||||||
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
|
||||||
|
|
||||||
###########################################
|
class Kwargs(NamedTuple):
|
||||||
# 2. Define the component with those types
|
surname: str
|
||||||
###########################################
|
age: int
|
||||||
|
maybe_var: Optional[int] = None # May be ommited
|
||||||
|
|
||||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
class Slots(NamedTuple):
|
||||||
|
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||||
|
# plain string, or a function that returns a string.
|
||||||
|
my_slot: Optional[SlotInput] = None
|
||||||
|
# Use `Slot` to allow ONLY `Slot` instances.
|
||||||
|
# The generic is optional, and it specifies the data available
|
||||||
|
# to the slot function.
|
||||||
|
footer: Slot[ButtonFooterSlotData]
|
||||||
|
|
||||||
class Button(ButtonType):
|
# Add type hints to the data method
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
...
|
# The parameters are instances of the classes we defined
|
||||||
```
|
assert isinstance(args, Button.Args)
|
||||||
|
assert isinstance(kwargs, Button.Kwargs)
|
||||||
|
assert isinstance(slots, Button.Slots)
|
||||||
|
|
||||||
When you then call
|
args.name # str
|
||||||
[`Component.render`](../../../reference/api#django_components.Component.render)
|
kwargs.age # int
|
||||||
or [`Component.render_to_response`](../../../reference/api#django_components.Component.render_to_response),
|
slots.footer # Slot[ButtonFooterSlotData]
|
||||||
you will get type hints:
|
|
||||||
|
|
||||||
```python
|
# Add type hints to the render call
|
||||||
Button.render(
|
Button.render(
|
||||||
# ERROR: Expects a string
|
args=Button.Args(
|
||||||
args=(123,),
|
name="John",
|
||||||
kwargs={
|
),
|
||||||
"name": "John",
|
kwargs=Button.Kwargs(
|
||||||
# ERROR: Expects an integer
|
surname="Doe",
|
||||||
"age": "invalid",
|
age=30,
|
||||||
},
|
),
|
||||||
slots={
|
slots=Button.Slots(
|
||||||
"header": "...",
|
footer=Button.Slot(lambda ctx, slot_data, slot_ref: slot_data.value + 1),
|
||||||
"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) or omit them.
|
If you don't want to validate some parts, set them to `None` or omit them.
|
||||||
|
|
||||||
The following will validate only the keyword inputs:
|
The following will validate only the keyword inputs:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
ButtonType = Component[Any, ButtonKwargs]
|
class Button(Component):
|
||||||
|
# We could also omit these
|
||||||
|
Args = None
|
||||||
|
Slots = None
|
||||||
|
|
||||||
class Button(ButtonType):
|
class Kwargs(NamedTuple):
|
||||||
...
|
name: str
|
||||||
|
age: int
|
||||||
|
|
||||||
|
# Only `kwargs` is instantiated. `args` and `slots` are not.
|
||||||
|
def get_template_data(self, args, kwargs: Kwargs, slots, context: Context):
|
||||||
|
assert isinstance(args, list)
|
||||||
|
assert isinstance(slots, dict)
|
||||||
|
assert isinstance(kwargs, Button.Kwargs)
|
||||||
|
|
||||||
|
args[0] # str
|
||||||
|
slots["footer"] # Slot[ButtonFooterSlotData]
|
||||||
|
kwargs.age # int
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
|
||||||
|
Components can receive slots as strings, functions, or instances of [`Slot`](../../../reference/api#django_components.Slot).
|
||||||
|
|
||||||
|
Internally these are all normalized to instances of [`Slot`](../../../reference/api#django_components.Slot).
|
||||||
|
|
||||||
|
Therefore, the `slots` dictionary available in data methods (like
|
||||||
|
[`get_template_data()`](../../../reference/api#django_components.Component.get_template_data))
|
||||||
|
will always be a dictionary of [`Slot`](../../../reference/api#django_components.Slot) instances.
|
||||||
|
|
||||||
|
To correctly type this dictionary, you should set the fields of `Slots` to
|
||||||
|
[`Slot`](../../../reference/api#django_components.Slot) or [`SlotInput`](../../../reference/api#django_components.SlotInput):
|
||||||
|
|
||||||
|
[`SlotInput`](../../../reference/api#django_components.SlotInput) is a union of `Slot`, string, and function types.
|
||||||
|
|
||||||
## Typing data
|
## 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).
|
You can use [`Component.TemplateData`](../../../reference/api#django_components.Component.TemplateData),
|
||||||
|
[`Component.JsData`](../../../reference/api#django_components.Component.JsData),
|
||||||
|
and [`Component.CssData`](../../../reference/api#django_components.Component.CssData) to type the data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_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.
|
When you set these classes, at render time they will be instantiated with the data returned from these methods.
|
||||||
|
|
||||||
|
This way, each component can have runtime validation of the returned data:
|
||||||
|
|
||||||
|
- When you use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
|
or [`@dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
|
||||||
|
instantiating these classes will check ONLY for the presence of the attributes.
|
||||||
|
- When you use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/),
|
||||||
|
instantiating these classes will check for the presence AND type of the attributes.
|
||||||
|
|
||||||
|
If you omit the [`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
|
||||||
|
[`JsData`](../../../reference/api#django_components.Component.JsData), or
|
||||||
|
[`CssData`](../../../reference/api#django_components.Component.CssData) classes,
|
||||||
|
or set them to `None`, the validation and instantiation will be skipped.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from typing_extensions import NotRequired, Tuple, TypedDict
|
from typing import NamedTuple
|
||||||
from django_components import Component, Slot, SlotContent
|
from django_components import Component
|
||||||
|
|
||||||
###########################################
|
class Button(Component):
|
||||||
# 1. Define the types
|
class TemplateData(NamedTuple):
|
||||||
###########################################
|
data1: str
|
||||||
|
data2: int
|
||||||
|
|
||||||
# Data returned from `get_context_data`
|
class JsData(NamedTuple):
|
||||||
class ButtonData(TypedDict):
|
js_data1: str
|
||||||
data1: str
|
js_data2: int
|
||||||
data2: int
|
|
||||||
|
|
||||||
# Data returned from `get_js_data`
|
class CssData(NamedTuple):
|
||||||
class ButtonJsData(TypedDict):
|
css_data1: str
|
||||||
js_data1: str
|
css_data2: int
|
||||||
js_data2: int
|
|
||||||
|
|
||||||
# Data returned from `get_css_data`
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
class ButtonCssData(TypedDict):
|
return {
|
||||||
css_data1: str
|
"data1": "...",
|
||||||
css_data2: int
|
"data2": 123,
|
||||||
|
}
|
||||||
|
|
||||||
###########################################
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
# 2. Define the component with those types
|
return {
|
||||||
###########################################
|
"js_data1": "...",
|
||||||
|
"js_data2": 123,
|
||||||
|
}
|
||||||
|
|
||||||
ButtonType = Component[Any, Any, Any, ButtonData, ButtonJsData, ButtonCssData]
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
|
return {
|
||||||
class Button(ButtonType):
|
"css_data1": "...",
|
||||||
def get_context_data(self, *args, **kwargs):
|
"css_data2": 123,
|
||||||
...
|
}
|
||||||
|
|
||||||
def get_js_data(self, *args, **kwargs):
|
|
||||||
...
|
|
||||||
|
|
||||||
def get_css_data(self, *args, **kwargs):
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When you then call
|
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly.
|
||||||
[`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
|
```python
|
||||||
ButtonType = Component[Any, Any, Any, Any, ButtonJsData]
|
from typing import NamedTuple
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
class Button(ButtonType):
|
class Button(Component):
|
||||||
...
|
class TemplateData(NamedTuple):
|
||||||
|
data1: str
|
||||||
|
data2: int
|
||||||
|
|
||||||
|
class JsData(NamedTuple):
|
||||||
|
js_data1: str
|
||||||
|
js_data2: int
|
||||||
|
|
||||||
|
class CssData(NamedTuple):
|
||||||
|
css_data1: str
|
||||||
|
css_data2: int
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
return Button.TemplateData(
|
||||||
|
data1="...",
|
||||||
|
data2=123,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
|
return Button.JsData(
|
||||||
|
js_data1="...",
|
||||||
|
js_data2=123,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
|
return Button.CssData(
|
||||||
|
css_data1="...",
|
||||||
|
css_data2=123,
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Custom types
|
||||||
|
|
||||||
|
We recommend to use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
|
||||||
|
for the `Args` class, and [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),
|
||||||
|
[dataclasses](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
|
||||||
|
or [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
|
||||||
|
for `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData` classes.
|
||||||
|
|
||||||
|
However, you can use any class, as long as they meet the conditions below.
|
||||||
|
|
||||||
|
For example, here is how you can use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
|
||||||
|
to validate the inputs at runtime.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import Component
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Table(Component):
|
||||||
|
class Kwargs(BaseModel):
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
assert isinstance(kwargs, Table.Kwargs)
|
||||||
|
|
||||||
|
Table.render(
|
||||||
|
kwargs=Table.Kwargs(name="John", age=30),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Args` class
|
||||||
|
|
||||||
|
The [`Args`](../../../reference/api#django_components.Component.Args) class
|
||||||
|
represents a list of positional arguments. It must meet two conditions:
|
||||||
|
|
||||||
|
1. The constructor for the `Args` class must accept positional arguments.
|
||||||
|
|
||||||
|
```py
|
||||||
|
Args(*args)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The `Args` instance must be convertable to a list.
|
||||||
|
|
||||||
|
```py
|
||||||
|
list(Args(1, 2, 3))
|
||||||
|
```
|
||||||
|
|
||||||
|
To implement the conversion to a list, you can implement the `__iter__()` method:
|
||||||
|
|
||||||
|
```py
|
||||||
|
class MyClass:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = 1
|
||||||
|
self.y = 2
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter([('x', self.x), ('y', self.y)])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dictionary classes
|
||||||
|
|
||||||
|
On the other hand, other types
|
||||||
|
([`Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||||
|
[`Slots`](../../../reference/api#django_components.Component.Slots),
|
||||||
|
[`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
|
||||||
|
[`JsData`](../../../reference/api#django_components.Component.JsData),
|
||||||
|
and [`CssData`](../../../reference/api#django_components.Component.CssData))
|
||||||
|
represent dictionaries. They must meet these two conditions:
|
||||||
|
|
||||||
|
1. The constructor must accept keyword arguments.
|
||||||
|
|
||||||
|
```py
|
||||||
|
Kwargs(**kwargs)
|
||||||
|
Slots(**slots)
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The instance must be convertable to a dictionary.
|
||||||
|
|
||||||
|
```py
|
||||||
|
dict(Kwargs(a=1, b=2))
|
||||||
|
dict(Slots(a=1, b=2))
|
||||||
|
```
|
||||||
|
|
||||||
|
To implement the conversion to a dictionary, you can implement either:
|
||||||
|
|
||||||
|
1. `_asdict()` method
|
||||||
|
```py
|
||||||
|
class MyClass:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = 1
|
||||||
|
self.y = 2
|
||||||
|
|
||||||
|
def _asdict(self):
|
||||||
|
return {'x': self.x, 'y': self.y}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Or make the class dict-like with `__iter__()` and `__getitem__()`
|
||||||
|
```py
|
||||||
|
class MyClass:
|
||||||
|
def __init__(self):
|
||||||
|
self.x = 1
|
||||||
|
self.y = 2
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter([('x', self.x), ('y', self.y)])
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return getattr(self, key)
|
||||||
|
```
|
||||||
|
|
||||||
## Passing variadic args and kwargs
|
## Passing variadic args and kwargs
|
||||||
|
|
||||||
You may have a function that accepts a variable number of args or kwargs:
|
You may have a component that accepts any number of args or kwargs.
|
||||||
|
|
||||||
```py
|
However, this cannot be described with the current Python's typing system (as of v0.140).
|
||||||
def get_context_data(self, *args, **kwargs):
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
This is not supported with the typed components.
|
|
||||||
|
|
||||||
As a workaround:
|
As a workaround:
|
||||||
|
|
||||||
- For a variable number of positional arguments (`*args`), set a positional argument that accepts a list of values:
|
- For a variable number of positional arguments (`*args`), set a positional argument that accepts a list of values:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
# Tuple of one member of list of strings
|
class Table(Component):
|
||||||
Args = Tuple[List[str]]
|
class Args(NamedTuple):
|
||||||
|
args: List[str]
|
||||||
|
|
||||||
|
Table.render(
|
||||||
|
args=Table.Args(args=["a", "b", "c"]),
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
- For a variable number of keyword arguments (`**kwargs`), set a keyword argument that accepts a dictionary of values:
|
- For a variable number of keyword arguments (`**kwargs`), set a keyword argument that accepts a dictionary of values:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
class Kwargs(TypedDict):
|
class Table(Component):
|
||||||
variable: str
|
class Kwargs(NamedTuple):
|
||||||
another: int
|
variable: str
|
||||||
# Pass any extra keys under `extra`
|
another: int
|
||||||
extra: Dict[str, any]
|
# Pass any extra keys under `extra`
|
||||||
|
extra: Dict[str, any]
|
||||||
|
|
||||||
|
Table.render(
|
||||||
|
kwargs=Table.Kwargs(
|
||||||
|
variable="a",
|
||||||
|
another=1,
|
||||||
|
extra={"foo": "bar"},
|
||||||
|
),
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Handling no args or no kwargs
|
## Handling no args or no kwargs
|
||||||
|
|
||||||
To declare that a component accepts no args, kwargs, etc, you can use the
|
To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the `pass` keyword:
|
||||||
[`EmptyTuple`](../../../reference/api#django_components.EmptyTuple) and
|
|
||||||
[`EmptyDict`](../../../reference/api#django_components.EmptyDict) types:
|
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from django_components import Component, EmptyDict, EmptyTuple
|
from typing import NamedTuple
|
||||||
|
from django_components import Component
|
||||||
|
|
||||||
class Button(Component[EmptyTuple, EmptyDict, EmptyDict, EmptyDict, EmptyDict, EmptyDict]):
|
class Button(Component):
|
||||||
...
|
class Args(NamedTuple):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Kwargs(NamedTuple):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Slots(NamedTuple):
|
||||||
|
pass
|
||||||
```
|
```
|
||||||
|
|
||||||
## Runtime input validation with types
|
This can get repetitive, so we added a [`Empty`](../../../reference/api#django_components.Empty) type to make it easier:
|
||||||
|
|
||||||
!!! warning
|
```py
|
||||||
|
from django_components import Component, Empty
|
||||||
|
|
||||||
Input validation was part of Django Components from version 0.96 to 0.135.
|
class Button(Component):
|
||||||
|
Args = Empty
|
||||||
|
Kwargs = Empty
|
||||||
|
Slots = Empty
|
||||||
|
```
|
||||||
|
|
||||||
Since v0.136, input validation is available as a separate extension.
|
## Runtime type validation
|
||||||
|
|
||||||
By defualt, when you add types to your component, this will only set up static type hints,
|
When you add types to your component, and implement
|
||||||
but it will not validate the inputs.
|
them as [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) or [`dataclass`](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass), the validation will check only for the presence of the attributes.
|
||||||
|
|
||||||
To enable input validation, you need to install the [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic) extension:
|
So this will not catch if you pass a string to an `int` attribute.
|
||||||
|
|
||||||
|
To enable runtime type validation, you need to use [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/), and install the [`djc-ext-pydantic`](https://github.com/django-components/djc-ext-pydantic) extension.
|
||||||
|
|
||||||
|
The `djc-ext-pydantic` extension ensures compatibility between django-components' classes such as `Component`, or `Slot` and Pydantic models.
|
||||||
|
|
||||||
|
First install the extension:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install djc-ext-pydantic
|
pip install djc-ext-pydantic
|
||||||
```
|
```
|
||||||
|
|
||||||
And add the extension to your project:
|
And then add the extension to your project:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
COMPONENTS = {
|
COMPONENTS = {
|
||||||
|
@ -255,22 +474,126 @@ COMPONENTS = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`djc-ext-pydantic` integrates [Pydantic](https://pydantic.dev/) for input and data validation. It uses the types defined on the component's class to validate inputs of Django components.
|
<!-- TODO_V1 - REMOVE IN v1 -->
|
||||||
|
|
||||||
## Usage for Python <3.11
|
## Migrating from generics to class attributes
|
||||||
|
|
||||||
On Python 3.8-3.10, use `typing_extensions`
|
In versions 0.92 to 0.139 (inclusive), the component typing was specified through generics.
|
||||||
|
|
||||||
|
Since v0.140, the types must be specified as class attributes of the [Component](../../../reference/api#django_components.Component) class -
|
||||||
|
[`Args`](../../../reference/api#django_components.Component.Args),
|
||||||
|
[`Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||||
|
[`Slots`](../../../reference/api#django_components.Component.Slots),
|
||||||
|
[`TemplateData`](../../../reference/api#django_components.Component.TemplateData),
|
||||||
|
[`JsData`](../../../reference/api#django_components.Component.JsData),
|
||||||
|
and [`CssData`](../../../reference/api#django_components.Component.CssData).
|
||||||
|
|
||||||
|
This change was necessary to make it possible to subclass components. Subclassing with generics was otherwise too complicated. Read the discussion [here](https://github.com/django-components/django-components/issues/1122).
|
||||||
|
|
||||||
|
Because of this change, the [`Component.render()`](../../../reference/api#django_components.Component.render)
|
||||||
|
method is no longer typed.
|
||||||
|
To type-check the inputs, you should wrap the inputs in [`Component.Args`](../../../reference/api#django_components.Component.Args),
|
||||||
|
[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
|
||||||
|
[`Component.Slots`](../../../reference/api#django_components.Component.Slots), etc.
|
||||||
|
|
||||||
|
For example, if you had a component like this:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing_extensions import TypedDict, NotRequired
|
from typing import NotRequired, Tuple, TypedDict
|
||||||
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
|
ButtonArgs = Tuple[int, str]
|
||||||
|
|
||||||
|
class ButtonKwargs(TypedDict):
|
||||||
|
variable: str
|
||||||
|
another: int
|
||||||
|
maybe_var: NotRequired[int] # May be omitted
|
||||||
|
|
||||||
|
class ButtonSlots(TypedDict):
|
||||||
|
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||||
|
# plain string, or a function that returns a string.
|
||||||
|
my_slot: NotRequired[SlotInput]
|
||||||
|
# Use `Slot` to allow ONLY `Slot` instances.
|
||||||
|
another_slot: Slot
|
||||||
|
|
||||||
|
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"] # Slot[MySlotData]
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
args=(1, "hello"),
|
||||||
|
kwargs={
|
||||||
|
"variable": "world",
|
||||||
|
"another": 123,
|
||||||
|
},
|
||||||
|
slots={
|
||||||
|
"my_slot": "...",
|
||||||
|
"another_slot": Slot(lambda: ...),
|
||||||
|
},
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Additionally on Python 3.8-3.9, also import `annotations`:
|
The steps to migrate are:
|
||||||
|
|
||||||
|
1. Convert all the types (`ButtonArgs`, `ButtonKwargs`, `ButtonSlots`) to subclasses
|
||||||
|
of [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
|
||||||
|
2. Move these types inside the Component class (`Button`), and rename them to `Args`, `Kwargs`, and `Slots`.
|
||||||
|
3. If you defined typing for the data methods (like [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data)), move them inside the Component class, and rename them to `TemplateData`, `JsData`, and `CssData`.
|
||||||
|
4. Remove the `Component` generic.
|
||||||
|
5. If you accessed the `args`, `kwargs`, or `slots` attributes via
|
||||||
|
[`self.input`](../../../reference/api#django_components.Component.input), you will need to add the type hints yourself, because [`self.input`](../../../reference/api#django_components.Component.input) is no longer typed.
|
||||||
|
|
||||||
|
Otherwise, you may use [`Component.get_template_data()`](../../../reference/api#django_components.Component.get_template_data) instead of [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), as `get_template_data()` receives `args`, `kwargs`, `slots` and `context` as arguments. You will still need to add the type hints yourself.
|
||||||
|
|
||||||
|
6. Lastly, you will need to update the [`Component.render()`](../../../reference/api#django_components.Component.render)
|
||||||
|
calls to wrap the inputs in [`Component.Args`](../../../reference/api#django_components.Component.Args), [`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs), and [`Component.Slots`](../../../reference/api#django_components.Component.Slots), to manually add type hints.
|
||||||
|
|
||||||
|
Thus, the code above will become:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from __future__ import annotations
|
from typing import NamedTuple, Optional
|
||||||
|
from django.template import Context
|
||||||
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
|
# The Component class does not take any generics
|
||||||
|
class Button(Component):
|
||||||
|
# Types are now defined inside the component class
|
||||||
|
class Args(NamedTuple):
|
||||||
|
size: int
|
||||||
|
text: str
|
||||||
|
|
||||||
|
class Kwargs(NamedTuple):
|
||||||
|
variable: str
|
||||||
|
another: int
|
||||||
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
|
class Slots(NamedTuple):
|
||||||
|
# Use `SlotInput` to allow slots to be given as `Slot` instance,
|
||||||
|
# plain string, or a function that returns a string.
|
||||||
|
my_slot: Optional[SlotInput] = None
|
||||||
|
# Use `Slot` to allow ONLY `Slot` instances.
|
||||||
|
another_slot: Slot
|
||||||
|
|
||||||
|
# The args, kwargs, slots are instances of the component's Args, Kwargs, and Slots classes
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
args.size # int
|
||||||
|
kwargs.variable # str
|
||||||
|
slots.my_slot # Slot[MySlotData]
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
# Wrap the inputs in the component's Args, Kwargs, and Slots classes
|
||||||
|
args=Button.Args(1, "hello"),
|
||||||
|
kwargs=Button.Kwargs(
|
||||||
|
variable="world",
|
||||||
|
another=123,
|
||||||
|
),
|
||||||
|
slots=Button.Slots(
|
||||||
|
my_slot="...",
|
||||||
|
another_slot=Slot(lambda: ...),
|
||||||
|
),
|
||||||
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
Moreover, on 3.10 and less, you may not be able to use `NotRequired`, and instead you will need to mark either all keys are required, or all keys as optional, using TypeDict's `total` kwarg.
|
|
||||||
|
|
||||||
[See PEP-655](https://peps.python.org/pep-0655) for more info.
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ Example:
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
def get_context_data(self, *args, **attrs):
|
def get_context_data(self, *args, **attrs):
|
||||||
# Access component's ID
|
# Access component's ID
|
||||||
assert self.id == "djc1A2b3c"
|
assert self.id == "c1A2b3c"
|
||||||
|
|
||||||
# Access component's inputs, slots and context
|
# Access component's inputs, slots and context
|
||||||
assert self.input.args == (123, "str")
|
assert self.input.args == (123, "str")
|
||||||
|
@ -66,7 +66,7 @@ If you need to expand this limit, please open an issue on GitHub.
|
||||||
class Table(Component):
|
class Table(Component):
|
||||||
def get_context_data(self, *args, **attrs):
|
def get_context_data(self, *args, **attrs):
|
||||||
# Access component's ID
|
# Access component's ID
|
||||||
assert self.id == "djc1A2b3c"
|
assert self.id == "c1A2b3c"
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
```
|
```
|
||||||
|
|
|
@ -379,52 +379,53 @@ class Header(Component):
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Static type hints
|
### Input validation and static type hints
|
||||||
|
|
||||||
Components API is fully typed, and supports [static type hints](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/).
|
Avoid needless errors with [type hints and runtime input validation](https://django-components.github.io/django-components/latest/concepts/advanced/typing_and_validation/).
|
||||||
|
|
||||||
To opt-in to static type hints, define types for component's args, kwargs, slots, and more:
|
To opt-in to input validation, define types for component's args, kwargs, slots, and more:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, Slot
|
from typing import NamedTuple, Optional
|
||||||
|
from django.template import Context
|
||||||
|
from django_components import Component, Slot, SlotInput
|
||||||
|
|
||||||
from django_components import Component
|
class Button(Component):
|
||||||
|
class Args(NamedTuple):
|
||||||
|
size: int
|
||||||
|
text: str
|
||||||
|
|
||||||
ButtonArgs = Tuple[int, str]
|
class Kwargs(NamedTuple):
|
||||||
|
variable: str
|
||||||
|
another: int
|
||||||
|
maybe_var: Optional[int] = None # May be omitted
|
||||||
|
|
||||||
class ButtonKwargs(TypedDict):
|
class Slots(NamedTuple):
|
||||||
variable: str
|
my_slot: Optional[SlotInput] = None
|
||||||
another: int
|
another_slot: SlotInput
|
||||||
maybe_var: NotRequired[int] # May be omitted
|
|
||||||
|
|
||||||
class ButtonSlots(TypedDict):
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
# Use `Slot` for slot functions.
|
args.size # int
|
||||||
my_slot: NotRequired[Slot]
|
kwargs.variable # str
|
||||||
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
slots.my_slot # Slot[MySlotData]
|
||||||
another_slot: SlotContent
|
|
||||||
|
|
||||||
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"] # Slot
|
|
||||||
```
|
```
|
||||||
|
|
||||||
When you then call
|
To have type hints when calling
|
||||||
[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or
|
[`Button.render()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render) or
|
||||||
[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response),
|
[`Button.render_to_response()`](https://django-components.github.io/django-components/latest/reference/api/#django_components.Component.render_to_response),
|
||||||
you will get type hints:
|
wrap the inputs in their respective `Args`, `Kwargs`, and `Slots` classes:
|
||||||
|
|
||||||
```py
|
```py
|
||||||
Button.render(
|
Button.render(
|
||||||
# Error: First arg must be `int`, got `float`
|
# Error: First arg must be `int`, got `float`
|
||||||
args=(1.25, "abc"),
|
args=Button.Args(
|
||||||
|
size=1.25,
|
||||||
|
text="abc",
|
||||||
|
),
|
||||||
# Error: Key "another" is missing
|
# Error: Key "another" is missing
|
||||||
kwargs={
|
kwargs=Button.Kwargs(
|
||||||
"variable": "text",
|
variable="text",
|
||||||
},
|
),
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -71,11 +71,7 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.EmptyDict
|
::: django_components.Empty
|
||||||
options:
|
|
||||||
show_if_no_docstring: true
|
|
||||||
|
|
||||||
::: django_components.EmptyTuple
|
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
@ -95,6 +91,10 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.SlotInput
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.SlotRef
|
::: django_components.SlotRef
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
|
@ -54,7 +54,8 @@ python manage.py components ext run <extension> <command>
|
||||||
## `components create`
|
## `components create`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
|
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||||
|
[--dry-run]
|
||||||
name
|
name
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -237,7 +238,7 @@ List all extensions.
|
||||||
- `--columns COLUMNS`
|
- `--columns COLUMNS`
|
||||||
- Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`.
|
- Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`.
|
||||||
- `-s`, `--simple`
|
- `-s`, `--simple`
|
||||||
- Only show table data, without headers. Use this option for generating machine-readable output.
|
- Only show table data, without headers. Use this option for generating machine- readable output.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -400,7 +401,7 @@ List all components created in this project.
|
||||||
- `--columns COLUMNS`
|
- `--columns COLUMNS`
|
||||||
- Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`.
|
- Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`.
|
||||||
- `-s`, `--simple`
|
- `-s`, `--simple`
|
||||||
- Only show table data, without headers. Use this option for generating machine-readable output.
|
- Only show table data, without headers. Use this option for generating machine- readable output.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -463,7 +464,8 @@ ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAc
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||||
|
[--skip-checks]
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -507,9 +509,10 @@ Deprecated. Use `components upgrade` instead.
|
||||||
## `startcomponent`
|
## `startcomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
|
||||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||||
|
[--skip-checks]
|
||||||
name
|
name
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -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#L1066" 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#L1088" 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#L1655" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L2622" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ can access only the data that was explicitly passed to it:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L614" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L648" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ user = self.inject("user_data")["user"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L155" target="_blank">See source code</a>
|
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L189" target="_blank">See source code</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ from django_components.extensions.view import ComponentView
|
||||||
from django_components.extensions.url import ComponentUrl, get_component_url
|
from django_components.extensions.url import ComponentUrl, get_component_url
|
||||||
from django_components.library import TagProtectedError
|
from django_components.library import TagProtectedError
|
||||||
from django_components.node import BaseNode, template_tag
|
from django_components.node import BaseNode, template_tag
|
||||||
from django_components.slots import SlotContent, Slot, SlotFunc, SlotRef, SlotResult
|
from django_components.slots import Slot, SlotContent, SlotFunc, SlotInput, SlotRef, SlotResult
|
||||||
from django_components.tag_formatter import (
|
from django_components.tag_formatter import (
|
||||||
ComponentFormatter,
|
ComponentFormatter,
|
||||||
ShorthandComponentFormatter,
|
ShorthandComponentFormatter,
|
||||||
|
@ -65,7 +65,7 @@ from django_components.template import cached_template
|
||||||
import django_components.types as types
|
import django_components.types as types
|
||||||
from django_components.util.loader import ComponentFileEntry, get_component_dirs, get_component_files
|
from django_components.util.loader import ComponentFileEntry, get_component_dirs, get_component_files
|
||||||
from django_components.util.routing import URLRoute, URLRouteHandler
|
from django_components.util.routing import URLRoute, URLRouteHandler
|
||||||
from django_components.util.types import EmptyTuple, EmptyDict
|
from django_components.util.types import Empty
|
||||||
|
|
||||||
# isort: on
|
# isort: on
|
||||||
|
|
||||||
|
@ -103,8 +103,7 @@ __all__ = [
|
||||||
"ContextBehavior",
|
"ContextBehavior",
|
||||||
"Default",
|
"Default",
|
||||||
"DynamicComponent",
|
"DynamicComponent",
|
||||||
"EmptyTuple",
|
"Empty",
|
||||||
"EmptyDict",
|
|
||||||
"format_attributes",
|
"format_attributes",
|
||||||
"get_component_by_class_id",
|
"get_component_by_class_id",
|
||||||
"get_component_dirs",
|
"get_component_dirs",
|
||||||
|
@ -126,9 +125,10 @@ __all__ = [
|
||||||
"RegistrySettings",
|
"RegistrySettings",
|
||||||
"render_dependencies",
|
"render_dependencies",
|
||||||
"ShorthandComponentFormatter",
|
"ShorthandComponentFormatter",
|
||||||
"SlotContent",
|
|
||||||
"Slot",
|
"Slot",
|
||||||
|
"SlotContent",
|
||||||
"SlotFunc",
|
"SlotFunc",
|
||||||
|
"SlotInput",
|
||||||
"SlotRef",
|
"SlotRef",
|
||||||
"SlotResult",
|
"SlotResult",
|
||||||
"TagFormatterABC",
|
"TagFormatterABC",
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Type, Union
|
from typing import TYPE_CHECKING, Callable, Dict, List, NamedTuple, Optional, Set, Type, TypeVar, Union
|
||||||
from weakref import ReferenceType, finalize
|
from weakref import ReferenceType, finalize
|
||||||
|
|
||||||
from django.template import Library
|
from django.template import Library
|
||||||
|
@ -18,15 +18,7 @@ from django_components.tag_formatter import TagFormatterABC, get_tag_formatter
|
||||||
from django_components.util.weakref import cached_ref
|
from django_components.util.weakref import cached_ref
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components.component import (
|
from django_components.component import Component
|
||||||
ArgsType,
|
|
||||||
Component,
|
|
||||||
CssDataType,
|
|
||||||
DataType,
|
|
||||||
JsDataType,
|
|
||||||
KwargsType,
|
|
||||||
SlotsType,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||||
|
@ -36,6 +28,9 @@ else:
|
||||||
AllRegistries = List[ReferenceType]
|
AllRegistries = List[ReferenceType]
|
||||||
|
|
||||||
|
|
||||||
|
TComponent = TypeVar("TComponent", bound="Component")
|
||||||
|
|
||||||
|
|
||||||
class AlreadyRegistered(Exception):
|
class AlreadyRegistered(Exception):
|
||||||
"""
|
"""
|
||||||
Raised when you try to register a [Component](../api#django_components#Component),
|
Raised when you try to register a [Component](../api#django_components#Component),
|
||||||
|
@ -264,6 +259,10 @@ class ComponentRegistry:
|
||||||
)
|
)
|
||||||
|
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
|
# Skip if `extensions` was deleted before this registry
|
||||||
|
if not extensions:
|
||||||
|
return
|
||||||
|
|
||||||
extensions.on_registry_deleted(
|
extensions.on_registry_deleted(
|
||||||
OnRegistryDeletedContext(
|
OnRegistryDeletedContext(
|
||||||
registry=self,
|
registry=self,
|
||||||
|
@ -615,8 +614,8 @@ _the_registry = registry
|
||||||
|
|
||||||
|
|
||||||
def register(name: str, registry: Optional[ComponentRegistry] = None) -> Callable[
|
def register(name: str, registry: Optional[ComponentRegistry] = None) -> Callable[
|
||||||
[Type["Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]"]],
|
[Type[TComponent]],
|
||||||
Type["Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]"],
|
Type[TComponent],
|
||||||
]:
|
]:
|
||||||
"""
|
"""
|
||||||
Class decorator for registering a [component](./#django_components.Component)
|
Class decorator for registering a [component](./#django_components.Component)
|
||||||
|
@ -661,9 +660,7 @@ def register(name: str, registry: Optional[ComponentRegistry] = None) -> Callabl
|
||||||
if registry is None:
|
if registry is None:
|
||||||
registry = _the_registry
|
registry = _the_registry
|
||||||
|
|
||||||
def decorator(
|
def decorator(component: Type[TComponent]) -> Type[TComponent]:
|
||||||
component: Type["Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]"],
|
|
||||||
) -> Type["Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]"]:
|
|
||||||
registry.register(name=name, component=component)
|
registry.register(name=name, component=component)
|
||||||
return component
|
return component
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ from typing import (
|
||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
Literal,
|
Literal,
|
||||||
|
Mapping,
|
||||||
Optional,
|
Optional,
|
||||||
Sequence,
|
Sequence,
|
||||||
Set,
|
Set,
|
||||||
|
@ -134,7 +135,7 @@ def cache_component_js(comp_cls: Type["Component"]) -> None:
|
||||||
# with `Components.manager.registerComponentData`.
|
# with `Components.manager.registerComponentData`.
|
||||||
# 3. Actually run a component's JS instance with `Components.manager.callComponent`,
|
# 3. Actually run a component's JS instance with `Components.manager.callComponent`,
|
||||||
# specifying the components HTML elements with `component_id`, and JS vars with `input_hash`.
|
# specifying the components HTML elements with `component_id`, and JS vars with `input_hash`.
|
||||||
def cache_component_js_vars(comp_cls: Type["Component"], js_vars: Dict) -> Optional[str]:
|
def cache_component_js_vars(comp_cls: Type["Component"], js_vars: Mapping) -> Optional[str]:
|
||||||
if not is_nonempty_str(comp_cls.js):
|
if not is_nonempty_str(comp_cls.js):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -184,7 +185,7 @@ def cache_component_css(comp_cls: Type["Component"]) -> None:
|
||||||
# the CSS vars under the CSS selector `[data-djc-css-a1b2c3]`. We define the stylesheet
|
# the CSS vars under the CSS selector `[data-djc-css-a1b2c3]`. We define the stylesheet
|
||||||
# with variables separately from `Component.css`, because different instances may return different
|
# with variables separately from `Component.css`, because different instances may return different
|
||||||
# data from `get_css_data()`, which will live in different stylesheets.
|
# data from `get_css_data()`, which will live in different stylesheets.
|
||||||
def cache_component_css_vars(comp_cls: Type["Component"], css_vars: Dict) -> Optional[str]:
|
def cache_component_css_vars(comp_cls: Type["Component"], css_vars: Mapping) -> Optional[str]:
|
||||||
if not is_nonempty_str(comp_cls.css):
|
if not is_nonempty_str(comp_cls.css):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -125,7 +125,10 @@ class ComponentDefaults(ComponentExtension.ExtensionClass): # type: ignore[misc
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import Component, Default
|
from django_components import Component, Default
|
||||||
|
|
||||||
|
@ -133,6 +136,7 @@ class ComponentDefaults(ComponentExtension.ExtensionClass): # type: ignore[misc
|
||||||
class Defaults:
|
class Defaults:
|
||||||
position = "left"
|
position = "left"
|
||||||
selected_items = Default(lambda: [1, 2, 3])
|
selected_items = Default(lambda: [1, 2, 3])
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -17,17 +17,25 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
||||||
"""
|
"""
|
||||||
The interface for `Component.View`.
|
The interface for `Component.View`.
|
||||||
|
|
||||||
Override the methods of this class to define the behavior of the component.
|
The fields of this class are used to configure the component views and URLs.
|
||||||
|
|
||||||
This class is a subclass of `django.views.View`. The `Component` instance is available
|
This class is a subclass of
|
||||||
|
[`django.views.View`](https://docs.djangoproject.com/en/5.2/ref/class-based-views/base/#view).
|
||||||
|
The [`Component`](../api#django_components.Component) instance is available
|
||||||
via `self.component`.
|
via `self.component`.
|
||||||
|
|
||||||
|
Override the methods of this class to define the behavior of the component.
|
||||||
|
|
||||||
|
Read more about [Component views and URLs](../../concepts/fundamentals/component_views_urls).
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```python
|
```python
|
||||||
class MyComponent(Component):
|
class MyComponent(Component):
|
||||||
class View:
|
class View:
|
||||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
return HttpResponse("Hello, world!")
|
return HttpResponse("Hello, world!")
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# NOTE: This attribute must be declared on the class for `View.as_view()` to allow
|
# NOTE: This attribute must be declared on the class for `View.as_view()` to allow
|
||||||
|
|
|
@ -248,7 +248,7 @@ def component_post_render(
|
||||||
# This is where we actually render the component
|
# This is where we actually render the component
|
||||||
#
|
#
|
||||||
# NOTE: [1:] because the root component will be yet again added to the error's
|
# NOTE: [1:] because the root component will be yet again added to the error's
|
||||||
# `components` list in `_render` so we remove the first element from the path.
|
# `components` list in `_render_with_error_trace` so we remove the first element from the path.
|
||||||
with component_error_message(full_path[1:]):
|
with component_error_message(full_path[1:]):
|
||||||
curr_comp_content, grandchild_component_attrs = curr_comp_renderer(curr_comp_attrs)
|
curr_comp_content, grandchild_component_attrs = curr_comp_renderer(curr_comp_attrs)
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,41 @@ class Slot(Generic[TSlotData]):
|
||||||
|
|
||||||
# NOTE: This must be defined here, so we don't have any forward references
|
# NOTE: This must be defined here, so we don't have any forward references
|
||||||
# otherwise Pydantic has problem resolving the types.
|
# otherwise Pydantic has problem resolving the types.
|
||||||
SlotContent = Union[SlotResult, SlotFunc[TSlotData], Slot[TSlotData]]
|
SlotInput = Union[SlotResult, SlotFunc[TSlotData], Slot[TSlotData]]
|
||||||
|
"""
|
||||||
|
When rendering a component with [`Component.render()`](../api#django_components.Component.render)
|
||||||
|
or [`Component.render_to_response()`](../api#django_components.Component.render_to_response),
|
||||||
|
the slots may be given a strings, functions, or [`Slot`](../api#django_components.Slot) instances.
|
||||||
|
This type describes that union.
|
||||||
|
|
||||||
|
Use this type when typing the slots in your component.
|
||||||
|
|
||||||
|
`SlotInput` accepts an optional type parameter to specify the data dictionary that will be passed to the
|
||||||
|
slot content function.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import NamedTuple
|
||||||
|
from typing_extensions import TypedDict
|
||||||
|
from django_components import Component, SlotInput
|
||||||
|
|
||||||
|
class TableFooterSlotData(TypedDict):
|
||||||
|
page_number: int
|
||||||
|
|
||||||
|
class Table(Component):
|
||||||
|
class Slots(NamedTuple):
|
||||||
|
header: SlotInput
|
||||||
|
footer: SlotInput[TableFooterSlotData]
|
||||||
|
|
||||||
|
template = "<div>{% slot 'footer' %}</div>"
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
# TODO_V1 - REMOVE, superseded by SlotInput
|
||||||
|
SlotContent = SlotInput[TSlotData]
|
||||||
|
"""
|
||||||
|
DEPRECATED: Use [`SlotInput`](../api#django_components.SlotInput) instead. Will be removed in v1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
# Internal type aliases
|
# Internal type aliases
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Helper types for IDEs."""
|
"""Helper types for IDEs."""
|
||||||
|
|
||||||
from django_components.util.types import Annotated
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
css = Annotated[str, "css"]
|
css = Annotated[str, "css"]
|
||||||
django_html = Annotated[str, "django_html"]
|
django_html = Annotated[str, "django_html"]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from dataclasses import asdict, is_dataclass
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
@ -130,3 +131,19 @@ def is_glob(filepath: str) -> bool:
|
||||||
|
|
||||||
def flatten(lst: Iterable[Iterable[T]]) -> List[T]:
|
def flatten(lst: Iterable[Iterable[T]]) -> List[T]:
|
||||||
return list(chain.from_iterable(lst))
|
return list(chain.from_iterable(lst))
|
||||||
|
|
||||||
|
|
||||||
|
def to_dict(data: Any) -> dict:
|
||||||
|
"""
|
||||||
|
Convert object to a dict.
|
||||||
|
|
||||||
|
Handles `dict`, `NamedTuple`, and `dataclass`.
|
||||||
|
"""
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
elif hasattr(data, "_asdict"): # Case: NamedTuple
|
||||||
|
return data._asdict()
|
||||||
|
elif is_dataclass(data): # Case: dataclass
|
||||||
|
return asdict(data) # type: ignore[arg-type]
|
||||||
|
|
||||||
|
return dict(data)
|
||||||
|
|
|
@ -1,144 +1,30 @@
|
||||||
import sys
|
from typing import NamedTuple
|
||||||
import typing
|
|
||||||
from typing import Any, Tuple
|
|
||||||
|
|
||||||
# See https://peps.python.org/pep-0655/#usage-in-python-3-11
|
|
||||||
# NOTE: Pydantic requires typing_extensions.TypedDict until (not incl) 3.12
|
|
||||||
if sys.version_info >= (3, 12):
|
|
||||||
from typing import TypedDict
|
|
||||||
else:
|
|
||||||
from typing_extensions import TypedDict as TypedDict
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import Annotated # type: ignore
|
|
||||||
except ImportError:
|
|
||||||
|
|
||||||
@typing.no_type_check
|
|
||||||
class Annotated: # type: ignore
|
|
||||||
def __init__(self, type_: str, *args: Any, **kwargs: Any):
|
|
||||||
self.type_ = type_
|
|
||||||
self.metadata = args, kwargs
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"Annotated[{self.type_}, {self.metadata[0]!r}, {self.metadata[1]!r}]"
|
|
||||||
|
|
||||||
def __getitem__(self, params: Any) -> "Annotated[Any, Any, Any]": # type: ignore
|
|
||||||
if not isinstance(params, tuple):
|
|
||||||
params = (params,)
|
|
||||||
return Annotated(self.type_, *params, **self.metadata[1]) # type: ignore
|
|
||||||
|
|
||||||
def __class_getitem__(self, *params: Any) -> "Annotated[Any, Any, Any]": # type: ignore
|
|
||||||
return Annotated(*params) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
EmptyTuple = Tuple[()]
|
class Empty(NamedTuple):
|
||||||
"""
|
|
||||||
Tuple with no members.
|
|
||||||
|
|
||||||
You can use this to define a [Component](../api#django_components.Component)
|
|
||||||
that accepts NO positional arguments:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from django_components import Component, EmptyTuple
|
|
||||||
|
|
||||||
class Table(Component(EmptyTuple, Any, Any, Any, Any, Any))
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
After that, when you call [`Component.render()`](../api#django_components.Component.render)
|
|
||||||
or [`Component.render_to_response()`](../api#django_components.Component.render_to_response),
|
|
||||||
the `args` parameter will raise type error if `args` is anything else than an empty
|
|
||||||
tuple.
|
|
||||||
|
|
||||||
```python
|
|
||||||
Table.render(
|
|
||||||
args: (),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Omitting `args` is also fine:
|
|
||||||
|
|
||||||
```python
|
|
||||||
Table.render()
|
|
||||||
```
|
|
||||||
|
|
||||||
Other values are not allowed. This will raise an error with MyPy:
|
|
||||||
|
|
||||||
```python
|
|
||||||
Table.render(
|
|
||||||
args: ("one", 2, "three"),
|
|
||||||
)
|
|
||||||
```
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyDict(TypedDict):
|
|
||||||
"""
|
"""
|
||||||
TypedDict with no members.
|
Type for an object with no members.
|
||||||
|
|
||||||
You can use this to define a [Component](../api#django_components.Component)
|
You can use this to define [Component](../api#django_components.Component)
|
||||||
that accepts NO kwargs, or NO slots, or returns NO data from
|
types that accept NO args, kwargs, slots, etc:
|
||||||
[`Component.get_context_data()`](../api#django_components.Component.get_context_data)
|
|
||||||
/
|
|
||||||
[`Component.get_js_data()`](../api#django_components.Component.get_js_data)
|
|
||||||
/
|
|
||||||
[`Component.get_css_data()`](../api#django_components.Component.get_css_data):
|
|
||||||
|
|
||||||
Accepts NO kwargs:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from django_components import Component, EmptyDict
|
from django_components import Component, Empty
|
||||||
|
|
||||||
class Table(Component(Any, EmptyDict, Any, Any, Any, Any))
|
class Table(Component):
|
||||||
|
Args = Empty
|
||||||
|
Kwargs = Empty
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
Accepts NO slots:
|
This class is a shorthand for:
|
||||||
|
|
||||||
```python
|
```py
|
||||||
from django_components import Component, EmptyDict
|
class Empty(NamedTuple):
|
||||||
|
pass
|
||||||
class Table(Component(Any, Any, EmptyDict, Any, Any, Any))
|
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Returns NO data from `get_context_data()`:
|
Read more about [Typing and validation](../../concepts/advanced/typing_and_validation).
|
||||||
|
|
||||||
```python
|
|
||||||
from django_components import Component, EmptyDict
|
|
||||||
|
|
||||||
class Table(Component(Any, Any, Any, EmptyDict, Any, Any))
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Going back to the example with NO kwargs, when you then call
|
|
||||||
[`Component.render()`](../api#django_components.Component.render)
|
|
||||||
or [`Component.render_to_response()`](../api#django_components.Component.render_to_response),
|
|
||||||
the `kwargs` parameter will raise type error if `kwargs` is anything else than an empty
|
|
||||||
dict.
|
|
||||||
|
|
||||||
```python
|
|
||||||
Table.render(
|
|
||||||
kwargs: {},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
Omitting `kwargs` is also fine:
|
|
||||||
|
|
||||||
```python
|
|
||||||
Table.render()
|
|
||||||
```
|
|
||||||
|
|
||||||
Other values are not allowed. This will raise an error with MyPy:
|
|
||||||
|
|
||||||
```python
|
|
||||||
Table.render(
|
|
||||||
kwargs: {
|
|
||||||
"one": 2,
|
|
||||||
"three": 4,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
```
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -85,10 +85,10 @@ class TestComponentMediaCache:
|
||||||
<div>Template only component</div>
|
<div>Template only component</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_js_data(self):
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_css_data(self):
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@register("test_media_no_vars")
|
@register("test_media_no_vars")
|
||||||
|
@ -100,10 +100,10 @@ class TestComponentMediaCache:
|
||||||
js = "console.log('Hello from JS');"
|
js = "console.log('Hello from JS');"
|
||||||
css = ".novars-component { color: blue; }"
|
css = ".novars-component { color: blue; }"
|
||||||
|
|
||||||
def get_js_data(self):
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_css_data(self):
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
class TestMediaAndVarsComponent(Component):
|
class TestMediaAndVarsComponent(Component):
|
||||||
|
@ -114,10 +114,10 @@ class TestComponentMediaCache:
|
||||||
js = "console.log('Hello from full component');"
|
js = "console.log('Hello from full component');"
|
||||||
css = ".full-component { color: blue; }"
|
css = ".full-component { color: blue; }"
|
||||||
|
|
||||||
def get_js_data(self):
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
return {"message": "Hello"}
|
return {"message": "Hello"}
|
||||||
|
|
||||||
def get_css_data(self):
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
return {"color": "blue"}
|
return {"color": "blue"}
|
||||||
|
|
||||||
# Register our test cache
|
# Register our test cache
|
||||||
|
|
|
@ -4,8 +4,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, Tuple, no_type_check
|
from typing import Any, Dict, no_type_check
|
||||||
from typing_extensions import NotRequired, TypedDict
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -20,8 +19,6 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
||||||
from django_components import (
|
from django_components import (
|
||||||
Component,
|
Component,
|
||||||
ComponentView,
|
ComponentView,
|
||||||
SlotContent,
|
|
||||||
Slot,
|
|
||||||
all_components,
|
all_components,
|
||||||
get_component_by_class_id,
|
get_component_by_class_id,
|
||||||
register,
|
register,
|
||||||
|
@ -385,100 +382,6 @@ class TestComponent:
|
||||||
|
|
||||||
assert SimpleComponent.render() == "Hello"
|
assert SimpleComponent.render() == "Hello"
|
||||||
|
|
||||||
def test_typing(self):
|
|
||||||
# Types
|
|
||||||
ButtonArgs = Tuple[str, ...]
|
|
||||||
|
|
||||||
class ButtonKwargs(TypedDict):
|
|
||||||
name: str
|
|
||||||
age: int
|
|
||||||
maybe_var: NotRequired[int]
|
|
||||||
|
|
||||||
class ButtonFooterSlotData(TypedDict):
|
|
||||||
value: int
|
|
||||||
|
|
||||||
class ButtonSlots(TypedDict):
|
|
||||||
# Use `SlotContent` when you want to allow either function (`Slot` instance)
|
|
||||||
# or plain string.
|
|
||||||
header: SlotContent
|
|
||||||
# Use `Slot` for slot functions. The generic specifies the data available to the slot function.
|
|
||||||
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Tests - We simply check that these don't raise any errors
|
|
||||||
# nor any type errors.
|
|
||||||
ButtonType1 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData, ButtonCssData]
|
|
||||||
ButtonType2 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData, ButtonJsData]
|
|
||||||
ButtonType3 = Component[ButtonArgs, ButtonKwargs, ButtonSlots, ButtonData]
|
|
||||||
ButtonType4 = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
|
||||||
ButtonType5 = Component[ButtonArgs, ButtonKwargs]
|
|
||||||
ButtonType6 = Component[ButtonArgs]
|
|
||||||
|
|
||||||
class Button1(ButtonType1):
|
|
||||||
template = "<button>Click me!</button>"
|
|
||||||
|
|
||||||
class Button2(ButtonType2):
|
|
||||||
template = "<button>Click me!</button>"
|
|
||||||
|
|
||||||
class Button3(ButtonType3):
|
|
||||||
template = "<button>Click me!</button>"
|
|
||||||
|
|
||||||
class Button4(ButtonType4):
|
|
||||||
template = "<button>Click me!</button>"
|
|
||||||
|
|
||||||
class Button5(ButtonType5):
|
|
||||||
template = "<button>Click me!</button>"
|
|
||||||
|
|
||||||
class Button6(ButtonType6):
|
|
||||||
template = "<button>Click me!</button>"
|
|
||||||
|
|
||||||
Button1.render(
|
|
||||||
args=("arg1", "arg2"),
|
|
||||||
kwargs={"name": "name", "age": 123},
|
|
||||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
|
||||||
)
|
|
||||||
|
|
||||||
Button2.render(
|
|
||||||
args=("arg1", "arg2"),
|
|
||||||
kwargs={"name": "name", "age": 123},
|
|
||||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
|
||||||
)
|
|
||||||
|
|
||||||
Button3.render(
|
|
||||||
args=("arg1", "arg2"),
|
|
||||||
kwargs={"name": "name", "age": 123},
|
|
||||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
|
||||||
)
|
|
||||||
|
|
||||||
Button4.render(
|
|
||||||
args=("arg1", "arg2"),
|
|
||||||
kwargs={"name": "name", "age": 123},
|
|
||||||
slots={"header": "HEADER", "footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER")},
|
|
||||||
)
|
|
||||||
|
|
||||||
Button5.render(
|
|
||||||
args=("arg1", "arg2"),
|
|
||||||
kwargs={"name": "name", "age": 123},
|
|
||||||
)
|
|
||||||
|
|
||||||
Button6.render(
|
|
||||||
args=("arg1", "arg2"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
class TestComponentRender:
|
class TestComponentRender:
|
||||||
|
|
|
@ -32,7 +32,7 @@ class TestComponentDefaults:
|
||||||
# Check that args and slots are NOT affected by the defaults
|
# Check that args and slots are NOT affected by the defaults
|
||||||
assert self.input.args == [123]
|
assert self.input.args == [123]
|
||||||
assert [*self.input.slots.keys()] == ["my_slot"]
|
assert [*self.input.slots.keys()] == ["my_slot"]
|
||||||
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT"
|
assert self.input.slots["my_slot"](Context(), None, None) == "MY_SLOT" # type: ignore[arg-type]
|
||||||
|
|
||||||
assert self.input.kwargs == {
|
assert self.input.kwargs == {
|
||||||
"variable": "test", # User-given
|
"variable": "test", # User-given
|
||||||
|
|
615
tests/test_component_typing.py
Normal file
615
tests/test_component_typing.py
Normal file
|
@ -0,0 +1,615 @@
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
from typing_extensions import NotRequired, TypedDict
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.template import Context
|
||||||
|
|
||||||
|
from django_components import Component, Empty, Slot, SlotInput
|
||||||
|
|
||||||
|
from django_components.testing import djc_test
|
||||||
|
from .testutils import setup_test_config
|
||||||
|
|
||||||
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestComponentTyping:
|
||||||
|
def test_data_methods_input_typed(self):
|
||||||
|
template_called = False
|
||||||
|
js_called = False
|
||||||
|
css_called = False
|
||||||
|
|
||||||
|
class ButtonFooterSlotData(TypedDict):
|
||||||
|
value: int
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
class Args(NamedTuple):
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Kwargs:
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
maybe_var: Optional[int] = None
|
||||||
|
|
||||||
|
class Slots(TypedDict):
|
||||||
|
# Use `SlotInput` when you want to pass slot as string
|
||||||
|
header: SlotInput
|
||||||
|
# Use `Slot` for slot functions.
|
||||||
|
# The generic specifies the data available to the slot function
|
||||||
|
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||||
|
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
nonlocal template_called
|
||||||
|
template_called = True
|
||||||
|
|
||||||
|
assert isinstance(args, Button.Args)
|
||||||
|
assert isinstance(kwargs, Button.Kwargs)
|
||||||
|
assert isinstance(slots, dict)
|
||||||
|
assert isinstance(context, Context)
|
||||||
|
|
||||||
|
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
nonlocal js_called
|
||||||
|
js_called = True
|
||||||
|
|
||||||
|
assert isinstance(args, Button.Args)
|
||||||
|
assert isinstance(kwargs, Button.Kwargs)
|
||||||
|
assert isinstance(slots, dict)
|
||||||
|
assert isinstance(context, Context)
|
||||||
|
|
||||||
|
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
nonlocal css_called
|
||||||
|
css_called = True
|
||||||
|
|
||||||
|
assert isinstance(args, Button.Args)
|
||||||
|
assert isinstance(kwargs, Button.Kwargs)
|
||||||
|
assert isinstance(slots, dict)
|
||||||
|
assert isinstance(context, Context)
|
||||||
|
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||||
|
kwargs=Button.Kwargs(name="name", age=123),
|
||||||
|
slots=Button.Slots(
|
||||||
|
header="HEADER",
|
||||||
|
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert template_called
|
||||||
|
assert js_called
|
||||||
|
assert css_called
|
||||||
|
|
||||||
|
def test_data_methods_input_not_typed_by_default(self):
|
||||||
|
template_called = False
|
||||||
|
js_called = False
|
||||||
|
css_called = False
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal template_called
|
||||||
|
template_called = True
|
||||||
|
|
||||||
|
assert isinstance(args, list)
|
||||||
|
assert isinstance(kwargs, dict)
|
||||||
|
assert isinstance(slots, dict)
|
||||||
|
assert isinstance(context, Context)
|
||||||
|
|
||||||
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal js_called
|
||||||
|
js_called = True
|
||||||
|
|
||||||
|
assert isinstance(args, list)
|
||||||
|
assert isinstance(kwargs, dict)
|
||||||
|
assert isinstance(slots, dict)
|
||||||
|
assert isinstance(context, Context)
|
||||||
|
|
||||||
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal css_called
|
||||||
|
css_called = True
|
||||||
|
|
||||||
|
assert isinstance(args, list)
|
||||||
|
assert isinstance(kwargs, dict)
|
||||||
|
assert isinstance(slots, dict)
|
||||||
|
assert isinstance(context, Context)
|
||||||
|
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
args=["arg1", "arg2"],
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
slots={
|
||||||
|
"header": "HEADER",
|
||||||
|
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert template_called
|
||||||
|
assert js_called
|
||||||
|
assert css_called
|
||||||
|
|
||||||
|
def test_data_methods_output_typed(self):
|
||||||
|
template_called = False
|
||||||
|
js_called = False
|
||||||
|
css_called = False
|
||||||
|
|
||||||
|
template_data_instance = None
|
||||||
|
js_data_instance = None
|
||||||
|
css_data_instance = None
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
# Data returned from `get_context_data`
|
||||||
|
@dataclass
|
||||||
|
class TemplateData:
|
||||||
|
data1: str
|
||||||
|
data2: int
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
nonlocal template_data_instance
|
||||||
|
template_data_instance = self
|
||||||
|
|
||||||
|
# Data returned from `get_js_data`
|
||||||
|
@dataclass
|
||||||
|
class JsData:
|
||||||
|
js_data1: str
|
||||||
|
js_data2: int
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
nonlocal js_data_instance
|
||||||
|
js_data_instance = self
|
||||||
|
|
||||||
|
# Data returned from `get_css_data`
|
||||||
|
@dataclass
|
||||||
|
class CssData:
|
||||||
|
css_data1: str
|
||||||
|
css_data2: int
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
nonlocal css_data_instance
|
||||||
|
css_data_instance = self
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal template_called
|
||||||
|
template_called = True
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data1": "data1",
|
||||||
|
"data2": 123,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal js_called
|
||||||
|
js_called = True
|
||||||
|
|
||||||
|
return {
|
||||||
|
"js_data1": "js_data1",
|
||||||
|
"js_data2": 123,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal css_called
|
||||||
|
css_called = True
|
||||||
|
|
||||||
|
return {
|
||||||
|
"css_data1": "css_data1",
|
||||||
|
"css_data2": 123,
|
||||||
|
}
|
||||||
|
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
args=["arg1", "arg2"],
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
slots={
|
||||||
|
"header": "HEADER",
|
||||||
|
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert template_called
|
||||||
|
assert js_called
|
||||||
|
assert css_called
|
||||||
|
|
||||||
|
assert isinstance(template_data_instance, Button.TemplateData)
|
||||||
|
assert isinstance(js_data_instance, Button.JsData)
|
||||||
|
assert isinstance(css_data_instance, Button.CssData)
|
||||||
|
|
||||||
|
assert template_data_instance == Button.TemplateData(data1="data1", data2=123)
|
||||||
|
assert js_data_instance == Button.JsData(js_data1="js_data1", js_data2=123)
|
||||||
|
assert css_data_instance == Button.CssData(css_data1="css_data1", css_data2=123)
|
||||||
|
|
||||||
|
def test_data_methods_output_typed_reuses_instances(self):
|
||||||
|
template_called = False
|
||||||
|
js_called = False
|
||||||
|
css_called = False
|
||||||
|
|
||||||
|
template_data_instance = None
|
||||||
|
js_data_instance = None
|
||||||
|
css_data_instance = None
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
# Data returned from `get_context_data`
|
||||||
|
@dataclass
|
||||||
|
class TemplateData:
|
||||||
|
data1: str
|
||||||
|
data2: int
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
nonlocal template_data_instance
|
||||||
|
template_data_instance = self
|
||||||
|
|
||||||
|
# Data returned from `get_js_data`
|
||||||
|
@dataclass
|
||||||
|
class JsData:
|
||||||
|
js_data1: str
|
||||||
|
js_data2: int
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
nonlocal js_data_instance
|
||||||
|
js_data_instance = self
|
||||||
|
|
||||||
|
# Data returned from `get_css_data`
|
||||||
|
@dataclass
|
||||||
|
class CssData:
|
||||||
|
css_data1: str
|
||||||
|
css_data2: int
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
nonlocal css_data_instance
|
||||||
|
css_data_instance = self
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal template_called
|
||||||
|
template_called = True
|
||||||
|
|
||||||
|
data = Button.TemplateData(
|
||||||
|
data1="data1",
|
||||||
|
data2=123,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the instance to None to check if the instance is reused
|
||||||
|
nonlocal template_data_instance
|
||||||
|
template_data_instance = None
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_js_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal js_called
|
||||||
|
js_called = True
|
||||||
|
|
||||||
|
data = Button.JsData(
|
||||||
|
js_data1="js_data1",
|
||||||
|
js_data2=123,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the instance to None to check if the instance is reused
|
||||||
|
nonlocal js_data_instance
|
||||||
|
js_data_instance = None
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_css_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal css_called
|
||||||
|
css_called = True
|
||||||
|
|
||||||
|
data = Button.CssData(
|
||||||
|
css_data1="css_data1",
|
||||||
|
css_data2=123,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Reset the instance to None to check if the instance is reused
|
||||||
|
nonlocal css_data_instance
|
||||||
|
css_data_instance = None
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
args=["arg1", "arg2"],
|
||||||
|
kwargs={"name": "name", "age": 123},
|
||||||
|
slots={
|
||||||
|
"header": "HEADER",
|
||||||
|
"footer": Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert template_called
|
||||||
|
assert js_called
|
||||||
|
assert css_called
|
||||||
|
|
||||||
|
assert template_data_instance is not None
|
||||||
|
assert js_data_instance is not None
|
||||||
|
assert css_data_instance is not None
|
||||||
|
|
||||||
|
def test_builtin_classes(self):
|
||||||
|
class ButtonFooterSlotData(TypedDict):
|
||||||
|
value: int
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
class Args(NamedTuple):
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Kwargs:
|
||||||
|
name: str
|
||||||
|
age: int
|
||||||
|
maybe_var: Optional[int] = None
|
||||||
|
|
||||||
|
class Slots(TypedDict):
|
||||||
|
# Use `SlotInput` when you want to pass slot as string
|
||||||
|
header: SlotInput
|
||||||
|
# Use `Slot` for slot functions.
|
||||||
|
# The generic specifies the data available to the slot function
|
||||||
|
footer: NotRequired[Slot[ButtonFooterSlotData]]
|
||||||
|
|
||||||
|
# Data returned from `get_context_data`
|
||||||
|
class TemplateData(NamedTuple):
|
||||||
|
data1: str
|
||||||
|
data2: int
|
||||||
|
data3: str
|
||||||
|
|
||||||
|
# Data returned from `get_js_data`
|
||||||
|
@dataclass
|
||||||
|
class JsData:
|
||||||
|
js_data1: str
|
||||||
|
js_data2: int
|
||||||
|
js_data3: str
|
||||||
|
|
||||||
|
# Data returned from `get_css_data`
|
||||||
|
@dataclass
|
||||||
|
class CssData:
|
||||||
|
css_data1: str
|
||||||
|
css_data2: int
|
||||||
|
css_data3: str
|
||||||
|
|
||||||
|
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
return self.TemplateData(
|
||||||
|
data1=kwargs.name,
|
||||||
|
data2=kwargs.age,
|
||||||
|
data3=args.arg1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
return self.JsData(
|
||||||
|
js_data1=kwargs.name,
|
||||||
|
js_data2=kwargs.age,
|
||||||
|
js_data3=args.arg1,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||||
|
return self.CssData(
|
||||||
|
css_data1=kwargs.name,
|
||||||
|
css_data2=kwargs.age,
|
||||||
|
css_data3=args.arg1,
|
||||||
|
)
|
||||||
|
|
||||||
|
template = "<button>Click me!</button>"
|
||||||
|
|
||||||
|
# Success case
|
||||||
|
Button.render(
|
||||||
|
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||||
|
kwargs=Button.Kwargs(name="name", age=123),
|
||||||
|
slots=Button.Slots(
|
||||||
|
header="HEADER",
|
||||||
|
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Failure case 1: NamedTuple raises error when a required argument is missing
|
||||||
|
with pytest.raises(
|
||||||
|
TypeError,
|
||||||
|
match=re.escape("missing 1 required positional argument: 'arg2'"),
|
||||||
|
):
|
||||||
|
Button.render(
|
||||||
|
# Missing arg2
|
||||||
|
args=Button.Args(arg1="arg1"), # type: ignore[call-arg]
|
||||||
|
kwargs=Button.Kwargs(name="name", age=123),
|
||||||
|
slots=Button.Slots(
|
||||||
|
header="HEADER",
|
||||||
|
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Failure case 2: Dataclass raises error when a required argument is missing
|
||||||
|
with pytest.raises(
|
||||||
|
TypeError,
|
||||||
|
match=re.escape("missing 1 required positional argument: 'name'"),
|
||||||
|
):
|
||||||
|
Button.render(
|
||||||
|
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||||
|
# Name missing
|
||||||
|
kwargs=Button.Kwargs(age=123), # type: ignore[call-arg]
|
||||||
|
slots=Button.Slots(
|
||||||
|
header="HEADER",
|
||||||
|
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Failure case 3
|
||||||
|
# NOTE: While we would expect this to raise, seems that TypedDict (`Slots`)
|
||||||
|
# does NOT raise an error when a required key is missing.
|
||||||
|
Button.render(
|
||||||
|
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||||
|
kwargs=Button.Kwargs(name="name", age=123),
|
||||||
|
slots=Button.Slots( # type: ignore[typeddict-item]
|
||||||
|
footer=Slot(lambda ctx, slot_data, slot_ref: "FOOTER"), # Missing header
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Failure case 4: Data object is not of the expected type
|
||||||
|
class ButtonBad(Button):
|
||||||
|
class TemplateData(NamedTuple):
|
||||||
|
data1: str
|
||||||
|
data2: int # Removed data3
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
TypeError,
|
||||||
|
match=re.escape("got an unexpected keyword argument 'data3'"),
|
||||||
|
):
|
||||||
|
ButtonBad.render(
|
||||||
|
args=ButtonBad.Args(arg1="arg1", arg2="arg2"),
|
||||||
|
kwargs=ButtonBad.Kwargs(name="name", age=123),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_empty_type(self):
|
||||||
|
template_called = False
|
||||||
|
|
||||||
|
class Button(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
Args = Empty
|
||||||
|
Kwargs = Empty
|
||||||
|
|
||||||
|
def get_template_data(self, args, kwargs, slots, context):
|
||||||
|
nonlocal template_called
|
||||||
|
template_called = True
|
||||||
|
|
||||||
|
assert isinstance(args, Empty)
|
||||||
|
assert isinstance(kwargs, Empty)
|
||||||
|
|
||||||
|
# Success case
|
||||||
|
Button.render()
|
||||||
|
assert template_called
|
||||||
|
|
||||||
|
# Failure cases
|
||||||
|
with pytest.raises(TypeError, match=re.escape("got an unexpected keyword argument 'arg1'")):
|
||||||
|
Button.render(
|
||||||
|
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match=re.escape("got an unexpected keyword argument 'arg1'")):
|
||||||
|
Button.render(
|
||||||
|
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_args_class_raises_on_invalid(self):
|
||||||
|
class Button(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Args:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
||||||
|
Button.render(
|
||||||
|
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
|
)
|
||||||
|
|
||||||
|
class Button2(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Args:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
def __init__(self, arg1: str, arg2: str):
|
||||||
|
self.arg1 = arg1
|
||||||
|
self.arg2 = arg2
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match=re.escape("'Args' object is not iterable")):
|
||||||
|
Button2.render(
|
||||||
|
args=Button2.Args(arg1="arg1", arg2="arg2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Button3(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Args:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter([self.arg1, self.arg2])
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
|
||||||
|
Button3.render(
|
||||||
|
args=Button3.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_args_class_custom(self):
|
||||||
|
class Button(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Args:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
def __init__(self, arg1: str, arg2: str):
|
||||||
|
self.arg1 = arg1
|
||||||
|
self.arg2 = arg2
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter([self.arg1, self.arg2])
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_kwargs_class_raises_on_invalid(self):
|
||||||
|
class Button(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Kwargs:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
||||||
|
Button.render(
|
||||||
|
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
|
)
|
||||||
|
|
||||||
|
class Button2(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Kwargs:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
def __init__(self, arg1: str, arg2: str):
|
||||||
|
self.arg1 = arg1
|
||||||
|
self.arg2 = arg2
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match=re.escape("'Kwargs' object is not iterable")):
|
||||||
|
Button2.render(
|
||||||
|
kwargs=Button2.Kwargs(arg1="arg1", arg2="arg2"),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Button3(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Kwargs:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
def _asdict(self):
|
||||||
|
return {"arg1": self.arg1, "arg2": self.arg2}
|
||||||
|
|
||||||
|
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
|
||||||
|
Button3.render(
|
||||||
|
kwargs=Button3.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_kwargs_class_custom(self):
|
||||||
|
class Button(Component):
|
||||||
|
template = "Hello"
|
||||||
|
|
||||||
|
class Kwargs:
|
||||||
|
arg1: str
|
||||||
|
arg2: str
|
||||||
|
|
||||||
|
def __init__(self, arg1: str, arg2: str):
|
||||||
|
self.arg1 = arg1
|
||||||
|
self.arg2 = arg2
|
||||||
|
|
||||||
|
def _asdict(self):
|
||||||
|
return {"arg1": self.arg1, "arg2": self.arg2}
|
||||||
|
|
||||||
|
Button.render(
|
||||||
|
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"),
|
||||||
|
)
|
|
@ -198,7 +198,10 @@ class TestComponentAsView(SimpleTestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
return self.render_to_response({"name": "Bob"}, {"second_slot": "Nice to meet you, Bob"})
|
return self.render_to_response(
|
||||||
|
context={"name": "Bob"},
|
||||||
|
slots={"second_slot": "Nice to meet you, Bob"},
|
||||||
|
)
|
||||||
|
|
||||||
client = CustomClient(urlpatterns=[path("test_slot/", MockComponentSlot.as_view())])
|
client = CustomClient(urlpatterns=[path("test_slot/", MockComponentSlot.as_view())])
|
||||||
response = client.get("/test_slot/")
|
response = client.get("/test_slot/")
|
||||||
|
@ -223,7 +226,10 @@ class TestComponentAsView(SimpleTestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
return self.render_to_response({}, {"test_slot": "<script>alert(1);</script>"})
|
return self.render_to_response(
|
||||||
|
context={},
|
||||||
|
slots={"test_slot": "<script>alert(1);</script>"},
|
||||||
|
)
|
||||||
|
|
||||||
client = CustomClient(urlpatterns=[path("test_slot_insecure/", MockInsecureComponentSlot.as_view())])
|
client = CustomClient(urlpatterns=[path("test_slot_insecure/", MockInsecureComponentSlot.as_view())])
|
||||||
response = client.get("/test_slot_insecure/")
|
response = client.get("/test_slot_insecure/")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue