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

This commit is contained in:
Juro Oravec 2025-10-20 21:20:41 +00:00
parent c66bd21231
commit 0255782ee2
31 changed files with 620 additions and 294 deletions

View file

@ -1,5 +1,56 @@
# 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):
...
```
## v0.142.3
#### 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:
```py
from typing import NamedTuple, Optional
from typing import Optional
from django.template import Context
from django_components import Component, Slot, SlotInput
class Button(Component):
class Args(NamedTuple):
class Args:
size: int
text: str
class Kwargs(NamedTuple):
class Kwargs:
variable: str
another: int
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
class Slots:
my_slot: Optional[SlotInput] = None
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`.
```djc_py
from typing import NamedTuple, Optional
from typing import Optional
from django_components import Component, SlotInput, register, types
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)
class MyMenu(Component):
# Define the types
class Args(NamedTuple):
class Args:
size: int
text: str
class Kwargs(NamedTuple):
class Kwargs:
vertical: Optional[bool] = None
klass: Optional[str] = None
style: Optional[str] = None
class Slots(NamedTuple):
class Slots:
default: Optional[SlotInput] = None
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):

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:
```djc_py
from typing import NamedTuple, Optional
from typing import Optional
from django.template import Context, Template
from django.utils.safestring import mark_safe
from django_components import Component, OnRenderGenerator, SlotInput, types
class ErrorFallback(Component):
class Slots(NamedTuple):
class Slots:
default: 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
class ProfileCard(Component):
class Kwargs(NamedTuple):
class Kwargs:
show_details: bool = True
```

View file

@ -15,7 +15,7 @@ Each method handles the data independently - you can define different data for t
```python
class ProfileCard(Component):
class Kwargs(NamedTuple):
class Kwargs:
user_id: int
show_details: bool
@ -51,7 +51,7 @@ If [`get_template_data()`](../../../reference/api/#django_components.Component.g
class ProfileCard(Component):
template_file = "profile_card.html"
class Kwargs(NamedTuple):
class Kwargs:
user_id: int
show_details: bool
@ -182,10 +182,10 @@ class ProfileCard(Component):
```py
class ProfileCard(Component):
class Args(NamedTuple):
class Args:
user_id: int
class Kwargs(NamedTuple):
class Kwargs:
show_details: bool
# Access inputs directly as parameters
@ -228,10 +228,10 @@ class ProfileCard(Component):
```py
class ProfileCard(Component):
class Args(NamedTuple):
class Args:
user_id: int
class Kwargs(NamedTuple):
class Kwargs:
show_details: bool
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")
class ProfileCard(Component):
class Kwargs(NamedTuple):
class Kwargs:
show_details: bool
class Defaults:
@ -344,7 +344,7 @@ class ProfileCard(Component):
```py
class ProfileCard(Component):
class Kwargs(NamedTuple):
class Kwargs:
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).
```python
from typing import NamedTuple, Optional
from typing import Optional
from django_components import Component, SlotInput
class Button(Component):
class Args(NamedTuple):
class Args:
name: str
class Kwargs(NamedTuple):
class Kwargs:
surname: str
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
class Slots:
my_slot: Optional[SlotInput] = None
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.
```python
from typing import NamedTuple
from django_components import Component
class Button(Component):
class TemplateData(NamedTuple):
class TemplateData(
data1: str
data2: int
class JsData(NamedTuple):
class JsData:
js_data1: str
js_data2: int
class CssData(NamedTuple):
class CssData:
css_data1: str
css_data2: int

View file

@ -92,7 +92,7 @@ With `Args` class:
from django_components import Component
class Table(Component):
class Args(NamedTuple):
class Args:
page: int
per_page: int
@ -137,7 +137,7 @@ With `Kwargs` class:
from django_components import Component
class Table(Component):
class Kwargs(NamedTuple):
class Kwargs:
page: int
per_page: int
@ -182,7 +182,7 @@ With `Slots` class:
from django_components import Component, Slot, SlotInput
class Table(Component):
class Slots(NamedTuple):
class Slots:
header: 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"`:
```python
from typing import NamedTuple
from django_components import Component, register
@register("button")
class Button(Component):
template_file = "button.html"
class Kwargs(NamedTuple):
class Kwargs:
name: 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.
```python
from typing import NamedTuple, Optional
from typing import Optional
from django_components import Component, SlotInput
class Button(Component):
template_file = "button.html"
class Args(NamedTuple):
class Args:
name: str
class Kwargs(NamedTuple):
class Kwargs:
surname: str
age: int
class Slots(NamedTuple):
class Slots:
footer: Optional[SlotInput] = None
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.
```python
from typing import NamedTuple, Optional
from typing import Optional
from django_components import Component, SlotInput
class Button(Component):
template_file = "button.html"
class Args(NamedTuple):
class Args(
name: str
class Kwargs(NamedTuple):
class Kwargs:
surname: str
age: int
class Slots(NamedTuple):
class Slots:
footer: Optional[SlotInput] = None
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).
```python
from typing import NamedTuple, Optional
from typing import Optional
from django_components import Component, Slot, SlotInput
# Define the component with the types
class Button(Component):
class Args(NamedTuple):
class Args(
name: str
class Kwargs(NamedTuple):
class Kwargs:
surname: str
age: int
class Slots(NamedTuple):
class Slots:
my_slot: Optional[SlotInput] = None
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.
```py
from typing import NamedTuple, Optional
from typing import Optional
from django.template import Context
from django_components import Component, SlotInput
class Button(Component):
class Args(NamedTuple):
class Args:
size: int
text: str
class Kwargs(NamedTuple):
class Kwargs:
variable: str
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
class Slots:
my_slot: Optional[SlotInput] = None
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),
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_js_data()`](../../../reference/api#django_components.Component.get_js_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.
```python
from typing_extensions import NamedTuple, TypedDict
from typing_extensions import TypedDict
from django.template import Context
from django_components import Component, Slot, SlotInput
@ -90,15 +90,15 @@ class ButtonFooterSlotData(TypedDict):
# Define the component with the types
class Button(Component):
class Args(NamedTuple):
class Args:
name: str
class Kwargs(NamedTuple):
class Kwargs:
surname: str
age: int
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
class Slots:
# 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
@ -143,7 +143,7 @@ class Button(Component):
Args = None
Slots = None
class Kwargs(NamedTuple):
class Kwargs:
name: str
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.
```python
from typing import NamedTuple
from django_components import Component
class Button(Component):
class TemplateData(NamedTuple):
class TemplateData:
data1: str
data2: int
class JsData(NamedTuple):
class JsData:
js_data1: str
js_data2: int
class CssData(NamedTuple):
class CssData:
css_data1: str
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.
```python
from typing import NamedTuple
from django_components import Component
class Button(Component):
class TemplateData(NamedTuple):
class TemplateData:
data1: str
data2: int
class JsData(NamedTuple):
class JsData:
js_data1: str
js_data2: int
class CssData(NamedTuple):
class CssData:
css_data1: str
css_data2: int
@ -270,34 +268,63 @@ class Button(Component):
## 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),
So far, we've defined the input classes like `Kwargs` as simple classes.
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),
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
@ -390,7 +417,7 @@ As a workaround:
```py
class Table(Component):
class Args(NamedTuple):
class Args:
args: List[str]
Table.render(
@ -402,7 +429,7 @@ As a workaround:
```py
class Table(Component):
class Kwargs(NamedTuple):
class Kwargs:
variable: str
another: int
# 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:
```py
from typing import NamedTuple
from django_components import Component
class Button(Component):
class Args(NamedTuple):
class Args:
pass
class Kwargs(NamedTuple):
class Kwargs:
pass
class Slots(NamedTuple):
class Slots:
pass
```
@ -459,14 +485,14 @@ In the example below, `ButtonExtra` inherits `Kwargs` from `Button`, but overrid
from django_components import Component, Empty
class Button(Component):
class Args(NamedTuple):
class Args:
size: int
class Kwargs(NamedTuple):
class Kwargs:
color: str
class ButtonExtra(Button):
class Args(NamedTuple):
class Args:
name: str
size: int
@ -491,10 +517,10 @@ Compare the following:
```py
class Button(Component):
class Args(NamedTuple):
class Args:
size: int
class Kwargs(NamedTuple):
class Kwargs:
color: str
# Both `Args` and `Kwargs` are defined on the class
@ -624,23 +650,23 @@ The steps to migrate are:
Thus, the code above will become:
```py
from typing import NamedTuple, Optional
from typing import 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):
class Args:
size: int
text: str
class Kwargs(NamedTuple):
class Kwargs:
variable: str
another: int
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
class Slots:
# 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

View file

@ -1,6 +1,6 @@
# ruff: noqa: S311
import random
from typing import NamedTuple, Optional
from typing import Optional
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")
class OfferCardOld(Component):
class Kwargs(NamedTuple):
class Kwargs:
savings_percent: int
def get_template_data(self, args, kwargs, slots, context):
@ -45,7 +45,7 @@ class OfferCardNew(OfferCardOld):
@register("offer_card")
class OfferCard(Component):
class Kwargs(NamedTuple):
class Kwargs:
savings_percent: int
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
@ -14,7 +14,7 @@ error_rate = {
@register("api_widget")
class ApiWidget(Component):
class Kwargs(NamedTuple):
class Kwargs:
simulate_error: bool = False
def get_template_data(self, args, kwargs: Kwargs, slots, context):

View file

@ -1,6 +1,5 @@
# ruff: noqa: S311
import random
from typing import NamedTuple
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")
class WeatherWidget(Component):
class Kwargs(NamedTuple):
class Kwargs:
location: str
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
@ -9,7 +9,7 @@ DESCRIPTION = "Form that automatically arranges fields in a grid and generates l
class FormGrid(Component):
"""Form that automatically arranges fields in a grid and generates labels."""
class Kwargs(NamedTuple):
class Kwargs:
editable: bool = True
method: str = "post"
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
# We will create the label for the field automatically
label = FormGridLabel.render(
kwargs=FormGridLabel.Kwargs(field_name=field_name),
kwargs=FormGridLabel.Kwargs(field_name=field_name), # type: ignore[call-arg]
deps_strategy="ignore",
)
@ -134,7 +134,7 @@ class FormGridLabel(Component):
</label>
"""
class Kwargs(NamedTuple):
class Kwargs:
field_name: str
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")
class ThankYouMessage(Component):
class Kwargs(NamedTuple):
class Kwargs:
name: str
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
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):
"""A simple fragment with JS and CSS."""
class Kwargs(NamedTuple):
class Kwargs:
type: str
template: types.django_html = """
@ -37,7 +35,7 @@ class SimpleFragment(Component):
class AlpineFragment(Component):
"""A fragment that defines an AlpineJS component."""
class Kwargs(NamedTuple):
class Kwargs:
type: str
# 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
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")
class Recursion(Component):
class Kwargs(NamedTuple):
class Kwargs:
current_depth: int = 0
def get_template_data(self, args, kwargs: Kwargs, slots, context):

View file

@ -263,7 +263,7 @@ class Tablist(Component):
{% endprovide %}
"""
class Kwargs(NamedTuple):
class Kwargs:
id: Optional[str] = None
name: str = "Tabs"
selected_tab: Optional[str] = None
@ -341,7 +341,7 @@ class Tab(Component):
{% endprovide %}
"""
class Kwargs(NamedTuple):
class Kwargs:
header: str
disabled: bool = False
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:
```py
from typing import NamedTuple, Optional
from typing import Optional
from django.template import Context
from django_components import Component, Slot, SlotInput
class Button(Component):
class Args(NamedTuple):
class Args:
size: int
text: str
class Kwargs(NamedTuple):
class Kwargs:
variable: str
another: int
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
class Slots:
my_slot: Optional[SlotInput] = None
another_slot: SlotInput

View file

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

View file

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

View file

@ -1,6 +1,6 @@
# ruff: noqa: ARG002, N804, N805
import sys
from dataclasses import dataclass
from dataclasses import dataclass, is_dataclass
from inspect import signature
from types import MethodType
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.exception import component_error_message
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.weakref import cached_ref
@ -311,7 +318,7 @@ class ComponentVars(NamedTuple):
@register("table")
class Table(Component):
class Args(NamedTuple):
class Args:
page: int
per_page: int
@ -363,7 +370,7 @@ class ComponentVars(NamedTuple):
@register("table")
class Table(Component):
class Kwargs(NamedTuple):
class Kwargs:
page: int
per_page: int
@ -415,7 +422,7 @@ class ComponentVars(NamedTuple):
@register("table")
class Table(Component):
class Slots(NamedTuple):
class Slots:
footer: SlotInput
template = '''
@ -506,6 +513,34 @@ class ComponentMeta(ComponentMediaMeta):
attrs["template_file"] = attrs.pop("template_name")
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))
# 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:
```py
from typing import NamedTuple
from django_components import Component
class Table(Component):
class Args(NamedTuple):
class Args:
color: str
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:
- 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).
"""
@ -654,11 +693,10 @@ class Component(metaclass=ComponentMeta):
will be the instance of this class:
```py
from typing import NamedTuple
from django_components import Component
class Table(Component):
class Kwargs(NamedTuple):
class Kwargs:
color: str
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:
- 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).
"""
@ -711,11 +754,10 @@ class Component(metaclass=ComponentMeta):
will be the instance of this class:
```py
from typing import NamedTuple
from django_components import Component, Slot, SlotInput
class Table(Component):
class Slots(NamedTuple):
class Slots:
header: SlotInput
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:
- 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).
!!! info
@ -1081,18 +1128,17 @@ class Component(metaclass=ComponentMeta):
**Example:**
```py
from typing import NamedTuple
from django.template import Context
from django_components import Component, SlotInput
class MyComponent(Component):
class Args(NamedTuple):
class Args:
color: str
class Kwargs(NamedTuple):
class Kwargs:
size: int
class Slots(NamedTuple):
class Slots:
footer: SlotInput
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
@ -1123,7 +1169,7 @@ class Component(metaclass=ComponentMeta):
```py
class MyComponent(Component):
class TemplateData(NamedTuple):
class TemplateData:
color: str
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
[`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
TemplateData(**template_data)
```
- 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.
You can also return an instance of `TemplateData` directly from
[`get_template_data()`](../api#django_components.Component.get_template_data)
to get type hints:
```py
from typing import NamedTuple
from django_components import Component
class Table(Component):
class TemplateData(NamedTuple):
class TemplateData:
color: str
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
[`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 `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).
!!! info
@ -1376,13 +1421,13 @@ class Component(metaclass=ComponentMeta):
from django_components import Component, SlotInput
class MyComponent(Component):
class Args(NamedTuple):
class Args:
color: str
class Kwargs(NamedTuple):
class Kwargs:
size: int
class Slots(NamedTuple):
class Slots:
footer: SlotInput
def get_js_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
@ -1413,7 +1458,7 @@ class Component(metaclass=ComponentMeta):
```py
class MyComponent(Component):
class JsData(NamedTuple):
class JsData:
color: str
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
[`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
JsData(**js_data)
```
- 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.
You can also return an instance of `JsData` directly from
[`get_js_data()`](../api#django_components.Component.get_js_data)
to get type hints:
```py
from typing import NamedTuple
from django_components import Component
class Table(Component):
class JsData(NamedTuple):
class JsData(
color: str
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
[`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 `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).
!!! info
@ -1664,18 +1708,17 @@ class Component(metaclass=ComponentMeta):
**Example:**
```py
from typing import NamedTuple
from django.template import Context
from django_components import Component, SlotInput
class MyComponent(Component):
class Args(NamedTuple):
class Args:
color: str
class Kwargs(NamedTuple):
class Kwargs:
size: int
class Slots(NamedTuple):
class Slots:
footer: SlotInput
def get_css_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
@ -1705,7 +1748,7 @@ class Component(metaclass=ComponentMeta):
```py
class MyComponent(Component):
class CssData(NamedTuple):
class CssData:
color: str
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
[`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
CssData(**css_data)
```
- 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.
You can also return an instance of `CssData` directly from
[`get_css_data()`](../api#django_components.Component.get_css_data)
to get type hints:
```py
from typing import NamedTuple
from django_components import Component
class Table(Component):
class CssData(NamedTuple):
class CssData:
color: str
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
[`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 `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).
!!! info
@ -2567,7 +2609,7 @@ class Component(metaclass=ComponentMeta):
from django_components import Component
class Table(Component):
class Args(NamedTuple):
class Args:
page: int
per_page: int
@ -2635,7 +2677,7 @@ class Component(metaclass=ComponentMeta):
from django_components import Component
class Table(Component):
class Kwargs(NamedTuple):
class Kwargs:
page: int
per_page: int
@ -2706,7 +2748,7 @@ class Component(metaclass=ComponentMeta):
from django_components import Component, Slot, SlotInput
class Table(Component):
class Slots(NamedTuple):
class Slots:
header: SlotInput
footer: SlotInput
@ -3314,19 +3356,19 @@ class Component(metaclass=ComponentMeta):
Read more on [Typing and validation](../../concepts/fundamentals/typing_and_validation).
```python
from typing import NamedTuple, Optional
from typing import Optional
from django_components import Component, Slot, SlotInput
# Define the component with the types
class Button(Component):
class Args(NamedTuple):
class Args:
name: str
class Kwargs(NamedTuple):
class Kwargs:
surname: str
age: int
class Slots(NamedTuple):
class Slots:
my_slot: Optional[SlotInput] = None
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.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`.
"""
class Kwargs(NamedTuple):
class Kwargs:
fallback: Optional[str] = None
class Slots(NamedTuple):
class Slots:
default: Optional[SlotInput] = None
content: Optional[SlotInput] = None
fallback: Optional[SlotInput] = None

View file

@ -16,6 +16,14 @@ class DependenciesExtension(ComponentExtension):
# 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.
#
# 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:
cache_component_js(ctx.component_cls, force=True)
cache_component_css(ctx.component_cls, force=True)

View file

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

View file

@ -1,8 +1,10 @@
import re
import sys
from collections import namedtuple
from dataclasses import asdict, is_dataclass
from hashlib import md5
from importlib import import_module
from inspect import getmembers
from itertools import chain
from types import ModuleType
from typing import (
@ -267,3 +269,50 @@ def format_as_ascii_table(
def is_generator(obj: Any) -> bool:
"""Check if an object is a generator with send method."""
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) # noqa: PYI024
tuple_cls.__annotations__ = cls.__annotations__
return tuple_cls

View file

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

View file

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

View file

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

View file

@ -16,7 +16,7 @@ setup_test_config()
@djc_test
class TestComponentTyping:
def test_data_methods_input_typed(self):
def test_data_methods_input_typed_custom_classes(self):
template_called = False
js_called = False
css_called = False
@ -84,6 +84,74 @@ class TestComponentTyping:
assert js_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):
template_called = False
js_called = False
@ -132,7 +200,71 @@ class TestComponentTyping:
assert js_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
js_called = False
css_called = False
@ -489,43 +621,57 @@ class TestComponentTyping:
)
def test_custom_args_class_raises_on_invalid(self):
class Parent:
pass
class Button(Component):
template = "Hello"
class Args:
class Args(Parent):
arg1: 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")):
Button.render(
args=Button.Args(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
)
class Parent2:
def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1
self.arg2 = arg2
class Button2(Component):
template = "Hello"
class Args:
class Args(Parent2):
arg1: str
arg2: str
def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1
self.arg2 = arg2
assert not issubclass(Button2.Args, tuple)
assert issubclass(Button2.Args, Parent2)
with pytest.raises(TypeError, match=re.escape("'Args' object is not iterable")):
Button2.render(
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):
template = "Hello"
class Args:
class Args(Parent3):
arg1: str
arg2: str
def __iter__(self):
return iter([self.arg1, self.arg2])
assert not issubclass(Button3.Args, tuple)
assert issubclass(Button3.Args, Parent3)
with pytest.raises(TypeError, match=re.escape("Args() takes no arguments")):
Button3.render(
@ -533,62 +679,77 @@ class TestComponentTyping:
)
def test_custom_args_class_custom(self):
class Parent:
def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1
self.arg2 = arg2
def __iter__(self):
return iter([self.arg1, self.arg2])
class Button(Component):
template = "Hello"
class Args:
class Args(Parent):
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 Parent:
pass
class Button(Component):
template = "Hello"
class Kwargs:
class Kwargs(Parent):
arg1: 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")):
Button.render(
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"), # type: ignore[call-arg]
)
class Parent2:
def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1
self.arg2 = arg2
class Button2(Component):
template = "Hello"
class Kwargs:
class Kwargs(Parent2):
arg1: str
arg2: str
def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1
self.arg2 = arg2
assert not issubclass(Button2.Kwargs, tuple)
assert issubclass(Button2.Kwargs, Parent2)
with pytest.raises(TypeError, match=re.escape("'Kwargs' object is not iterable")):
Button2.render(
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):
template = "Hello"
class Kwargs:
class Kwargs(Parent3):
arg1: str
arg2: str
def _asdict(self):
return {"arg1": self.arg1, "arg2": self.arg2}
assert not issubclass(Button3.Kwargs, tuple)
assert issubclass(Button3.Kwargs, Parent3)
with pytest.raises(TypeError, match=re.escape("Kwargs() takes no arguments")):
Button3.render(
@ -596,19 +757,23 @@ class TestComponentTyping:
)
def test_custom_kwargs_class_custom(self):
class Parent:
def __init__(self, arg1: str, arg2: str):
self.arg1 = arg1
self.arg2 = arg2
def _asdict(self):
return {"arg1": self.arg1, "arg2": self.arg2}
class Button(Component):
template = "Hello"
class Kwargs:
class Kwargs(Parent):
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}
assert not issubclass(Button.Kwargs, tuple)
assert issubclass(Button.Kwargs, Parent)
Button.render(
kwargs=Button.Kwargs(arg1="arg1", arg2="arg2"),
@ -618,14 +783,14 @@ class TestComponentTyping:
class Button(Component):
template = "Hello"
class Args(NamedTuple):
class Args:
size: int
class Kwargs(NamedTuple):
class Kwargs:
color: str
class ButtonExtra(Button):
class Args(NamedTuple):
class Args:
name: str
size: int
@ -635,6 +800,6 @@ class TestComponentTyping:
assert ButtonExtra.Kwargs is Button.Kwargs
ButtonExtra.render(
args=ButtonExtra.Args(name="John", size=30),
kwargs=ButtonExtra.Kwargs(color="red"),
args=ButtonExtra.Args(name="John", size=30), # type: ignore[call-arg]
kwargs=ButtonExtra.Kwargs(color="red"), # type: ignore[call-arg]
)

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.
"""
from typing import NamedTuple
import pytest
from django.http import HttpRequest
from django.shortcuts import render
@ -53,7 +51,7 @@ class TestTemplatePartialsIntegration:
})()
""" # noqa: E501
class Kwargs(NamedTuple):
class Kwargs:
date: str
def get_template_data(self, args, kwargs: Kwargs, slots, context):