mirror of
https://github.com/django-components/django-components.git
synced 2025-08-04 22:38:00 +00:00
feat: validate component inputs if types are given (#629)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
682bfc4239
commit
4a9cf7e26d
7 changed files with 641 additions and 98 deletions
226
README.md
226
README.md
|
@ -46,6 +46,8 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
|||
- [Use components outside of templates](#use-components-outside-of-templates)
|
||||
- [Use components as views](#use-components-as-views)
|
||||
- [Pre-defined components](#pre-defined-components)
|
||||
- [Typing and validating components](#typing-and-validating-components)
|
||||
- [Pre-defined components](#pre-defined-components)
|
||||
- [Registering components](#registering-components)
|
||||
- [Autodiscovery](#autodiscovery)
|
||||
- [Using slots in templates](#using-slots-in-templates)
|
||||
|
@ -632,58 +634,6 @@ MyComponent.render_to_response(
|
|||
)
|
||||
```
|
||||
|
||||
### Adding type hints with Generics
|
||||
|
||||
The `Component` class optionally accepts type parameters
|
||||
that allow you to specify the types of args, kwargs, slots, and
|
||||
data.
|
||||
|
||||
```py
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotFunc
|
||||
|
||||
# Positional inputs - Tuple
|
||||
Args = Tuple[int, str]
|
||||
|
||||
# Kwargs inputs - Mapping
|
||||
class Kwargs(TypedDict):
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: NotRequired[int]
|
||||
|
||||
# Data returned from `get_context_data` - Mapping
|
||||
class Data(TypedDict):
|
||||
variable: str
|
||||
|
||||
# The data available to the `my_slot` scoped slot
|
||||
class MySlotData(TypedDict):
|
||||
value: int
|
||||
|
||||
# Slot functions - Mapping
|
||||
class Slots(TypedDict):
|
||||
# Use SlotFunc for slot functions.
|
||||
# The generic specifies the `data` dictionary
|
||||
my_slot: NotRequired[SlotFunc[MySlotData]]
|
||||
|
||||
class Button(Component[Args, Kwargs, Data, Slots]):
|
||||
def get_context_data(self, variable, another):
|
||||
return {
|
||||
"variable": variable,
|
||||
}
|
||||
```
|
||||
|
||||
When you then call `Component.render` or `Component.render_to_response`, you will get type hints:
|
||||
|
||||
```py
|
||||
Button.render(
|
||||
# Error: First arg must be `int`, got `float`
|
||||
args=(1.25, "abc"),
|
||||
# Error: Key "another" is missing
|
||||
kwargs={
|
||||
"variable": "text",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Response class of `render_to_response`
|
||||
|
||||
While `render` method returns a plain string, `render_to_response` wraps the rendered content in a "Response" class. By default, this is `django.http.HttpResponse`.
|
||||
|
@ -855,6 +805,178 @@ class MyComponent(Component):
|
|||
do_something_extra(request, *args, **kwargs)
|
||||
```
|
||||
|
||||
## Typing and validating components
|
||||
|
||||
### Adding type hints with Generics
|
||||
|
||||
The `Component` class optionally accepts type parameters
|
||||
that allow you to specify the types of args, kwargs, slots, and
|
||||
data:
|
||||
|
||||
```py
|
||||
class Button(Component[Args, Kwargs, Data, Slots]):
|
||||
...
|
||||
```
|
||||
|
||||
- `Args` - Must be a `Tuple` or `Any`
|
||||
- `Kwargs` - Must be a `TypedDict` or `Any`
|
||||
- `Data` - Must be a `TypedDict` or `Any`
|
||||
- `Slots` - Must be a `TypedDict` or `Any`
|
||||
|
||||
Here's a full example:
|
||||
|
||||
```py
|
||||
from typing import NotRequired, Tuple, TypedDict, SlotContent, SlotFunc
|
||||
|
||||
# Positional inputs
|
||||
Args = Tuple[int, str]
|
||||
|
||||
# Kwargs inputs
|
||||
class Kwargs(TypedDict):
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: NotRequired[int] # May be ommited
|
||||
|
||||
# Data returned from `get_context_data`
|
||||
class Data(TypedDict):
|
||||
variable: str
|
||||
|
||||
# The data available to the `my_slot` scoped slot
|
||||
class MySlotData(TypedDict):
|
||||
value: int
|
||||
|
||||
# Slots
|
||||
class Slots(TypedDict):
|
||||
# Use SlotFunc for slot functions.
|
||||
# The generic specifies the `data` dictionary
|
||||
my_slot: NotRequired[SlotFunc[MySlotData]]
|
||||
# SlotContent == Union[str, SafeString]
|
||||
another_slot: SlotContent
|
||||
|
||||
class Button(Component[Args, Kwargs, Data, Slots]):
|
||||
def get_context_data(self, variable, another):
|
||||
return {
|
||||
"variable": variable,
|
||||
}
|
||||
```
|
||||
|
||||
When you then call `Component.render` or `Component.render_to_response`, you will get type hints:
|
||||
|
||||
```py
|
||||
Button.render(
|
||||
# Error: First arg must be `int`, got `float`
|
||||
args=(1.25, "abc"),
|
||||
# Error: Key "another" is missing
|
||||
kwargs={
|
||||
"variable": "text",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
#### Usage for Python <3.11
|
||||
|
||||
On Python 3.8-3.10, use `typing_extensions`
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, NotRequired
|
||||
```
|
||||
|
||||
Additionally on Python 3.8-3.9, also import `annotations`:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
||||
### Passing additional args or kwargs
|
||||
|
||||
You may have a function that supports any number of args or kwargs:
|
||||
|
||||
```py
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
...
|
||||
```
|
||||
|
||||
This is not supported with the typed components.
|
||||
|
||||
As a workaround:
|
||||
- For `*args`, set a positional argument that accepts a list of values:
|
||||
|
||||
```py
|
||||
# Tuple of one member of list of strings
|
||||
Args = Tuple[List[str]]
|
||||
```
|
||||
|
||||
- For `*kwargs`, set a keyword argument that accepts a dictionary of values:
|
||||
|
||||
```py
|
||||
class Kwargs(TypedDict):
|
||||
variable: str
|
||||
another: int
|
||||
# Pass any extra keys under `extra`
|
||||
extra: Dict[str, any]
|
||||
```
|
||||
|
||||
### Handling no args or no kwargs
|
||||
|
||||
To declare that a component accepts no Args, Kwargs, etc, you can use `EmptyTuple` and `EmptyDict` types:
|
||||
|
||||
```py
|
||||
from django_components import Component, EmptyDict, EmptyTuple
|
||||
|
||||
Args = EmptyTuple
|
||||
Kwargs = Data = Slots = EmptyDict
|
||||
|
||||
class Button(Component[Args, Kwargs, Data, Slots]):
|
||||
...
|
||||
```
|
||||
|
||||
### Runtime input validation with types
|
||||
|
||||
> NOTE: Kwargs, slots, and data validation is supported only for Python >=3.11
|
||||
|
||||
In Python 3.11 and later, when you specify the component types, you will get also runtime validation of the inputs you pass to `Component.render` or `Component.render_to_response`.
|
||||
|
||||
So, using the example from before, if you ignored the type errors and still ran the following code:
|
||||
|
||||
```py
|
||||
Button.render(
|
||||
# Error: First arg must be `int`, got `float`
|
||||
args=(1.25, "abc"),
|
||||
# Error: Key "another" is missing
|
||||
kwargs={
|
||||
"variable": "text",
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
This would raise a `TypeError`:
|
||||
|
||||
```txt
|
||||
Component 'Button' expected positional argument at index 0 to be <class 'int'>, got 1.25 of type <class 'float'>
|
||||
```
|
||||
|
||||
In case you need to skip these errors, you can either set the faulty member to `Any`, e.g.:
|
||||
|
||||
```py
|
||||
# Changed `int` to `Any`
|
||||
Args = Tuple[Any, str]
|
||||
```
|
||||
|
||||
Or you can replace `Args` with `Any` altogether, to skip the validation of args:
|
||||
|
||||
```py
|
||||
# Replaced `Args` with `Any`
|
||||
class Button(Component[Any, Kwargs, Data, Slots]):
|
||||
...
|
||||
```
|
||||
|
||||
Same applies to kwargs, data, and slots.
|
||||
|
||||
## Pre-defined components
|
||||
|
||||
### Dynamic components
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue