refactor: make it optional having to specify parent class of Args, Kwargs, Slots, etc (#1466)

This commit is contained in:
Juro Oravec 2025-10-21 15:30:08 +02:00 committed by GitHub
parent 0aeb96fa40
commit c37628dea0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 661 additions and 299 deletions

View file

@ -1,5 +1,60 @@
# Release notes # Release notes
## v0.142.4
#### Refactor
- Simpler syntax for defining component inputs.
When defining `Args`, `Kwargs`, `Slots`, `JsData`, `CssData`, `TemplateData`, these data classes now don't have to subclass any other class.
If they are not subclassing (nor `@dataclass`), these data classes will be automatically converted to `NamedTuples`:
Before - the `Args`, `Kwargs`, and `Slots` (etc..) had to be NamedTuples, dataclasses, or Pydantic models:
```py
from typing import NamedTuple
from django_components import Component
class Button(Component):
class Args(NamedTuple):
size: int
text: str
class Kwargs(NamedTuple):
variable: str
maybe_var: Optional[int] = None
class Slots(NamedTuple):
my_slot: Optional[SlotInput] = None
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
...
```
Now these classes are automatically converted to `NamedTuples` if they don't subclass anything else:
```py
class Button(Component):
class Args: # Same as `Args(NamedTuple)`
size: int
text: str
class Kwargs: # Same as `Kwargs(NamedTuple)`
variable: str
maybe_var: Optional[int] = None
class Slots: # Same as `Slots(NamedTuple)`
my_slot: Optional[SlotInput] = None
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
...
```
- Extension authors: The `ExtensionComponentConfig` can be instantiated with `None` instead of a component instance.
This allows to call component-level extension methods outside of the normal rendering lifecycle.
## v0.142.3 ## v0.142.3
#### Fix #### Fix

View file

@ -405,21 +405,21 @@ Avoid needless errors with [type hints and runtime input validation](https://dja
To opt-in to input validation, define types for component's args, kwargs, slots, and more: To opt-in to input validation, define types for component's args, kwargs, slots, and more:
```py ```py
from typing import NamedTuple, Optional from typing import Optional
from django.template import Context from django.template import Context
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
size: int size: int
text: str text: str
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
another: int another: int
maybe_var: Optional[int] = None # May be omitted maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple): class Slots:
my_slot: Optional[SlotInput] = None my_slot: Optional[SlotInput] = None
another_slot: SlotInput another_slot: SlotInput

View file

@ -101,7 +101,7 @@ For live examples, see the [Examples](../../examples/index.md).
It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`. It's also a good idea to have a common prefix for your components, so they can be easily distinguished from users' components. In the example below, we use the prefix `my_` / `My`.
```djc_py ```djc_py
from typing import NamedTuple, Optional from typing import Optional
from django_components import Component, SlotInput, register, types from django_components import Component, SlotInput, register, types
from myapp.templatetags.mytags import comp_registry from myapp.templatetags.mytags import comp_registry
@ -111,16 +111,16 @@ For live examples, see the [Examples](../../examples/index.md).
@register("my_menu", registry=comp_registry) @register("my_menu", registry=comp_registry)
class MyMenu(Component): class MyMenu(Component):
# Define the types # Define the types
class Args(NamedTuple): class Args:
size: int size: int
text: str text: str
class Kwargs(NamedTuple): class Kwargs:
vertical: Optional[bool] = None vertical: Optional[bool] = None
klass: Optional[str] = None klass: Optional[str] = None
style: Optional[str] = None style: Optional[str] = None
class Slots(NamedTuple): class Slots:
default: Optional[SlotInput] = None default: Optional[SlotInput] = None
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context): def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):

View file

@ -65,12 +65,14 @@ E.g.:
Each method of the nested classes has access to the `component` attribute, Each method of the nested classes has access to the `component` attribute,
which points to the component instance. which points to the component instance.
This may be `None` if the methods does NOT run during the rendering.
```python ```python
class MyTable(Component): class MyTable(Component):
class MyExtension: class MyExtension:
def get(self, request): def get(self, request):
# `self.component` points to the instance of `MyTable` Component. # `self.component` points to the instance of `MyTable` Component.
return self.component.render_to_response(request=request) return self.component.kwargs.some_input
``` ```
### Example: Component as View ### Example: Component as View

View file

@ -289,14 +289,14 @@ To implement this, we render the fallback slot in [`on_render()`](../../../refer
and return it if an error occured: and return it if an error occured:
```djc_py ```djc_py
from typing import NamedTuple, Optional from typing import Optional
from django.template import Context, Template from django.template import Context, Template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django_components import Component, OnRenderGenerator, SlotInput, types from django_components import Component, OnRenderGenerator, SlotInput, types
class ErrorFallback(Component): class ErrorFallback(Component):
class Slots(NamedTuple): class Slots:
default: Optional[SlotInput] = None default: Optional[SlotInput] = None
fallback: Optional[SlotInput] = None fallback: Optional[SlotInput] = None

View file

@ -74,7 +74,7 @@ and so `selected_items` will be set to `[1, 2, 3]`.
```py ```py
class ProfileCard(Component): class ProfileCard(Component):
class Kwargs(NamedTuple): class Kwargs:
show_details: bool = True show_details: bool = True
``` ```

View file

@ -15,7 +15,7 @@ Each method handles the data independently - you can define different data for t
```python ```python
class ProfileCard(Component): class ProfileCard(Component):
class Kwargs(NamedTuple): class Kwargs:
user_id: int user_id: int
show_details: bool show_details: bool
@ -51,7 +51,7 @@ If [`get_template_data()`](../../../reference/api/#django_components.Component.g
class ProfileCard(Component): class ProfileCard(Component):
template_file = "profile_card.html" template_file = "profile_card.html"
class Kwargs(NamedTuple): class Kwargs:
user_id: int user_id: int
show_details: bool show_details: bool
@ -182,10 +182,10 @@ class ProfileCard(Component):
```py ```py
class ProfileCard(Component): class ProfileCard(Component):
class Args(NamedTuple): class Args:
user_id: int user_id: int
class Kwargs(NamedTuple): class Kwargs:
show_details: bool show_details: bool
# Access inputs directly as parameters # Access inputs directly as parameters
@ -228,10 +228,10 @@ class ProfileCard(Component):
```py ```py
class ProfileCard(Component): class ProfileCard(Component):
class Args(NamedTuple): class Args:
user_id: int user_id: int
class Kwargs(NamedTuple): class Kwargs:
show_details: bool show_details: bool
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context): def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
@ -320,7 +320,7 @@ from django_components import Component, Default, register
@register("profile_card") @register("profile_card")
class ProfileCard(Component): class ProfileCard(Component):
class Kwargs(NamedTuple): class Kwargs:
show_details: bool show_details: bool
class Defaults: class Defaults:
@ -344,7 +344,7 @@ class ProfileCard(Component):
```py ```py
class ProfileCard(Component): class ProfileCard(Component):
class Kwargs(NamedTuple): class Kwargs:
show_details: bool = True show_details: bool = True
``` ```
@ -391,18 +391,18 @@ This will also validate the inputs at runtime, as the type classes will be insta
Read more about [Component typing](../../fundamentals/typing_and_validation). Read more about [Component typing](../../fundamentals/typing_and_validation).
```python ```python
from typing import NamedTuple, Optional from typing import Optional
from django_components import Component, SlotInput from django_components import Component, SlotInput
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
name: str name: str
class Kwargs(NamedTuple): class Kwargs:
surname: str surname: str
maybe_var: Optional[int] = None # May be omitted maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple): class Slots:
my_slot: Optional[SlotInput] = None my_slot: Optional[SlotInput] = None
footer: SlotInput footer: SlotInput
@ -433,19 +433,18 @@ and [`CssData`](../../../reference/api/#django_components.Component.CssData) cla
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class. For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class.
```python ```python
from typing import NamedTuple
from django_components import Component from django_components import Component
class Button(Component): class Button(Component):
class TemplateData(NamedTuple): class TemplateData(
data1: str data1: str
data2: int data2: int
class JsData(NamedTuple): class JsData:
js_data1: str js_data1: str
js_data2: int js_data2: int
class CssData(NamedTuple): class CssData:
css_data1: str css_data1: str
css_data2: int css_data2: int

View file

@ -92,7 +92,7 @@ With `Args` class:
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class Args(NamedTuple): class Args:
page: int page: int
per_page: int per_page: int
@ -137,7 +137,7 @@ With `Kwargs` class:
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class Kwargs(NamedTuple): class Kwargs:
page: int page: int
per_page: int per_page: int
@ -182,7 +182,7 @@ With `Slots` class:
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
class Table(Component): class Table(Component):
class Slots(NamedTuple): class Slots:
header: SlotInput header: SlotInput
footer: SlotInput footer: SlotInput

View file

@ -80,14 +80,13 @@ For a component to be renderable with the [`{% component %}`](../../../reference
For example, if you register a component under the name `"button"`: For example, if you register a component under the name `"button"`:
```python ```python
from typing import NamedTuple
from django_components import Component, register from django_components import Component, register
@register("button") @register("button")
class Button(Component): class Button(Component):
template_file = "button.html" template_file = "button.html"
class Kwargs(NamedTuple): class Kwargs:
name: str name: str
job: str job: str
@ -207,20 +206,20 @@ The [`Component.render()`](../../../reference/api/#django_components.Component.r
This is the equivalent of calling the [`{% component %}`](../template_tags#component) tag. This is the equivalent of calling the [`{% component %}`](../template_tags#component) tag.
```python ```python
from typing import NamedTuple, Optional from typing import Optional
from django_components import Component, SlotInput from django_components import Component, SlotInput
class Button(Component): class Button(Component):
template_file = "button.html" template_file = "button.html"
class Args(NamedTuple): class Args:
name: str name: str
class Kwargs(NamedTuple): class Kwargs:
surname: str surname: str
age: int age: int
class Slots(NamedTuple): class Slots:
footer: Optional[SlotInput] = None footer: Optional[SlotInput] = None
def get_template_data(self, args, kwargs, slots, context): def get_template_data(self, args, kwargs, slots, context):
@ -264,20 +263,20 @@ Any extra arguments are passed to the [`HttpResponse`](https://docs.djangoprojec
constructor. constructor.
```python ```python
from typing import NamedTuple, Optional from typing import Optional
from django_components import Component, SlotInput from django_components import Component, SlotInput
class Button(Component): class Button(Component):
template_file = "button.html" template_file = "button.html"
class Args(NamedTuple): class Args(
name: str name: str
class Kwargs(NamedTuple): class Kwargs:
surname: str surname: str
age: int age: int
class Slots(NamedTuple): class Slots:
footer: Optional[SlotInput] = None footer: Optional[SlotInput] = None
def get_template_data(self, args, kwargs, slots, context): def get_template_data(self, args, kwargs, slots, context):
@ -486,19 +485,19 @@ and [`Slots`](../../../reference/api/#django_components.Component.Slots) classes
Read more on [Typing and validation](../../fundamentals/typing_and_validation). Read more on [Typing and validation](../../fundamentals/typing_and_validation).
```python ```python
from typing import NamedTuple, Optional from typing import Optional
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
# Define the component with the types # Define the component with the types
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args(
name: str name: str
class Kwargs(NamedTuple): class Kwargs:
surname: str surname: str
age: int age: int
class Slots(NamedTuple): class Slots:
my_slot: Optional[SlotInput] = None my_slot: Optional[SlotInput] = None
footer: SlotInput footer: SlotInput

View file

@ -20,20 +20,20 @@ that allow you to define the types of args, kwargs, slots, as well as the data r
Use this to add type hints to your components, to validate the inputs at runtime, and to document them. Use this to add type hints to your components, to validate the inputs at runtime, and to document them.
```py ```py
from typing import NamedTuple, Optional from typing import Optional
from django.template import Context from django.template import Context
from django_components import Component, SlotInput from django_components import Component, SlotInput
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
size: int size: int
text: str text: str
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
maybe_var: Optional[int] = None # May be omitted maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple): class Slots:
my_slot: Optional[SlotInput] = None my_slot: Optional[SlotInput] = None
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context): def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
@ -59,7 +59,7 @@ You can use [`Component.Args`](../../../reference/api#django_components.Componen
[`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs), [`Component.Kwargs`](../../../reference/api#django_components.Component.Kwargs),
and [`Component.Slots`](../../../reference/api#django_components.Component.Slots) to type the component inputs. 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 When you set these input classes, the `args`, `kwargs`, and `slots` parameters of the data methods
([`get_template_data()`](../../../reference/api#django_components.Component.get_template_data), ([`get_template_data()`](../../../reference/api#django_components.Component.get_template_data),
[`get_js_data()`](../../../reference/api#django_components.Component.get_js_data), [`get_js_data()`](../../../reference/api#django_components.Component.get_js_data),
[`get_css_data()`](../../../reference/api#django_components.Component.get_css_data)) [`get_css_data()`](../../../reference/api#django_components.Component.get_css_data))
@ -80,7 +80,7 @@ or set them to `None`, the inputs will be passed as plain lists or dictionaries,
and will not be validated. and will not be validated.
```python ```python
from typing_extensions import NamedTuple, TypedDict from typing_extensions import TypedDict
from django.template import Context from django.template import Context
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
@ -90,15 +90,15 @@ class ButtonFooterSlotData(TypedDict):
# Define the component with the types # Define the component with the types
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
name: str name: str
class Kwargs(NamedTuple): class Kwargs:
surname: str surname: str
age: int age: int
maybe_var: Optional[int] = None # May be omitted maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple): class Slots:
# Use `SlotInput` to allow slots to be given as `Slot` instance, # Use `SlotInput` to allow slots to be given as `Slot` instance,
# plain string, or a function that returns a string. # plain string, or a function that returns a string.
my_slot: Optional[SlotInput] = None my_slot: Optional[SlotInput] = None
@ -143,7 +143,7 @@ class Button(Component):
Args = None Args = None
Slots = None Slots = None
class Kwargs(NamedTuple): class Kwargs:
name: str name: str
age: int age: int
@ -195,19 +195,18 @@ If you omit the [`TemplateData`](../../../reference/api#django_components.Compon
or set them to `None`, the validation and instantiation will be skipped. or set them to `None`, the validation and instantiation will be skipped.
```python ```python
from typing import NamedTuple
from django_components import Component from django_components import Component
class Button(Component): class Button(Component):
class TemplateData(NamedTuple): class TemplateData:
data1: str data1: str
data2: int data2: int
class JsData(NamedTuple): class JsData:
js_data1: str js_data1: str
js_data2: int js_data2: int
class CssData(NamedTuple): class CssData:
css_data1: str css_data1: str
css_data2: int css_data2: int
@ -233,19 +232,18 @@ class Button(Component):
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly. For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly.
```python ```python
from typing import NamedTuple
from django_components import Component from django_components import Component
class Button(Component): class Button(Component):
class TemplateData(NamedTuple): class TemplateData:
data1: str data1: str
data2: int data2: int
class JsData(NamedTuple): class JsData:
js_data1: str js_data1: str
js_data2: int js_data2: int
class CssData(NamedTuple): class CssData:
css_data1: str css_data1: str
css_data2: int css_data2: int
@ -270,34 +268,63 @@ class Button(Component):
## Custom types ## Custom types
We recommend to use [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) So far, we've defined the input classes like `Kwargs` as simple classes.
for the `Args` class, and [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),
The truth is that when these classes don't subclass anything else,
they are converted to `NamedTuples` behind the scenes.
```py
class Table(Component):
class Kwargs:
name: str
age: int
```
is the same as:
```py
class Table(Component):
class Kwargs(NamedTuple):
name: str
age: int
```
You can actually set these classes to anything you want - whether it's dataclasses,
[Pydantic models](https://docs.pydantic.dev/latest/concepts/models/), or custom classes:
```py
from typing import NamedTuple, Optional
from django_components import Component, Optional
from pydantic import BaseModel
class Button(Component):
class Args(NamedTuple):
size: int
text: str
@dataclass
class Kwargs:
variable: str
maybe_var: Optional[int] = None
class Slots(BaseModel):
my_slot: Optional[SlotInput] = None
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
...
```
We recommend:
- [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
for the `Args` class
- [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple),
[dataclasses](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass), [dataclasses](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass),
or [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/) or [Pydantic models](https://docs.pydantic.dev/latest/concepts/models/)
for `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData` classes. for `Kwargs`, `Slots`, `TemplateData`, `JsData`, and `CssData` classes.
However, you can use any class, as long as they meet the conditions below. 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 ### `Args` class
The [`Args`](../../../reference/api#django_components.Component.Args) class The [`Args`](../../../reference/api#django_components.Component.Args) class
@ -390,7 +417,7 @@ As a workaround:
```py ```py
class Table(Component): class Table(Component):
class Args(NamedTuple): class Args:
args: List[str] args: List[str]
Table.render( Table.render(
@ -402,7 +429,7 @@ As a workaround:
```py ```py
class Table(Component): class Table(Component):
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
another: int another: int
# Pass any extra keys under `extra` # Pass any extra keys under `extra`
@ -422,17 +449,16 @@ As a workaround:
To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the `pass` keyword: To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the `pass` keyword:
```py ```py
from typing import NamedTuple
from django_components import Component from django_components import Component
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
pass pass
class Kwargs(NamedTuple): class Kwargs:
pass pass
class Slots(NamedTuple): class Slots:
pass pass
``` ```
@ -459,14 +485,14 @@ In the example below, `ButtonExtra` inherits `Kwargs` from `Button`, but overrid
from django_components import Component, Empty from django_components import Component, Empty
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
size: int size: int
class Kwargs(NamedTuple): class Kwargs:
color: str color: str
class ButtonExtra(Button): class ButtonExtra(Button):
class Args(NamedTuple): class Args:
name: str name: str
size: int size: int
@ -491,10 +517,10 @@ Compare the following:
```py ```py
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
size: int size: int
class Kwargs(NamedTuple): class Kwargs:
color: str color: str
# Both `Args` and `Kwargs` are defined on the class # Both `Args` and `Kwargs` are defined on the class
@ -624,23 +650,23 @@ The steps to migrate are:
Thus, the code above will become: Thus, the code above will become:
```py ```py
from typing import NamedTuple, Optional from typing import Optional
from django.template import Context from django.template import Context
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
# The Component class does not take any generics # The Component class does not take any generics
class Button(Component): class Button(Component):
# Types are now defined inside the component class # Types are now defined inside the component class
class Args(NamedTuple): class Args:
size: int size: int
text: str text: str
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
another: int another: int
maybe_var: Optional[int] = None # May be omitted maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple): class Slots:
# Use `SlotInput` to allow slots to be given as `Slot` instance, # Use `SlotInput` to allow slots to be given as `Slot` instance,
# plain string, or a function that returns a string. # plain string, or a function that returns a string.
my_slot: Optional[SlotInput] = None my_slot: Optional[SlotInput] = None

View file

@ -1,6 +1,6 @@
# ruff: noqa: S311 # ruff: noqa: S311
import random import random
from typing import NamedTuple, Optional from typing import Optional
from django_components import Component, register, types from django_components import Component, register, types
@ -9,7 +9,7 @@ DESCRIPTION = "Dynamically render different component versions. Use for A/B test
@register("offer_card_old") @register("offer_card_old")
class OfferCardOld(Component): class OfferCardOld(Component):
class Kwargs(NamedTuple): class Kwargs:
savings_percent: int savings_percent: int
def get_template_data(self, args, kwargs, slots, context): def get_template_data(self, args, kwargs, slots, context):
@ -45,7 +45,7 @@ class OfferCardNew(OfferCardOld):
@register("offer_card") @register("offer_card")
class OfferCard(Component): class OfferCard(Component):
class Kwargs(NamedTuple): class Kwargs:
savings_percent: int savings_percent: int
use_new_version: Optional[bool] = None use_new_version: Optional[bool] = None

View file

@ -1,4 +1,4 @@
from typing import Dict, List, NamedTuple from typing import Dict, List
from django_components import Component, register, types from django_components import Component, register, types
@ -14,7 +14,7 @@ error_rate = {
@register("api_widget") @register("api_widget")
class ApiWidget(Component): class ApiWidget(Component):
class Kwargs(NamedTuple): class Kwargs:
simulate_error: bool = False simulate_error: bool = False
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):

View file

@ -1,6 +1,5 @@
# ruff: noqa: S311 # ruff: noqa: S311
import random import random
from typing import NamedTuple
from django_components import Component, register, types from django_components import Component, register, types
@ -9,7 +8,7 @@ DESCRIPTION = "A component that catches errors and displays fallback content, si
@register("weather_widget") @register("weather_widget")
class WeatherWidget(Component): class WeatherWidget(Component):
class Kwargs(NamedTuple): class Kwargs:
location: str location: str
simulate_error: bool = False simulate_error: bool = False

View file

@ -1,4 +1,4 @@
from typing import Any, Dict, List, NamedTuple, Optional, Set, Tuple from typing import Any, Dict, List, Optional, Set, Tuple
from django_components import Component, Slot, register, types from django_components import Component, Slot, register, types
@ -9,7 +9,7 @@ DESCRIPTION = "Form that automatically arranges fields in a grid and generates l
class FormGrid(Component): class FormGrid(Component):
"""Form that automatically arranges fields in a grid and generates labels.""" """Form that automatically arranges fields in a grid and generates labels."""
class Kwargs(NamedTuple): class Kwargs:
editable: bool = True editable: bool = True
method: str = "post" method: str = "post"
form_content_attrs: Optional[dict] = None form_content_attrs: Optional[dict] = None
@ -114,7 +114,7 @@ def prepare_form_grid(slots: Dict[str, Slot]):
# Case: Component user didn't explicitly define how to render the label # Case: Component user didn't explicitly define how to render the label
# We will create the label for the field automatically # We will create the label for the field automatically
label = FormGridLabel.render( label = FormGridLabel.render(
kwargs=FormGridLabel.Kwargs(field_name=field_name), kwargs=FormGridLabel.Kwargs(field_name=field_name), # type: ignore[call-arg]
deps_strategy="ignore", deps_strategy="ignore",
) )
@ -134,7 +134,7 @@ class FormGridLabel(Component):
</label> </label>
""" """
class Kwargs(NamedTuple): class Kwargs:
field_name: str field_name: str
title: Optional[str] = None title: Optional[str] = None

View file

@ -9,7 +9,7 @@ DESCRIPTION = "Handle the entire form submission flow in a single file and witho
@register("thank_you_message") @register("thank_you_message")
class ThankYouMessage(Component): class ThankYouMessage(Component):
class Kwargs(NamedTuple): class Kwargs:
name: str name: str
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):

View file

@ -1,5 +1,3 @@
from typing import NamedTuple
from django_components import Component, register, types from django_components import Component, register, types
DESCRIPTION = "Use HTML fragments (partials) with HTMX, AlpineJS, or plain JS." DESCRIPTION = "Use HTML fragments (partials) with HTMX, AlpineJS, or plain JS."
@ -9,7 +7,7 @@ DESCRIPTION = "Use HTML fragments (partials) with HTMX, AlpineJS, or plain JS."
class SimpleFragment(Component): class SimpleFragment(Component):
"""A simple fragment with JS and CSS.""" """A simple fragment with JS and CSS."""
class Kwargs(NamedTuple): class Kwargs:
type: str type: str
template: types.django_html = """ template: types.django_html = """
@ -37,7 +35,7 @@ class SimpleFragment(Component):
class AlpineFragment(Component): class AlpineFragment(Component):
"""A fragment that defines an AlpineJS component.""" """A fragment that defines an AlpineJS component."""
class Kwargs(NamedTuple): class Kwargs:
type: str type: str
# The fragment is wrapped in `<template x-if="false">` so that we prevent # The fragment is wrapped in `<template x-if="false">` so that we prevent

View file

@ -1,5 +1,3 @@
from typing import NamedTuple
from django_components import Component, register, types from django_components import Component, register, types
DESCRIPTION = "100 nested components? Not a problem! Handle recursive rendering out of the box." DESCRIPTION = "100 nested components? Not a problem! Handle recursive rendering out of the box."
@ -7,7 +5,7 @@ DESCRIPTION = "100 nested components? Not a problem! Handle recursive rendering
@register("recursion") @register("recursion")
class Recursion(Component): class Recursion(Component):
class Kwargs(NamedTuple): class Kwargs:
current_depth: int = 0 current_depth: int = 0
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):

View file

@ -263,7 +263,7 @@ class Tablist(Component):
{% endprovide %} {% endprovide %}
""" """
class Kwargs(NamedTuple): class Kwargs:
id: Optional[str] = None id: Optional[str] = None
name: str = "Tabs" name: str = "Tabs"
selected_tab: Optional[str] = None selected_tab: Optional[str] = None
@ -341,7 +341,7 @@ class Tab(Component):
{% endprovide %} {% endprovide %}
""" """
class Kwargs(NamedTuple): class Kwargs:
header: str header: str
disabled: bool = False disabled: bool = False
id: Optional[str] = None id: Optional[str] = None

View file

@ -393,21 +393,21 @@ Avoid needless errors with [type hints and runtime input validation](https://dja
To opt-in to input validation, define types for component's args, kwargs, slots: To opt-in to input validation, define types for component's args, kwargs, slots:
```py ```py
from typing import NamedTuple, Optional from typing import Optional
from django.template import Context from django.template import Context
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
size: int size: int
text: str text: str
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
another: int another: int
maybe_var: Optional[int] = None # May be omitted maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple): class Slots:
my_slot: Optional[SlotInput] = None my_slot: Optional[SlotInput] = None
another_slot: SlotInput another_slot: SlotInput

View file

@ -1,5 +1,3 @@
from typing import NamedTuple
from django_components import Component, register from django_components import Component, register
@ -15,7 +13,7 @@ class Calendar(Component):
js_file = "calendar/calendar.js" js_file = "calendar/calendar.js"
# This component takes one parameter, a date string to show in the template # This component takes one parameter, a date string to show in the template
class Kwargs(NamedTuple): class Kwargs:
date: str date: str
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):
@ -46,7 +44,7 @@ class CalendarRelative(Component):
js_file = "calendar.js" js_file = "calendar.js"
# This component takes one parameter, a date string to show in the template # This component takes one parameter, a date string to show in the template
class Kwargs(NamedTuple): class Kwargs:
date: str date: str
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):

View file

@ -1,4 +1,4 @@
from typing import Any, NamedTuple from typing import Any
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
@ -17,7 +17,7 @@ class CalendarNested(Component):
js_file = "calendar.js" js_file = "calendar.js"
# This component takes one parameter, a date string to show in the template # This component takes one parameter, a date string to show in the template
class Kwargs(NamedTuple): class Kwargs:
date: str date: str
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):

View file

@ -207,7 +207,7 @@ class CreateCommand(ComponentCommand):
js_file = "{js_filename}" js_file = "{js_filename}"
css_file = "{css_filename}" css_file = "{css_filename}"
class Kwargs(NamedTuple): class Kwargs:
param: str param: str
class Defaults: class Defaults:

View file

@ -1,6 +1,6 @@
# ruff: noqa: ARG002, N804, N805 # ruff: noqa: ARG002, N804, N805
import sys import sys
from dataclasses import dataclass from dataclasses import dataclass, is_dataclass
from inspect import signature from inspect import signature
from types import MethodType from types import MethodType
from typing import ( from typing import (
@ -78,7 +78,14 @@ from django_components.template import cache_component_template_file, prepare_co
from django_components.util.context import gen_context_processors_data, snapshot_context from django_components.util.context import gen_context_processors_data, snapshot_context
from django_components.util.exception import component_error_message from django_components.util.exception import component_error_message
from django_components.util.logger import trace_component_msg from django_components.util.logger import trace_component_msg
from django_components.util.misc import default, gen_id, hash_comp_cls, is_generator, to_dict from django_components.util.misc import (
convert_class_to_namedtuple,
default,
gen_id,
hash_comp_cls,
is_generator,
to_dict,
)
from django_components.util.template_tag import TagAttr from django_components.util.template_tag import TagAttr
from django_components.util.weakref import cached_ref from django_components.util.weakref import cached_ref
@ -311,7 +318,7 @@ class ComponentVars(NamedTuple):
@register("table") @register("table")
class Table(Component): class Table(Component):
class Args(NamedTuple): class Args:
page: int page: int
per_page: int per_page: int
@ -363,7 +370,7 @@ class ComponentVars(NamedTuple):
@register("table") @register("table")
class Table(Component): class Table(Component):
class Kwargs(NamedTuple): class Kwargs:
page: int page: int
per_page: int per_page: int
@ -415,7 +422,7 @@ class ComponentVars(NamedTuple):
@register("table") @register("table")
class Table(Component): class Table(Component):
class Slots(NamedTuple): class Slots:
footer: SlotInput footer: SlotInput
template = ''' template = '''
@ -506,6 +513,34 @@ class ComponentMeta(ComponentMediaMeta):
attrs["template_file"] = attrs.pop("template_name") attrs["template_file"] = attrs.pop("template_name")
attrs["template_name"] = ComponentTemplateNameDescriptor() attrs["template_name"] = ComponentTemplateNameDescriptor()
# Allow to define data classes (`Args`, `Kwargs`, `Slots`, `TemplateData`, `JsData`, `CssData`)
# without explicitly subclassing anything. In which case we make them into a subclass of `NamedTuple`.
# In other words:
# ```py
# class MyTable(Component):
# class Kwargs(NamedTuple):
# ...
# ```
# Can be simplified to:
# ```py
# class MyTable(Component):
# class Kwargs:
# ...
# ```
for data_class_name in ["Args", "Kwargs", "Slots", "TemplateData", "JsData", "CssData"]:
data_class = attrs.get(data_class_name)
# Not a class
if data_class is None or not isinstance(data_class, type):
continue
# Is dataclass
if is_dataclass(data_class):
continue
# Has base class(es)
has_parents = data_class.__bases__ != (object,)
if has_parents:
continue
attrs[data_class_name] = convert_class_to_namedtuple(data_class)
cls = cast("Type[Component]", super().__new__(mcs, name, bases, attrs)) cls = cast("Type[Component]", super().__new__(mcs, name, bases, attrs))
# If the component defined `template_file`, then associate this Component class # If the component defined `template_file`, then associate this Component class
@ -598,11 +633,10 @@ class Component(metaclass=ComponentMeta):
will be the instance of this class: will be the instance of this class:
```py ```py
from typing import NamedTuple
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class Args(NamedTuple): class Args:
color: str color: str
size: int size: int
@ -615,15 +649,6 @@ class Component(metaclass=ComponentMeta):
} }
``` ```
The constructor of this class MUST accept positional arguments:
```py
Args(*args)
```
As such, a good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
Use `Args` to: Use `Args` to:
- Validate the input at runtime. - Validate the input at runtime.
@ -640,6 +665,20 @@ class Component(metaclass=ComponentMeta):
) )
``` ```
If you do not specify any bases, the `Args` class will be automatically
converted to a `NamedTuple`:
`class Args:` -> `class Args(NamedTuple):`
If you explicitly set bases, the constructor of this class MUST accept positional arguments:
```py
Args(*args)
```
As such, a good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation). Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
""" """
@ -654,11 +693,10 @@ class Component(metaclass=ComponentMeta):
will be the instance of this class: will be the instance of this class:
```py ```py
from typing import NamedTuple
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class Kwargs(NamedTuple): class Kwargs:
color: str color: str
size: int size: int
@ -671,16 +709,6 @@ class Component(metaclass=ComponentMeta):
} }
``` ```
The constructor of this class MUST accept keyword arguments:
```py
Kwargs(**kwargs)
```
As such, a good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
Use `Kwargs` to: Use `Kwargs` to:
- Validate the input at runtime. - Validate the input at runtime.
@ -697,6 +725,21 @@ class Component(metaclass=ComponentMeta):
) )
``` ```
If you do not specify any bases, the `Kwargs` class will be automatically
converted to a `NamedTuple`:
`class Kwargs:` -> `class Kwargs(NamedTuple):`
If you explicitly set bases, the constructor of this class MUST accept keyword arguments:
```py
Kwargs(**kwargs)
```
As such, a good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation). Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
""" """
@ -711,11 +754,10 @@ class Component(metaclass=ComponentMeta):
will be the instance of this class: will be the instance of this class:
```py ```py
from typing import NamedTuple
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
class Table(Component): class Table(Component):
class Slots(NamedTuple): class Slots:
header: SlotInput header: SlotInput
footer: Slot footer: Slot
@ -728,16 +770,6 @@ class Component(metaclass=ComponentMeta):
} }
``` ```
The constructor of this class MUST accept keyword arguments:
```py
Slots(**slots)
```
As such, a good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
Use `Slots` to: Use `Slots` to:
- Validate the input at runtime. - Validate the input at runtime.
@ -757,6 +789,21 @@ class Component(metaclass=ComponentMeta):
) )
``` ```
If you do not specify any bases, the `Slots` class will be automatically
converted to a `NamedTuple`:
`class Slots:` -> `class Slots(NamedTuple):`
If you explicitly set bases, the constructor of this class MUST accept keyword arguments:
```py
Slots(**slots)
```
As such, a good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation). Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
!!! info !!! info
@ -1081,18 +1128,17 @@ class Component(metaclass=ComponentMeta):
**Example:** **Example:**
```py ```py
from typing import NamedTuple
from django.template import Context from django.template import Context
from django_components import Component, SlotInput from django_components import Component, SlotInput
class MyComponent(Component): class MyComponent(Component):
class Args(NamedTuple): class Args:
color: str color: str
class Kwargs(NamedTuple): class Kwargs:
size: int size: int
class Slots(NamedTuple): class Slots:
footer: SlotInput footer: SlotInput
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context): def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
@ -1123,7 +1169,7 @@ class Component(metaclass=ComponentMeta):
```py ```py
class MyComponent(Component): class MyComponent(Component):
class TemplateData(NamedTuple): class TemplateData:
color: str color: str
size: int size: int
@ -1156,22 +1202,22 @@ class Component(metaclass=ComponentMeta):
If set and not `None`, then this class will be instantiated with the dictionary returned from If set and not `None`, then this class will be instantiated with the dictionary returned from
[`get_template_data()`](../api#django_components.Component.get_template_data) to validate the data. [`get_template_data()`](../api#django_components.Component.get_template_data) to validate the data.
The constructor of this class MUST accept keyword arguments: Use `TemplateData` to:
```py - Validate the data returned from
TemplateData(**template_data) [`get_template_data()`](../api#django_components.Component.get_template_data) at runtime.
``` - Set type hints for this data.
- Document the component data.
You can also return an instance of `TemplateData` directly from You can also return an instance of `TemplateData` directly from
[`get_template_data()`](../api#django_components.Component.get_template_data) [`get_template_data()`](../api#django_components.Component.get_template_data)
to get type hints: to get type hints:
```py ```py
from typing import NamedTuple
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class TemplateData(NamedTuple): class TemplateData:
color: str color: str
size: int size: int
@ -1182,17 +1228,16 @@ class Component(metaclass=ComponentMeta):
) )
``` ```
The constructor of this class MUST accept keyword arguments:
```py
TemplateData(**template_data)
```
A good starting point is to set this field to a subclass of A good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass). or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
Use `TemplateData` to:
- Validate the data returned from
[`get_template_data()`](../api#django_components.Component.get_template_data) at runtime.
- Set type hints for this data.
- Document the component data.
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation). Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
!!! info !!! info
@ -1376,13 +1421,13 @@ class Component(metaclass=ComponentMeta):
from django_components import Component, SlotInput from django_components import Component, SlotInput
class MyComponent(Component): class MyComponent(Component):
class Args(NamedTuple): class Args:
color: str color: str
class Kwargs(NamedTuple): class Kwargs:
size: int size: int
class Slots(NamedTuple): class Slots:
footer: SlotInput footer: SlotInput
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context): def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
@ -1413,7 +1458,7 @@ class Component(metaclass=ComponentMeta):
```py ```py
class MyComponent(Component): class MyComponent(Component):
class JsData(NamedTuple): class JsData:
color: str color: str
size: int size: int
@ -1439,22 +1484,22 @@ class Component(metaclass=ComponentMeta):
If set and not `None`, then this class will be instantiated with the dictionary returned from If set and not `None`, then this class will be instantiated with the dictionary returned from
[`get_js_data()`](../api#django_components.Component.get_js_data) to validate the data. [`get_js_data()`](../api#django_components.Component.get_js_data) to validate the data.
The constructor of this class MUST accept keyword arguments: Use `JsData` to:
```py - Validate the data returned from
JsData(**js_data) [`get_js_data()`](../api#django_components.Component.get_js_data) at runtime.
``` - Set type hints for this data.
- Document the component data.
You can also return an instance of `JsData` directly from You can also return an instance of `JsData` directly from
[`get_js_data()`](../api#django_components.Component.get_js_data) [`get_js_data()`](../api#django_components.Component.get_js_data)
to get type hints: to get type hints:
```py ```py
from typing import NamedTuple
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class JsData(NamedTuple): class JsData(
color: str color: str
size: int size: int
@ -1465,17 +1510,16 @@ class Component(metaclass=ComponentMeta):
) )
``` ```
The constructor of this class MUST accept keyword arguments:
```py
JsData(**js_data)
```
A good starting point is to set this field to a subclass of A good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass). or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
Use `JsData` to:
- Validate the data returned from
[`get_js_data()`](../api#django_components.Component.get_js_data) at runtime.
- Set type hints for this data.
- Document the component data.
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation). Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
!!! info !!! info
@ -1664,18 +1708,17 @@ class Component(metaclass=ComponentMeta):
**Example:** **Example:**
```py ```py
from typing import NamedTuple
from django.template import Context from django.template import Context
from django_components import Component, SlotInput from django_components import Component, SlotInput
class MyComponent(Component): class MyComponent(Component):
class Args(NamedTuple): class Args:
color: str color: str
class Kwargs(NamedTuple): class Kwargs:
size: int size: int
class Slots(NamedTuple): class Slots:
footer: SlotInput footer: SlotInput
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context): def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
@ -1705,7 +1748,7 @@ class Component(metaclass=ComponentMeta):
```py ```py
class MyComponent(Component): class MyComponent(Component):
class CssData(NamedTuple): class CssData:
color: str color: str
size: int size: int
@ -1731,22 +1774,22 @@ class Component(metaclass=ComponentMeta):
If set and not `None`, then this class will be instantiated with the dictionary returned from If set and not `None`, then this class will be instantiated with the dictionary returned from
[`get_css_data()`](../api#django_components.Component.get_css_data) to validate the data. [`get_css_data()`](../api#django_components.Component.get_css_data) to validate the data.
The constructor of this class MUST accept keyword arguments: Use `CssData` to:
```py - Validate the data returned from
CssData(**css_data) [`get_css_data()`](../api#django_components.Component.get_css_data) at runtime.
``` - Set type hints for this data.
- Document the component data.
You can also return an instance of `CssData` directly from You can also return an instance of `CssData` directly from
[`get_css_data()`](../api#django_components.Component.get_css_data) [`get_css_data()`](../api#django_components.Component.get_css_data)
to get type hints: to get type hints:
```py ```py
from typing import NamedTuple
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class CssData(NamedTuple): class CssData:
color: str color: str
size: int size: int
@ -1757,17 +1800,16 @@ class Component(metaclass=ComponentMeta):
) )
``` ```
The constructor of this class MUST accept keyword arguments:
```py
CssData(**css_data)
```
A good starting point is to set this field to a subclass of A good starting point is to set this field to a subclass of
[`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) [`NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple)
or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass). or a [dataclass](https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass).
Use `CssData` to:
- Validate the data returned from
[`get_css_data()`](../api#django_components.Component.get_css_data) at runtime.
- Set type hints for this data.
- Document the component data.
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation). Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
!!! info !!! info
@ -2567,7 +2609,7 @@ class Component(metaclass=ComponentMeta):
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class Args(NamedTuple): class Args:
page: int page: int
per_page: int per_page: int
@ -2635,7 +2677,7 @@ class Component(metaclass=ComponentMeta):
from django_components import Component from django_components import Component
class Table(Component): class Table(Component):
class Kwargs(NamedTuple): class Kwargs:
page: int page: int
per_page: int per_page: int
@ -2706,7 +2748,7 @@ class Component(metaclass=ComponentMeta):
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
class Table(Component): class Table(Component):
class Slots(NamedTuple): class Slots:
header: SlotInput header: SlotInput
footer: SlotInput footer: SlotInput
@ -3314,19 +3356,19 @@ class Component(metaclass=ComponentMeta):
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation). Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
```python ```python
from typing import NamedTuple, Optional from typing import Optional
from django_components import Component, Slot, SlotInput from django_components import Component, Slot, SlotInput
# Define the component with the types # Define the component with the types
class Button(Component): class Button(Component):
class Args(NamedTuple): class Args:
name: str name: str
class Kwargs(NamedTuple): class Kwargs:
surname: str surname: str
age: int age: int
class Slots(NamedTuple): class Slots:
my_slot: Optional[SlotInput] = None my_slot: Optional[SlotInput] = None
footer: SlotInput footer: SlotInput

View file

@ -1,4 +1,4 @@
from typing import NamedTuple, Optional, cast from typing import Optional, cast
from django.template import Context, Template from django.template import Context, Template
from django.template.exceptions import TemplateSyntaxError from django.template.exceptions import TemplateSyntaxError
@ -115,10 +115,10 @@ class ErrorFallback(Component):
Remember to define the `content` slot as function, so it's evaluated from inside of `ErrorFallback`. Remember to define the `content` slot as function, so it's evaluated from inside of `ErrorFallback`.
""" """
class Kwargs(NamedTuple): class Kwargs:
fallback: Optional[str] = None fallback: Optional[str] = None
class Slots(NamedTuple): class Slots:
default: Optional[SlotInput] = None default: Optional[SlotInput] = None
content: Optional[SlotInput] = None content: Optional[SlotInput] = None
fallback: Optional[SlotInput] = None fallback: Optional[SlotInput] = None

View file

@ -1,3 +1,4 @@
import sys
from functools import wraps from functools import wraps
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
@ -14,7 +15,7 @@ from typing import (
TypeVar, TypeVar,
Union, Union,
) )
from weakref import ref from weakref import ReferenceType, ref
import django.urls import django.urls
from django.template import Context, Origin, Template from django.template import Context, Origin, Template
@ -33,6 +34,13 @@ if TYPE_CHECKING:
from django_components.slots import Slot, SlotNode, SlotResult from django_components.slots import Slot, SlotNode, SlotResult
# NOTE: `ReferenceType` is NOT a generic pre-3.9
if sys.version_info >= (3, 9):
ComponentInstanceRef = ReferenceType["Component"]
else:
ComponentInstanceRef = ReferenceType
TCallable = TypeVar("TCallable", bound=Callable) TCallable = TypeVar("TCallable", bound=Callable)
TClass = TypeVar("TClass", bound=Type[Any]) TClass = TypeVar("TClass", bound=Type[Any])
@ -265,16 +273,27 @@ class ExtensionComponentConfig:
This attribute holds the owner [`Component`](./api.md#django_components.Component) instance This attribute holds the owner [`Component`](./api.md#django_components.Component) instance
that this extension is defined on. that this extension is defined on.
Some extensions like Storybook run outside of the component lifecycle,
so there is no component instance available when running extension's methods.
In such cases, this attribute will be `None`.
""" """
component: Optional[Component] = None
if self._component_ref is not None:
component = self._component_ref() component = self._component_ref()
if component is None: if component is None:
raise RuntimeError("Component has been garbage collected") raise RuntimeError("Component has been garbage collected")
return component return component
def __init__(self, component: "Component") -> None: def __init__(self, component: "Optional[Component]") -> None:
# NOTE: Use weak reference to avoid a circular reference between the component instance # NOTE: Use weak reference to avoid a circular reference between the component instance
# and the extension class. # and the extension class.
self._component_ref = ref(component) if component is not None:
self._component_ref: Optional[ComponentInstanceRef] = ref(component)
else:
# NOTE: Some extensions like Storybook run outside of the component lifecycle,
# so there is no component instance available when running extension's methods.
self._component_ref = None
# TODO_v1 - Delete # TODO_v1 - Delete

View file

@ -16,6 +16,14 @@ class DependenciesExtension(ComponentExtension):
# Cache the component's JS and CSS scripts when the class is created, so that # Cache the component's JS and CSS scripts when the class is created, so that
# components' JS/CSS files are accessible even before having to render the component first. # components' JS/CSS files are accessible even before having to render the component first.
#
# This is important for the scenario when the web server may restart in a middle of user
# session. In which case, if we did not cache the JS/CSS, then user may fail to retrieve
# JS/CSS of some component.
#
# Component JS/CSS is then also cached after each time a component is rendered.
# That way, if the component JS/CSS cache is smaller than the total number of
# components/assets, we add back the most-recent entries.
def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None: def on_component_class_created(self, ctx: OnComponentClassCreatedContext) -> None:
cache_component_js(ctx.component_cls, force=True) cache_component_js(ctx.component_cls, force=True)
cache_component_css(ctx.component_cls, force=True) cache_component_css(ctx.component_cls, force=True)

View file

@ -394,7 +394,6 @@ slot content function.
**Example:** **Example:**
```python ```python
from typing import NamedTuple
from typing_extensions import TypedDict from typing_extensions import TypedDict
from django_components import Component, SlotInput from django_components import Component, SlotInput
@ -402,7 +401,7 @@ class TableFooterSlotData(TypedDict):
page_number: int page_number: int
class Table(Component): class Table(Component):
class Slots(NamedTuple): class Slots:
header: SlotInput header: SlotInput
footer: SlotInput[TableFooterSlotData] footer: SlotInput[TableFooterSlotData]

View file

@ -1,8 +1,10 @@
import re import re
import sys import sys
from collections import namedtuple
from dataclasses import asdict, is_dataclass from dataclasses import asdict, is_dataclass
from hashlib import md5 from hashlib import md5
from importlib import import_module from importlib import import_module
from inspect import getmembers
from itertools import chain from itertools import chain
from types import ModuleType from types import ModuleType
from typing import ( from typing import (
@ -267,3 +269,56 @@ def format_as_ascii_table(
def is_generator(obj: Any) -> bool: def is_generator(obj: Any) -> bool:
"""Check if an object is a generator with send method.""" """Check if an object is a generator with send method."""
return hasattr(obj, "send") return hasattr(obj, "send")
def convert_class_to_namedtuple(cls: Type[Any]) -> Type[Tuple[Any, ...]]:
# Construct fields for a NamedTuple. Unfortunately one can't further subclass the subclass of `NamedTuple`,
# so we need to construct a new class with the same fields.
# NamedTuple has:
# - Required fields, which are defined without values (annotations only)
# - Optional fields with defaults
# ```py
# class Z:
# b: str # Required, annotated
# a: int = None # Optional, annotated
# c = 1 # NOT A FIELD! Class var!
# ```
# Annotations are stored in `X.__annotations__`, while the defaults are regular class attributes
# NOTE: We ignore dunder methods
# NOTE 2: All fields with default values must come after fields without defaults.
field_names = list(cls.__annotations__.keys())
# Get default values from the original class and set them on the new NamedTuple class
field_names_set = set(field_names)
defaults = {}
class_attrs = {}
for name, value in getmembers(cls):
if name.startswith("__"):
continue
# Field default
if name in field_names_set:
defaults[name] = value
else:
# Class attribute
class_attrs[name] = value
# Figure out how many tuple fields have defaults. We need to know this
# because NamedTuple functional syntax uses the pattern where defaults
# are applied from the end.
# Final call then looks like this:
# `namedtuple("MyClass", ["a", "b", "c", "d"], defaults=[3, 4])`
# with defaults c=3 and d=4
num_fields_with_defaults = len(defaults)
if num_fields_with_defaults:
defaults_list = [defaults[name] for name in field_names[-num_fields_with_defaults:]]
else:
defaults_list = []
tuple_cls = namedtuple(cls.__name__, field_names, defaults=defaults_list) # type: ignore[misc] # noqa: PYI024
# `collections.namedtuple` doesn't allow to specify annotations, so we pass them afterwards
tuple_cls.__annotations__ = cls.__annotations__
# Likewise, `collections.namedtuple` doesn't allow to specify class vars
for name, value in class_attrs.items():
setattr(tuple_cls, name, value)
return tuple_cls

View file

@ -5,7 +5,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
import os import os
import re import re
from typing import Any, List, Literal, NamedTuple, Optional from typing import Any, List, Literal, Optional
import pytest import pytest
from django.conf import settings from django.conf import settings
@ -591,23 +591,23 @@ class TestComponentRenderAPI:
class TestComponent(Component): class TestComponent(Component):
template = "" template = ""
class Args(NamedTuple): class Args:
variable: int variable: int
another: str another: str
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
another: int another: int
class Slots(NamedTuple): class Slots:
my_slot: SlotInput my_slot: SlotInput
def get_template_data(self, args, kwargs, slots, context): def get_template_data(self, args, kwargs, slots, context):
nonlocal called nonlocal called
called = True called = True
assert self.args == TestComponent.Args(123, "str") assert self.args == TestComponent.Args(123, "str") # type: ignore[call-arg]
assert self.kwargs == TestComponent.Kwargs(variable="test", another=1) assert self.kwargs == TestComponent.Kwargs(variable="test", another=1) # type: ignore[call-arg]
assert isinstance(self.slots, TestComponent.Slots) assert isinstance(self.slots, TestComponent.Slots)
assert isinstance(self.slots.my_slot, Slot) assert isinstance(self.slots.my_slot, Slot)
assert self.slots.my_slot() == "MY_SLOT" assert self.slots.my_slot() == "MY_SLOT"
@ -789,15 +789,15 @@ class TestComponentTemplateVars:
def test_args_kwargs_slots__simple_typed(self): def test_args_kwargs_slots__simple_typed(self):
class TestComponent(Component): class TestComponent(Component):
class Args(NamedTuple): class Args:
variable: int variable: int
another: str another: str
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
another: int another: int
class Slots(NamedTuple): class Slots:
my_slot: SlotInput my_slot: SlotInput
template: types.django_html = """ template: types.django_html = """
@ -898,15 +898,15 @@ class TestComponentTemplateVars:
""" """
class TestComponent(Component): class TestComponent(Component):
class Args(NamedTuple): class Args:
variable: int variable: int
another: str another: str
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
another: int another: int
class Slots(NamedTuple): class Slots:
my_slot: SlotInput my_slot: SlotInput
template: types.django_html = """ template: types.django_html = """

View file

@ -1,5 +1,4 @@
import re import re
from typing import NamedTuple
import pytest import pytest
from django.template import Context, Template from django.template import Context, Template
@ -21,7 +20,7 @@ class TestDynamicComponent:
Variable: <strong>{{ variable }}</strong> Variable: <strong>{{ variable }}</strong>
""" """
class Kwargs(NamedTuple): class Kwargs:
variable: str variable: str
variable2: str variable2: str

View file

@ -1,5 +1,3 @@
from typing import NamedTuple
import pytest import pytest
from django.template import Context, Template from django.template import Context, Template
from django.template.exceptions import TemplateSyntaxError from django.template.exceptions import TemplateSyntaxError
@ -313,7 +311,7 @@ class TestErrorFallbackComponent:
def test_error_fallback_nested_inside_another(self, components_settings): def test_error_fallback_nested_inside_another(self, components_settings):
@register("broken") @register("broken")
class BrokenComponent(Component): class BrokenComponent(Component):
class Kwargs(NamedTuple): class Kwargs:
msg: str msg: str
def on_render(self, context: Context, template: Template): def on_render(self, context: Context, template: Template):

View file

@ -16,7 +16,7 @@ setup_test_config()
@djc_test @djc_test
class TestComponentTyping: class TestComponentTyping:
def test_data_methods_input_typed(self): def test_data_methods_input_typed_custom_classes(self):
template_called = False template_called = False
js_called = False js_called = False
css_called = False css_called = False
@ -84,6 +84,74 @@ class TestComponentTyping:
assert js_called assert js_called
assert css_called assert css_called
def test_data_methods_input_typed_default_classes(self):
template_called = False
js_called = False
css_called = False
class ButtonFooterSlotData(TypedDict):
value: int
class Button(Component):
class Args:
arg1: str
arg2: str
class Kwargs:
name: str
age: int
maybe_var: Optional[int] = None
class Slots:
header: SlotInput
footer: Optional[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, Button.Slots)
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, Button.Slots)
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, Button.Slots)
assert isinstance(context, Context)
template = "<button>Click me!</button>"
assert issubclass(Button.Args, tuple)
assert issubclass(Button.Kwargs, tuple)
assert issubclass(Button.Slots, tuple)
Button.render(
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
kwargs=Button.Kwargs(name="name", age=123), # type: ignore[call-arg]
slots=Button.Slots( # type: ignore[call-arg]
header="HEADER",
footer=Slot(lambda _ctx: "FOOTER"),
),
)
assert template_called
assert js_called
assert css_called
def test_data_methods_input_not_typed_by_default(self): def test_data_methods_input_not_typed_by_default(self):
template_called = False template_called = False
js_called = False js_called = False
@ -132,7 +200,71 @@ class TestComponentTyping:
assert js_called assert js_called
assert css_called assert css_called
def test_data_methods_output_typed(self): def test_data_methods_output_typed_default_classes(self):
template_called = False
js_called = False
css_called = False
class Button(Component):
class TemplateData:
data1: str
data2: int
class JsData:
js_data1: str
js_data2: int
class CssData:
css_data1: str
css_data2: int
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>"
assert issubclass(Button.TemplateData, tuple)
assert issubclass(Button.JsData, tuple)
assert issubclass(Button.CssData, tuple)
Button.render(
args=["arg1", "arg2"],
kwargs={"name": "name", "age": 123},
slots={
"header": "HEADER",
"footer": Slot(lambda _ctx: "FOOTER"),
},
)
assert template_called
assert js_called
assert css_called
def test_data_methods_output_typed_custom_classes(self):
template_called = False template_called = False
js_called = False js_called = False
css_called = False css_called = False
@ -489,43 +621,57 @@ class TestComponentTyping:
) )
def test_custom_args_class_raises_on_invalid(self): def test_custom_args_class_raises_on_invalid(self):
class Parent:
pass
class Button(Component): class Button(Component):
template = "Hello" template = "Hello"
class Args: class Args(Parent):
arg1: str arg1: str
arg2: str arg2: str
assert issubclass(Button.Args, Parent)
assert not issubclass(Button.Args, tuple)
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")): with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
Button.render( Button.render(
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg] args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
) )
class Button2(Component): class Parent2:
template = "Hello"
class Args:
arg1: str
arg2: str
def __init__(self, arg1: str, arg2: str): def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1 self.arg1 = arg1
self.arg2 = arg2 self.arg2 = arg2
class Button2(Component):
template = "Hello"
class Args(Parent2):
arg1: str
arg2: str
assert not issubclass(Button2.Args, tuple)
assert issubclass(Button2.Args, Parent2)
with pytest.raises(TypeError, match=re.escape("'Args' object is not iterable")): with pytest.raises(TypeError, match=re.escape("'Args' object is not iterable")):
Button2.render( Button2.render(
args=Button2.Args(arg1="arg1", arg2="arg2"), args=Button2.Args(arg1="arg1", arg2="arg2"),
) )
class Parent3:
def __iter__(self):
return iter([self.arg1, self.arg2]) # type: ignore[attr-defined]
class Button3(Component): class Button3(Component):
template = "Hello" template = "Hello"
class Args: class Args(Parent3):
arg1: str arg1: str
arg2: str arg2: str
def __iter__(self): assert not issubclass(Button3.Args, tuple)
return iter([self.arg1, self.arg2]) assert issubclass(Button3.Args, Parent3)
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")): with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
Button3.render( Button3.render(
@ -533,13 +679,7 @@ class TestComponentTyping:
) )
def test_custom_args_class_custom(self): def test_custom_args_class_custom(self):
class Button(Component): class Parent:
template = "Hello"
class Args:
arg1: str
arg2: str
def __init__(self, arg1: str, arg2: str): def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1 self.arg1 = arg1
self.arg2 = arg2 self.arg2 = arg2
@ -547,48 +687,69 @@ class TestComponentTyping:
def __iter__(self): def __iter__(self):
return iter([self.arg1, self.arg2]) return iter([self.arg1, self.arg2])
class Button(Component):
template = "Hello"
class Args(Parent):
arg1: str
arg2: str
Button.render( Button.render(
args=Button.Args(arg1="arg1", arg2="arg2"), args=Button.Args(arg1="arg1", arg2="arg2"),
) )
def test_custom_kwargs_class_raises_on_invalid(self): def test_custom_kwargs_class_raises_on_invalid(self):
class Parent:
pass
class Button(Component): class Button(Component):
template = "Hello" template = "Hello"
class Kwargs: class Kwargs(Parent):
arg1: str arg1: str
arg2: str arg2: str
assert not issubclass(Button.Kwargs, tuple)
assert issubclass(Button.Kwargs, Parent)
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")): with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
Button.render( Button.render(
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg] kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
) )
class Button2(Component): class Parent2:
template = "Hello"
class Kwargs:
arg1: str
arg2: str
def __init__(self, arg1: str, arg2: str): def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1 self.arg1 = arg1
self.arg2 = arg2 self.arg2 = arg2
class Button2(Component):
template = "Hello"
class Kwargs(Parent2):
arg1: str
arg2: str
assert not issubclass(Button2.Kwargs, tuple)
assert issubclass(Button2.Kwargs, Parent2)
with pytest.raises(TypeError, match=re.escape("'Kwargs' object is not iterable")): with pytest.raises(TypeError, match=re.escape("'Kwargs' object is not iterable")):
Button2.render( Button2.render(
kwargs=Button2.Kwargs(arg1="arg1", arg2="arg2"), kwargs=Button2.Kwargs(arg1="arg1", arg2="arg2"),
) )
class Parent3:
def _asdict(self):
return {"arg1": self.arg1, "arg2": self.arg2} # type: ignore[attr-defined]
class Button3(Component): class Button3(Component):
template = "Hello" template = "Hello"
class Kwargs: class Kwargs(Parent3):
arg1: str arg1: str
arg2: str arg2: str
def _asdict(self): assert not issubclass(Button3.Kwargs, tuple)
return {"arg1": self.arg1, "arg2": self.arg2} assert issubclass(Button3.Kwargs, Parent3)
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")): with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
Button3.render( Button3.render(
@ -596,13 +757,7 @@ class TestComponentTyping:
) )
def test_custom_kwargs_class_custom(self): def test_custom_kwargs_class_custom(self):
class Button(Component): class Parent:
template = "Hello"
class Kwargs:
arg1: str
arg2: str
def __init__(self, arg1: str, arg2: str): def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1 self.arg1 = arg1
self.arg2 = arg2 self.arg2 = arg2
@ -610,6 +765,16 @@ class TestComponentTyping:
def _asdict(self): def _asdict(self):
return {"arg1": self.arg1, "arg2": self.arg2} return {"arg1": self.arg1, "arg2": self.arg2}
class Button(Component):
template = "Hello"
class Kwargs(Parent):
arg1: str
arg2: str
assert not issubclass(Button.Kwargs, tuple)
assert issubclass(Button.Kwargs, Parent)
Button.render( Button.render(
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"),
) )
@ -618,14 +783,14 @@ class TestComponentTyping:
class Button(Component): class Button(Component):
template = "Hello" template = "Hello"
class Args(NamedTuple): class Args:
size: int size: int
class Kwargs(NamedTuple): class Kwargs:
color: str color: str
class ButtonExtra(Button): class ButtonExtra(Button):
class Args(NamedTuple): class Args:
name: str name: str
size: int size: int
@ -635,6 +800,6 @@ class TestComponentTyping:
assert ButtonExtra.Kwargs is Button.Kwargs assert ButtonExtra.Kwargs is Button.Kwargs
ButtonExtra.render( ButtonExtra.render(
args=ButtonExtra.Args(name="John", size=30), args=ButtonExtra.Args(name="John", size=30), # type: ignore[call-arg]
kwargs=ButtonExtra.Kwargs(color="red"), kwargs=ButtonExtra.Kwargs(color="red"), # type: ignore[call-arg]
) )

View file

@ -242,6 +242,11 @@ class TestExtensions:
del TestAccessComp del TestAccessComp
gc.collect() gc.collect()
@djc_test(components_settings={"extensions": [DummyExtension]})
def test_instantiate_ext_component_config_none(self):
config = DummyExtension.ComponentConfig(None)
assert isinstance(config, DummyExtension.ComponentConfig)
def test_raises_on_extension_name_conflict(self): def test_raises_on_extension_name_conflict(self):
@djc_test(components_settings={"extensions": [RenderExtension]}) @djc_test(components_settings={"extensions": [RenderExtension]})
def inner(): def inner():

View file

@ -8,8 +8,6 @@ This file can be deleted after Django 5.2 reached end of life.
See https://github.com/django-components/django-components/issues/1323#issuecomment-3163478287. See https://github.com/django-components/django-components/issues/1323#issuecomment-3163478287.
""" """
from typing import NamedTuple
import pytest import pytest
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import render from django.shortcuts import render
@ -53,7 +51,7 @@ class TestTemplatePartialsIntegration:
})() })()
""" # noqa: E501 """ # noqa: E501
class Kwargs(NamedTuple): class Kwargs:
date: str date: str
def get_template_data(self, args, kwargs: Kwargs, slots, context): def get_template_data(self, args, kwargs: Kwargs, slots, context):