Typing and validation
Typing overview¤
Warning
In versions 0.92 to 0.139 (inclusive), the component typing was specified through generics.
Since v0.140, the types must be specified as class attributes of the Component class - Args
, Kwargs
, Slots
, TemplateData
, JsData
, and CssData
.
See Migrating from generics to class attributes for more info.
Warning
Input validation was NOT part of Django Components between versions 0.136 and 0.139 (inclusive).
The Component
class optionally accepts class attributes that allow you to define the types of args, kwargs, slots, as well as the data returned from the data methods.
Use this to add type hints to your components, to validate the inputs at runtime, and to document them.
from typing import NamedTuple, Optional
from django.template import Context
from django_components import Component, SlotInput
class Button(Component):
class Args(NamedTuple):
size: int
text: str
class Kwargs(NamedTuple):
variable: str
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
my_slot: Optional[SlotInput] = None
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
...
template_file = "button.html"
The class attributes are:
Args
- Type for positional arguments.Kwargs
- Type for keyword arguments.Slots
- Type for slots.TemplateData
- Type for data returned fromget_template_data()
.JsData
- Type for data returned fromget_js_data()
.CssData
- Type for data returned fromget_css_data()
.
You can specify as many or as few of these as you want, the rest will default to None
.
Typing inputs¤
You can use Component.Args
, Component.Kwargs
, and Component.Slots
to type the component inputs.
When you set these classes, at render time the args
, kwargs
, and slots
parameters of the data methods (get_template_data()
, get_js_data()
, get_css_data()
) will be instances of these classes.
This way, each component can have runtime validation of the inputs:
- When you use
NamedTuple
or@dataclass
, instantiating these classes will check ONLY for the presence of the attributes. - When you use Pydantic models, instantiating these classes will check for the presence AND type of the attributes.
If you omit the Args
, Kwargs
, or Slots
classes, or set them to None
, the inputs will be passed as plain lists or dictionaries, and will not be validated.
from typing_extensions import NamedTuple, TypedDict
from django.template import Context
from django_components import Component, Slot, SlotInput
# The data available to the `footer` scoped slot
class ButtonFooterSlotData(TypedDict):
value: int
# Define the component with the types
class Button(Component):
class Args(NamedTuple):
name: str
class Kwargs(NamedTuple):
surname: str
age: int
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
# Use `SlotInput` to allow slots to be given as `Slot` instance,
# plain string, or a function that returns a string.
my_slot: Optional[SlotInput] = None
# Use `Slot` to allow ONLY `Slot` instances.
# The generic is optional, and it specifies the data available
# to the slot function.
footer: Slot[ButtonFooterSlotData]
# Add type hints to the data method
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
# The parameters are instances of the classes we defined
assert isinstance(args, Button.Args)
assert isinstance(kwargs, Button.Kwargs)
assert isinstance(slots, Button.Slots)
args.name # str
kwargs.age # int
slots.footer # Slot[ButtonFooterSlotData]
# Add type hints to the render call
Button.render(
args=Button.Args(
name="John",
),
kwargs=Button.Kwargs(
surname="Doe",
age=30,
),
slots=Button.Slots(
footer=Slot(lambda ctx: "Click me!"),
),
)
If you don't want to validate some parts, set them to None
or omit them.
The following will validate only the keyword inputs:
class Button(Component):
# We could also omit these
Args = None
Slots = None
class Kwargs(NamedTuple):
name: str
age: int
# Only `kwargs` is instantiated. `args` and `slots` are not.
def get_template_data(self, args, kwargs: Kwargs, slots, context: Context):
assert isinstance(args, list)
assert isinstance(slots, dict)
assert isinstance(kwargs, Button.Kwargs)
args[0] # str
slots["footer"] # Slot[ButtonFooterSlotData]
kwargs.age # int
Info
Components can receive slots as strings, functions, or instances of Slot
.
Internally these are all normalized to instances of Slot
.
Therefore, the slots
dictionary available in data methods (like get_template_data()
) will always be a dictionary of Slot
instances.
To correctly type this dictionary, you should set the fields of Slots
to Slot
or SlotInput
:
SlotInput
is a union of Slot
, string, and function types.
Typing data¤
You can use Component.TemplateData
, Component.JsData
, and Component.CssData
to type the data returned from get_template_data()
, get_js_data()
, and get_css_data()
.
When you set these classes, at render time they will be instantiated with the data returned from these methods.
This way, each component can have runtime validation of the returned data:
- When you use
NamedTuple
or@dataclass
, instantiating these classes will check ONLY for the presence of the attributes. - When you use Pydantic models, instantiating these classes will check for the presence AND type of the attributes.
If you omit the TemplateData
, JsData
, or CssData
classes, or set them to None
, the validation and instantiation will be skipped.
from typing import NamedTuple
from django_components import Component
class Button(Component):
class TemplateData(NamedTuple):
data1: str
data2: int
class JsData(NamedTuple):
js_data1: str
js_data2: int
class CssData(NamedTuple):
css_data1: str
css_data2: int
def get_template_data(self, args, kwargs, slots, context):
return {
"data1": "...",
"data2": 123,
}
def get_js_data(self, args, kwargs, slots, context):
return {
"js_data1": "...",
"js_data2": 123,
}
def get_css_data(self, args, kwargs, slots, context):
return {
"css_data1": "...",
"css_data2": 123,
}
For each data method, you can either return a plain dictionary with the data, or an instance of the respective data class directly.
from typing import NamedTuple
from django_components import Component
class Button(Component):
class TemplateData(NamedTuple):
data1: str
data2: int
class JsData(NamedTuple):
js_data1: str
js_data2: int
class CssData(NamedTuple):
css_data1: str
css_data2: int
def get_template_data(self, args, kwargs, slots, context):
return Button.TemplateData(
data1="...",
data2=123,
)
def get_js_data(self, args, kwargs, slots, context):
return Button.JsData(
js_data1="...",
js_data2=123,
)
def get_css_data(self, args, kwargs, slots, context):
return Button.CssData(
css_data1="...",
css_data2=123,
)
Custom types¤
We recommend to use NamedTuple
for the Args
class, and NamedTuple
, dataclasses, or Pydantic 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 to validate the inputs at runtime.
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
class represents a list of positional arguments. It must meet two conditions:
-
The constructor for the
Args
class must accept positional arguments. -
The
Args
instance must be convertable to a list.
To implement the conversion to a list, you can implement the __iter__()
method:
class MyClass:
def __init__(self):
self.x = 1
self.y = 2
def __iter__(self):
return iter([('x', self.x), ('y', self.y)])
Dictionary classes¤
On the other hand, other types (Kwargs
, Slots
, TemplateData
, JsData
, and CssData
) represent dictionaries. They must meet these two conditions:
-
The constructor must accept keyword arguments.
-
The instance must be convertable to a dictionary.
To implement the conversion to a dictionary, you can implement either:
-
_asdict()
method -
Or make the class dict-like with
__iter__()
and__getitem__()
Passing variadic args and kwargs¤
You may have a component that accepts any number of args or kwargs.
However, this cannot be described with the current Python's typing system (as of v0.140).
As a workaround:
-
For a variable number of positional arguments (
*args
), set a positional argument that accepts a list of values: -
For a variable number of keyword arguments (
**kwargs
), set a keyword argument that accepts a dictionary of values:
Handling no args or no kwargs¤
To declare that a component accepts no args, kwargs, etc, define the types with no attributes using the pass
keyword:
from typing import NamedTuple
from django_components import Component
class Button(Component):
class Args(NamedTuple):
pass
class Kwargs(NamedTuple):
pass
class Slots(NamedTuple):
pass
This can get repetitive, so we added a Empty
type to make it easier:
from django_components import Component, Empty
class Button(Component):
Args = Empty
Kwargs = Empty
Slots = Empty
Subclassing¤
Subclassing components with types is simple.
Since each type class is a separate class attribute, you can just override them in the Component subclass.
In the example below, ButtonExtra
inherits Kwargs
from Button
, but overrides the Args
class.
from django_components import Component, Empty
class Button(Component):
class Args(NamedTuple):
size: int
class Kwargs(NamedTuple):
color: str
class ButtonExtra(Button):
class Args(NamedTuple):
name: str
size: int
# Stil works the same way!
ButtonExtra.render(
args=ButtonExtra.Args(name="John", size=30),
kwargs=ButtonExtra.Kwargs(color="red"),
)
The only difference is when it comes to type hints to the data methods like get_template_data()
.
When you define the nested classes like Args
and Kwargs
directly on the class, you can reference them just by their class name (Args
and Kwargs
).
But when you have a Component subclass, and it uses Args
or Kwargs
from the parent, you will have to reference the type as a forward reference, including the full name of the component (Button.Args
and Button.Kwargs
).
Compare the following:
class Button(Component):
class Args(NamedTuple):
size: int
class Kwargs(NamedTuple):
color: str
# Both `Args` and `Kwargs` are defined on the class
def get_template_data(self, args: Args, kwargs: Kwargs, slots, context):
pass
class ButtonExtra(Button):
class Args(NamedTuple):
name: str
size: int
# `Args` is defined on the subclass, `Kwargs` is defined on the parent
def get_template_data(self, args: Args, kwargs: "ButtonExtra.Kwargs", slots, context):
pass
class ButtonSame(Button):
# Both `Args` and `Kwargs` are defined on the parent
def get_template_data(self, args: "ButtonSame.Args", kwargs: "ButtonSame.Kwargs", slots, context):
pass
Runtime type validation¤
When you add types to your component, and implement them as NamedTuple
or dataclass
, the validation will check only for the presence of the attributes.
So this will not catch if you pass a string to an int
attribute.
To enable runtime type validation, you need to use Pydantic models, and install the djc-ext-pydantic
extension.
The djc-ext-pydantic
extension ensures compatibility between django-components' classes such as Component
, or Slot
and Pydantic models.
First install the extension:
And then add the extension to your project:
Migrating from generics to class attributes¤
In versions 0.92 to 0.139 (inclusive), the component typing was specified through generics.
Since v0.140, the types must be specified as class attributes of the Component class - Args
, Kwargs
, Slots
, TemplateData
, JsData
, and CssData
.
This change was necessary to make it possible to subclass components. Subclassing with generics was otherwise too complicated. Read the discussion here.
Because of this change, the Component.render()
method is no longer typed. To type-check the inputs, you should wrap the inputs in Component.Args
, Component.Kwargs
, Component.Slots
, etc.
For example, if you had a component like this:
from typing import NotRequired, Tuple, TypedDict
from django_components import Component, Slot, SlotInput
ButtonArgs = Tuple[int, str]
class ButtonKwargs(TypedDict):
variable: str
another: int
maybe_var: NotRequired[int] # May be omitted
class ButtonSlots(TypedDict):
# Use `SlotInput` to allow slots to be given as `Slot` instance,
# plain string, or a function that returns a string.
my_slot: NotRequired[SlotInput]
# Use `Slot` to allow ONLY `Slot` instances.
another_slot: Slot
ButtonType = Component[ButtonArgs, ButtonKwargs, ButtonSlots]
class Button(ButtonType):
def get_context_data(self, *args, **kwargs):
self.input.args[0] # int
self.input.kwargs["variable"] # str
self.input.slots["my_slot"] # Slot[MySlotData]
Button.render(
args=(1, "hello"),
kwargs={
"variable": "world",
"another": 123,
},
slots={
"my_slot": "...",
"another_slot": Slot(lambda ctx: ...),
},
)
The steps to migrate are:
- Convert all the types (
ButtonArgs
,ButtonKwargs
,ButtonSlots
) to subclasses ofNamedTuple
. - Move these types inside the Component class (
Button
), and rename them toArgs
,Kwargs
, andSlots
. - If you defined typing for the data methods (like
get_context_data()
), move them inside the Component class, and rename them toTemplateData
,JsData
, andCssData
. - Remove the
Component
generic. -
If you accessed the
args
,kwargs
, orslots
attributes viaself.input
, you will need to add the type hints yourself, becauseself.input
is no longer typed.Otherwise, you may use
Component.get_template_data()
instead ofget_context_data()
, asget_template_data()
receivesargs
,kwargs
,slots
andcontext
as arguments. You will still need to add the type hints yourself. -
Lastly, you will need to update the
Component.render()
calls to wrap the inputs inComponent.Args
,Component.Kwargs
, andComponent.Slots
, to manually add type hints.
Thus, the code above will become:
from typing import NamedTuple, Optional
from django.template import Context
from django_components import Component, Slot, SlotInput
# The Component class does not take any generics
class Button(Component):
# Types are now defined inside the component class
class Args(NamedTuple):
size: int
text: str
class Kwargs(NamedTuple):
variable: str
another: int
maybe_var: Optional[int] = None # May be omitted
class Slots(NamedTuple):
# Use `SlotInput` to allow slots to be given as `Slot` instance,
# plain string, or a function that returns a string.
my_slot: Optional[SlotInput] = None
# Use `Slot` to allow ONLY `Slot` instances.
another_slot: Slot
# The args, kwargs, slots are instances of the component's Args, Kwargs, and Slots classes
def get_template_data(self, args: Args, kwargs: Kwargs, slots: Slots, context: Context):
args.size # int
kwargs.variable # str
slots.my_slot # Slot[MySlotData]
Button.render(
# Wrap the inputs in the component's Args, Kwargs, and Slots classes
args=Button.Args(1, "hello"),
kwargs=Button.Kwargs(
variable="world",
another=123,
),
slots=Button.Slots(
my_slot="...",
another_slot=Slot(lambda ctx: ...),
),
)