mirror of
https://github.com/django-components/django-components.git
synced 2025-08-31 19:27:19 +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
|
||||
|
||||
## 🚨📢 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
|
||||
|
||||
#### 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
|
||||
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):
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: NotRequired[int] # May be omitted
|
||||
class Slots(NamedTuple):
|
||||
my_slot: Optional[SlotInput] = None
|
||||
another_slot: SlotInput
|
||||
|
||||
class ButtonSlots(TypedDict):
|
||||
# Use `Slot` for slot functions.
|
||||
my_slot: NotRequired[Slot]
|
||||
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
||||
another_slot: SlotContent
|
||||
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
|
||||
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]
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
args.size # int
|
||||
kwargs.variable # str
|
||||
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_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
|
||||
Button.render(
|
||||
# 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
|
||||
kwargs={
|
||||
"variable": "text",
|
||||
},
|
||||
kwargs=Button.Kwargs(
|
||||
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`.
|
||||
|
||||
```djc_py
|
||||
from typing import Dict, NotRequired, Optional, Tuple, TypedDict
|
||||
|
||||
from django_components import Component, SlotContent, register, types
|
||||
from typing import NamedTuple, Optional
|
||||
from django_components import Component, SlotInput, register, types
|
||||
|
||||
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
|
||||
# NOTE: Don't forget to set the `registry`!
|
||||
@register("my_menu", registry=comp_registry)
|
||||
class MyMenu(Component[MyMenuArgs, MyMenuProps, MyMenuSlots]):
|
||||
def get_context_data(
|
||||
self,
|
||||
*args,
|
||||
attrs: Optional[Dict] = None,
|
||||
):
|
||||
class MyMenu(Component):
|
||||
# Define the types
|
||||
class Args(NamedTuple):
|
||||
size: int
|
||||
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 {
|
||||
"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
|
||||
that allow you to specify the types of args, kwargs, slots, and data.
|
||||
!!! warning
|
||||
|
||||
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
|
||||
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):
|
||||
template_file = "button.html"
|
||||
class Kwargs(NamedTuple):
|
||||
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`
|
||||
- `Kwargs` - Keyword arguments, must be a `TypedDict` or `Any`
|
||||
- `Slots` - Slots, must be a `TypedDict` or `Any`
|
||||
- `TemplateData` - Data returned from [`get_context_data()`](../../../reference/api#django_components.Component.get_context_data), must be a `TypedDict` or `Any`
|
||||
- `JsData` - Data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), must be a `TypedDict` or `Any`
|
||||
- `CssData` - Data returned from [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data), must be a `TypedDict` or `Any`
|
||||
- [`Args`](../../../reference/api#django_components.Component.Args) - Type for positional arguments.
|
||||
- [`Kwargs`](../../../reference/api#django_components.Component.Kwargs) - Type for keyword arguments.
|
||||
- [`Slots`](../../../reference/api#django_components.Component.Slots) - Type for slots.
|
||||
- [`TemplateData`](../../../reference/api#django_components.Component.TemplateData) - Type for data returned from [`get_template_data()`](../../../reference/api#django_components.Component.get_template_data).
|
||||
- [`JsData`](../../../reference/api#django_components.Component.JsData) - Type for data returned from [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data).
|
||||
- [`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`.
|
||||
|
||||
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.
|
||||
You can specify as many or as few of these as you want, the rest will default to `None`.
|
||||
|
||||
## 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
|
||||
from typing_extensions import NotRequired, Tuple, TypedDict
|
||||
from django_components import Component, Slot, SlotContent
|
||||
|
||||
###########################################
|
||||
# 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
|
||||
from typing_extensions import NamedTuple, TypedDict
|
||||
from django.template import Context
|
||||
from django_components import Component, Slot, SlotInput
|
||||
|
||||
# The data available to the `footer` scoped slot
|
||||
class ButtonFooterSlotData(TypedDict):
|
||||
value: int
|
||||
|
||||
# Slots
|
||||
class ButtonSlots(TypedDict):
|
||||
# Use `SlotContent` when you want to allow either `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]]
|
||||
# Define the component with the types
|
||||
class Button(Component):
|
||||
class Args(NamedTuple):
|
||||
name: str
|
||||
|
||||
###########################################
|
||||
# 2. Define the component with those types
|
||||
###########################################
|
||||
class Kwargs(NamedTuple):
|
||||
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):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
...
|
||||
```
|
||||
# Add type hints to the data method
|
||||
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
|
||||
[`Component.render`](../../../reference/api#django_components.Component.render)
|
||||
or [`Component.render_to_response`](../../../reference/api#django_components.Component.render_to_response),
|
||||
you will get type hints:
|
||||
args.name # str
|
||||
kwargs.age # int
|
||||
slots.footer # Slot[ButtonFooterSlotData]
|
||||
|
||||
```python
|
||||
# Add type hints to the render call
|
||||
Button.render(
|
||||
# ERROR: Expects a string
|
||||
args=(123,),
|
||||
kwargs={
|
||||
"name": "John",
|
||||
# ERROR: Expects an integer
|
||||
"age": "invalid",
|
||||
},
|
||||
slots={
|
||||
"header": "...",
|
||||
"footer": lambda ctx, slot_data, slot_ref: slot_data.value + 1,
|
||||
# ERROR: Unexpected key "foo"
|
||||
"foo": "invalid",
|
||||
},
|
||||
args=Button.Args(
|
||||
name="John",
|
||||
),
|
||||
kwargs=Button.Kwargs(
|
||||
surname="Doe",
|
||||
age=30,
|
||||
),
|
||||
slots=Button.Slots(
|
||||
footer=Button.Slot(lambda ctx, slot_data, slot_ref: slot_data.value + 1),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
from typing_extensions import NotRequired, Tuple, TypedDict
|
||||
from django_components import Component, Slot, SlotContent
|
||||
from typing import NamedTuple
|
||||
from django_components import Component
|
||||
|
||||
###########################################
|
||||
# 1. Define the types
|
||||
###########################################
|
||||
class Button(Component):
|
||||
class TemplateData(NamedTuple):
|
||||
data1: str
|
||||
data2: int
|
||||
|
||||
# Data returned from `get_context_data`
|
||||
class ButtonData(TypedDict):
|
||||
data1: str
|
||||
data2: int
|
||||
class JsData(NamedTuple):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
|
||||
# Data returned from `get_js_data`
|
||||
class ButtonJsData(TypedDict):
|
||||
js_data1: str
|
||||
js_data2: int
|
||||
class CssData(NamedTuple):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
|
||||
# Data returned from `get_css_data`
|
||||
class ButtonCssData(TypedDict):
|
||||
css_data1: str
|
||||
css_data2: int
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"data1": "...",
|
||||
"data2": 123,
|
||||
}
|
||||
|
||||
###########################################
|
||||
# 2. Define the component with those types
|
||||
###########################################
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"js_data1": "...",
|
||||
"js_data2": 123,
|
||||
}
|
||||
|
||||
ButtonType = Component[Any, Any, Any, ButtonData, ButtonJsData, ButtonCssData]
|
||||
|
||||
class Button(ButtonType):
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
...
|
||||
|
||||
def get_js_data(self, *args, **kwargs):
|
||||
...
|
||||
|
||||
def get_css_data(self, *args, **kwargs):
|
||||
...
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
"css_data1": "...",
|
||||
"css_data2": 123,
|
||||
}
|
||||
```
|
||||
|
||||
When you then call
|
||||
[`Component.render`](../../../reference/api#django_components.Component.render)
|
||||
or [`Component.render_to_response`](../../../reference/api#django_components.Component.render_to_response),
|
||||
the component will raise an error if the data is not valid.
|
||||
|
||||
If you don't want to validate some parts, set them to [`Any`](https://docs.python.org/3/library/typing.html#typing.Any).
|
||||
|
||||
The following will validate only the data returned from `get_js_data`:
|
||||
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly.
|
||||
|
||||
```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
|
||||
|
||||
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
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
...
|
||||
```
|
||||
|
||||
This is not supported with the typed components.
|
||||
However, this cannot be described with the current Python's typing system (as of v0.140).
|
||||
|
||||
As a workaround:
|
||||
|
||||
- For a variable number of positional arguments (`*args`), set a positional argument that accepts a list of values:
|
||||
|
||||
```py
|
||||
# Tuple of one member of list of strings
|
||||
Args = Tuple[List[str]]
|
||||
class Table(Component):
|
||||
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:
|
||||
|
||||
```py
|
||||
class Kwargs(TypedDict):
|
||||
variable: str
|
||||
another: int
|
||||
# Pass any extra keys under `extra`
|
||||
extra: Dict[str, any]
|
||||
class Table(Component):
|
||||
class Kwargs(NamedTuple):
|
||||
variable: str
|
||||
another: int
|
||||
# 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
|
||||
|
||||
To declare that a component accepts no args, kwargs, etc, you can use the
|
||||
[`EmptyTuple`](../../../reference/api#django_components.EmptyTuple) and
|
||||
[`EmptyDict`](../../../reference/api#django_components.EmptyDict) types:
|
||||
To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the `pass` keyword:
|
||||
|
||||
```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,
|
||||
but it will not validate the inputs.
|
||||
When you add types to your component, and implement
|
||||
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
|
||||
pip install djc-ext-pydantic
|
||||
```
|
||||
|
||||
And add the extension to your project:
|
||||
And then add the extension to your project:
|
||||
|
||||
```py
|
||||
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
|
||||
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
|
||||
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):
|
||||
def get_context_data(self, *args, **attrs):
|
||||
# Access component's ID
|
||||
assert self.id == "djc1A2b3c"
|
||||
assert self.id == "c1A2b3c"
|
||||
|
||||
# Access component's inputs, slots and context
|
||||
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):
|
||||
def get_context_data(self, *args, **attrs):
|
||||
# Access component's ID
|
||||
assert self.id == "djc1A2b3c"
|
||||
assert self.id == "c1A2b3c"
|
||||
|
||||
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
|
||||
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):
|
||||
variable: str
|
||||
another: int
|
||||
maybe_var: NotRequired[int] # May be omitted
|
||||
class Slots(NamedTuple):
|
||||
my_slot: Optional[SlotInput] = None
|
||||
another_slot: SlotInput
|
||||
|
||||
class ButtonSlots(TypedDict):
|
||||
# Use `Slot` for slot functions.
|
||||
my_slot: NotRequired[Slot]
|
||||
# Use `SlotContent` when you want to allow either `Slot` instance or plain string
|
||||
another_slot: SlotContent
|
||||
|
||||
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
|
||||
|
||||
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
|
||||
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
|
||||
args.size # int
|
||||
kwargs.variable # str
|
||||
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_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
|
||||
Button.render(
|
||||
# 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
|
||||
kwargs={
|
||||
"variable": "text",
|
||||
},
|
||||
kwargs=Button.Kwargs(
|
||||
variable="text",
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
|
|
|
@ -71,11 +71,7 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.EmptyDict
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.EmptyTuple
|
||||
::: django_components.Empty
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
|
@ -95,6 +91,10 @@
|
|||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.SlotInput
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
||||
::: django_components.SlotRef
|
||||
options:
|
||||
show_if_no_docstring: true
|
||||
|
|
|
@ -54,7 +54,8 @@ python manage.py components ext run <extension> <command>
|
|||
## `components create`
|
||||
|
||||
```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
|
||||
|
||||
```
|
||||
|
@ -237,7 +238,7 @@ List all extensions.
|
|||
- `--columns COLUMNS`
|
||||
- Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`.
|
||||
- `-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`
|
||||
- Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`.
|
||||
- `-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
|
||||
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`
|
||||
|
||||
```txt
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
|
||||
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
name
|
||||
|
||||
```
|
||||
|
|
|
@ -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.library import TagProtectedError
|
||||
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 (
|
||||
ComponentFormatter,
|
||||
ShorthandComponentFormatter,
|
||||
|
@ -65,7 +65,7 @@ from django_components.template import cached_template
|
|||
import django_components.types as types
|
||||
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.types import EmptyTuple, EmptyDict
|
||||
from django_components.util.types import Empty
|
||||
|
||||
# isort: on
|
||||
|
||||
|
@ -103,8 +103,7 @@ __all__ = [
|
|||
"ContextBehavior",
|
||||
"Default",
|
||||
"DynamicComponent",
|
||||
"EmptyTuple",
|
||||
"EmptyDict",
|
||||
"Empty",
|
||||
"format_attributes",
|
||||
"get_component_by_class_id",
|
||||
"get_component_dirs",
|
||||
|
@ -126,9 +125,10 @@ __all__ = [
|
|||
"RegistrySettings",
|
||||
"render_dependencies",
|
||||
"ShorthandComponentFormatter",
|
||||
"SlotContent",
|
||||
"Slot",
|
||||
"SlotContent",
|
||||
"SlotFunc",
|
||||
"SlotInput",
|
||||
"SlotRef",
|
||||
"SlotResult",
|
||||
"TagFormatterABC",
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
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 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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import (
|
||||
ArgsType,
|
||||
Component,
|
||||
CssDataType,
|
||||
DataType,
|
||||
JsDataType,
|
||||
KwargsType,
|
||||
SlotsType,
|
||||
)
|
||||
from django_components.component import Component
|
||||
|
||||
|
||||
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||
|
@ -36,6 +28,9 @@ else:
|
|||
AllRegistries = List[ReferenceType]
|
||||
|
||||
|
||||
TComponent = TypeVar("TComponent", bound="Component")
|
||||
|
||||
|
||||
class AlreadyRegistered(Exception):
|
||||
"""
|
||||
Raised when you try to register a [Component](../api#django_components#Component),
|
||||
|
@ -264,6 +259,10 @@ class ComponentRegistry:
|
|||
)
|
||||
|
||||
def __del__(self) -> None:
|
||||
# Skip if `extensions` was deleted before this registry
|
||||
if not extensions:
|
||||
return
|
||||
|
||||
extensions.on_registry_deleted(
|
||||
OnRegistryDeletedContext(
|
||||
registry=self,
|
||||
|
@ -615,8 +614,8 @@ _the_registry = registry
|
|||
|
||||
|
||||
def register(name: str, registry: Optional[ComponentRegistry] = None) -> Callable[
|
||||
[Type["Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]"]],
|
||||
Type["Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]"],
|
||||
[Type[TComponent]],
|
||||
Type[TComponent],
|
||||
]:
|
||||
"""
|
||||
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:
|
||||
registry = _the_registry
|
||||
|
||||
def decorator(
|
||||
component: Type["Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]"],
|
||||
) -> Type["Component[ArgsType, KwargsType, SlotsType, DataType, JsDataType, CssDataType]"]:
|
||||
def decorator(component: Type[TComponent]) -> Type[TComponent]:
|
||||
registry.register(name=name, component=component)
|
||||
return component
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ from typing import (
|
|||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Mapping,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
|
@ -134,7 +135,7 @@ def cache_component_js(comp_cls: Type["Component"]) -> None:
|
|||
# with `Components.manager.registerComponentData`.
|
||||
# 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`.
|
||||
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):
|
||||
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
|
||||
# with variables separately from `Component.css`, because different instances may return different
|
||||
# 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):
|
||||
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.
|
||||
|
||||
Read more about [Component defaults](../../concepts/fundamentals/component_defaults).
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from django_components import Component, Default
|
||||
|
||||
|
@ -133,6 +136,7 @@ class ComponentDefaults(ComponentExtension.ExtensionClass): # type: ignore[misc
|
|||
class Defaults:
|
||||
position = "left"
|
||||
selected_items = Default(lambda: [1, 2, 3])
|
||||
```
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
@ -17,17 +17,25 @@ class ComponentView(ComponentExtension.ExtensionClass, View): # type: ignore
|
|||
"""
|
||||
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`.
|
||||
|
||||
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:**
|
||||
|
||||
```python
|
||||
class MyComponent(Component):
|
||||
class View:
|
||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return HttpResponse("Hello, world!")
|
||||
```
|
||||
"""
|
||||
|
||||
# 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
|
||||
#
|
||||
# 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:]):
|
||||
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
|
||||
# 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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""Helper types for IDEs."""
|
||||
|
||||
from django_components.util.types import Annotated
|
||||
from typing_extensions import Annotated
|
||||
|
||||
css = Annotated[str, "css"]
|
||||
django_html = Annotated[str, "django_html"]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import re
|
||||
import sys
|
||||
from dataclasses import asdict, is_dataclass
|
||||
from hashlib import md5
|
||||
from importlib import import_module
|
||||
from itertools import chain
|
||||
|
@ -130,3 +131,19 @@ def is_glob(filepath: str) -> bool:
|
|||
|
||||
def flatten(lst: Iterable[Iterable[T]]) -> List[T]:
|
||||
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
|
||||
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
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
EmptyTuple = Tuple[()]
|
||||
"""
|
||||
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):
|
||||
class Empty(NamedTuple):
|
||||
"""
|
||||
TypedDict with no members.
|
||||
Type for an object with no members.
|
||||
|
||||
You can use this to define a [Component](../api#django_components.Component)
|
||||
that accepts NO kwargs, or NO slots, or returns NO data from
|
||||
[`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:
|
||||
You can use this to define [Component](../api#django_components.Component)
|
||||
types that accept NO args, kwargs, slots, etc:
|
||||
|
||||
```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
|
||||
from django_components import Component, EmptyDict
|
||||
|
||||
class Table(Component(Any, Any, EmptyDict, Any, Any, Any))
|
||||
...
|
||||
```py
|
||||
class Empty(NamedTuple):
|
||||
pass
|
||||
```
|
||||
|
||||
Returns NO data from `get_context_data()`:
|
||||
|
||||
```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,
|
||||
},
|
||||
)
|
||||
```
|
||||
Read more about [Typing and validation](../../concepts/advanced/typing_and_validation).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
@ -85,10 +85,10 @@ class TestComponentMediaCache:
|
|||
<div>Template only component</div>
|
||||
"""
|
||||
|
||||
def get_js_data(self):
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
def get_css_data(self):
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
@register("test_media_no_vars")
|
||||
|
@ -100,10 +100,10 @@ class TestComponentMediaCache:
|
|||
js = "console.log('Hello from JS');"
|
||||
css = ".novars-component { color: blue; }"
|
||||
|
||||
def get_js_data(self):
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
def get_css_data(self):
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {}
|
||||
|
||||
class TestMediaAndVarsComponent(Component):
|
||||
|
@ -114,10 +114,10 @@ class TestComponentMediaCache:
|
|||
js = "console.log('Hello from full component');"
|
||||
css = ".full-component { color: blue; }"
|
||||
|
||||
def get_js_data(self):
|
||||
def get_js_data(self, args, kwargs, slots, context):
|
||||
return {"message": "Hello"}
|
||||
|
||||
def get_css_data(self):
|
||||
def get_css_data(self, args, kwargs, slots, context):
|
||||
return {"color": "blue"}
|
||||
|
||||
# Register our test cache
|
||||
|
|
|
@ -4,8 +4,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
|
|||
"""
|
||||
|
||||
import re
|
||||
from typing import Any, Dict, Tuple, no_type_check
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
from typing import Any, Dict, no_type_check
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
|
@ -20,8 +19,6 @@ from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
|||
from django_components import (
|
||||
Component,
|
||||
ComponentView,
|
||||
SlotContent,
|
||||
Slot,
|
||||
all_components,
|
||||
get_component_by_class_id,
|
||||
register,
|
||||
|
@ -385,100 +382,6 @@ class TestComponent:
|
|||
|
||||
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
|
||||
class TestComponentRender:
|
||||
|
|
|
@ -32,7 +32,7 @@ class TestComponentDefaults:
|
|||
# Check that args and slots are NOT affected by the defaults
|
||||
assert self.input.args == [123]
|
||||
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 == {
|
||||
"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:
|
||||
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())])
|
||||
response = client.get("/test_slot/")
|
||||
|
@ -223,7 +226,10 @@ class TestComponentAsView(SimpleTestCase):
|
|||
"""
|
||||
|
||||
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())])
|
||||
response = client.get("/test_slot_insecure/")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue