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 Optional
from django.template import Context
from django_components import Component, SlotInput
class Button(Component):
class Args:
size: int
text: str
class Kwargs:
variable: str
maybe_var: Optional[int] = None # May be omitted
class Slots:
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 input classes, 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
NamedTupleor@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 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:
name: str
class Kwargs:
surname: str
age: int
maybe_var: Optional[int] = None # May be omitted
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
# 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:
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
NamedTupleor@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 django_components import Component
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):
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 django_components import Component
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):
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¤
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.
is the same as:
You can actually set these classes to anything you want - whether it's dataclasses, Pydantic models, or custom classes:
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:
NamedTuplefor theArgsclassNamedTuple, dataclasses, or Pydantic models forKwargs,Slots,TemplateData,JsData, andCssDataclasses.
However, you can use any class, as long as they meet the conditions below.
Args class¤
The Args class represents a list of positional arguments. It must meet two conditions:
-
The constructor for the
Argsclass must accept positional arguments. -
The
Argsinstance 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 django_components import Component
class Button(Component):
class Args:
pass
class Kwargs:
pass
class Slots:
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:
size: int
class Kwargs:
color: str
class ButtonExtra(Button):
class Args:
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:
size: int
class Kwargs:
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
Componentgeneric. -
If you accessed the
args,kwargs, orslotsattributes viaself.input, you will need to add the type hints yourself, becauseself.inputis no longer typed.Otherwise, you may use
Component.get_template_data()instead ofget_context_data(), asget_template_data()receivesargs,kwargs,slotsandcontextas 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 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:
size: int
text: str
class Kwargs:
variable: str
another: int
maybe_var: Optional[int] = None # May be omitted
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
# 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: ...),
),
)