mirror of
https://github.com/django-components/django-components.git
synced 2025-08-09 00:37:59 +00:00
feat: validate component inputs if types are given (#629)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
682bfc4239
commit
4a9cf7e26d
7 changed files with 641 additions and 98 deletions
|
@ -10,6 +10,7 @@ from typing import (
|
|||
Dict,
|
||||
Generic,
|
||||
List,
|
||||
Literal,
|
||||
Mapping,
|
||||
Optional,
|
||||
Protocol,
|
||||
|
@ -60,7 +61,7 @@ from django_components.slots import (
|
|||
resolve_fill_nodes,
|
||||
resolve_slots,
|
||||
)
|
||||
from django_components.utils import gen_id
|
||||
from django_components.utils import gen_id, validate_typed_dict, validate_typed_tuple
|
||||
|
||||
# TODO_DEPRECATE_V1 - REMOVE IN V1, users should use top-level import instead
|
||||
# isort: off
|
||||
|
@ -196,6 +197,8 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
self.component_id = component_id or gen_id()
|
||||
self.registry = registry or registry_
|
||||
self._render_stack: Deque[RenderInput[ArgsType, KwargsType, SlotsType]] = deque()
|
||||
# None == uninitialized, False == No types, Tuple == types
|
||||
self._types: Optional[Union[Tuple[Any, Any, Any, Any], Literal[False]]] = None
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
cls._class_hash = hash(inspect.getfile(cls) + cls.__name__)
|
||||
|
@ -491,7 +494,10 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
)
|
||||
)
|
||||
|
||||
self._validate_inputs()
|
||||
|
||||
context_data = self.get_context_data(*args, **kwargs)
|
||||
self._validate_outputs(context_data)
|
||||
|
||||
with context.update(context_data):
|
||||
template = self.get_template(context)
|
||||
|
@ -578,7 +584,7 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
"""Fill component slots outside of template rendering."""
|
||||
slot_fills = {}
|
||||
for slot_name, content in slots_data.items():
|
||||
if isinstance(content, (str, SafeString)):
|
||||
if not callable(content):
|
||||
content_func = _nodelist_to_slot_render_func(
|
||||
NodeList([TextNode(conditional_escape(content) if escape_content else content)])
|
||||
)
|
||||
|
@ -599,6 +605,100 @@ class Component(Generic[ArgsType, KwargsType, DataType, SlotsType], metaclass=Co
|
|||
)
|
||||
return slot_fills
|
||||
|
||||
######################
|
||||
# VALIDATION
|
||||
######################
|
||||
|
||||
def _get_types(self) -> Optional[Tuple[Any, Any, Any, Any]]:
|
||||
"""
|
||||
Extract the types passed to the Component class.
|
||||
|
||||
So if a component subclasses Component class like so
|
||||
|
||||
```py
|
||||
class MyComp(Component[MyArgs, MyKwargs, Any, MySlots]):
|
||||
...
|
||||
```
|
||||
|
||||
Then we want to extract the tuple (MyArgs, MyKwargs, Any, MySlots).
|
||||
|
||||
Returns `None` if types were not provided. That is, the class was subclassed
|
||||
as:
|
||||
|
||||
```py
|
||||
class MyComp(Component):
|
||||
...
|
||||
```
|
||||
"""
|
||||
# For efficiency, the type extraction is done only once.
|
||||
# If `self._types` is `False`, that means that the types were not specified.
|
||||
# If `self._types` is `None`, then this is the first time running this method.
|
||||
# Otherwise, `self._types` should be a tuple of (Args, Kwargs, Data, Slots)
|
||||
if self._types == False: # noqa: E712
|
||||
return None
|
||||
elif self._types:
|
||||
return self._types
|
||||
|
||||
# Since a class can extend multiple classes, e.g.
|
||||
#
|
||||
# ```py
|
||||
# class MyClass(BaseOne, BaseTwo, ...):
|
||||
# ...
|
||||
# ```
|
||||
#
|
||||
# Then we need to find the base class that is our `Component` class.
|
||||
#
|
||||
# NOTE: __orig_bases__ is a tuple of _GenericAlias
|
||||
# See https://github.com/python/cpython/blob/709ef004dffe9cee2a023a3c8032d4ce80513582/Lib/typing.py#L1244
|
||||
# And https://github.com/python/cpython/issues/101688
|
||||
generics_bases: Tuple[Any, ...] = self.__orig_bases__ # type: ignore[attr-defined]
|
||||
component_generics_base = None
|
||||
for base in generics_bases:
|
||||
origin_cls = base.__origin__
|
||||
if origin_cls == Component or issubclass(origin_cls, Component):
|
||||
component_generics_base = base
|
||||
break
|
||||
|
||||
if not component_generics_base:
|
||||
# If we get here, it means that the Component class wasn't supplied any generics
|
||||
self._types = False
|
||||
return None
|
||||
|
||||
# If we got here, then we've found ourselves the typed Component class, e.g.
|
||||
#
|
||||
# `Component(Tuple[int], MyKwargs, MySlots, Any)`
|
||||
#
|
||||
# By accessing the __args__, we access individual types between the brackets, so
|
||||
#
|
||||
# (Tuple[int], MyKwargs, MySlots, Any)
|
||||
args_type, kwargs_type, data_type, slots_type = component_generics_base.__args__
|
||||
|
||||
self._types = args_type, kwargs_type, data_type, slots_type
|
||||
return self._types
|
||||
|
||||
def _validate_inputs(self) -> None:
|
||||
|
||||
maybe_inputs = self._get_types()
|
||||
if maybe_inputs is None:
|
||||
return
|
||||
args_type, kwargs_type, data_type, slots_type = maybe_inputs
|
||||
|
||||
# Validate args
|
||||
validate_typed_tuple(self.input.args, args_type, f"Component '{self.name}'", "positional argument")
|
||||
# Validate kwargs
|
||||
validate_typed_dict(self.input.kwargs, kwargs_type, f"Component '{self.name}'", "keyword argument")
|
||||
# Validate slots
|
||||
validate_typed_dict(self.input.slots, slots_type, f"Component '{self.name}'", "slot")
|
||||
|
||||
def _validate_outputs(self, data: Any) -> None:
|
||||
maybe_inputs = self._get_types()
|
||||
if maybe_inputs is None:
|
||||
return
|
||||
args_type, kwargs_type, data_type, slots_type = maybe_inputs
|
||||
|
||||
# Validate data
|
||||
validate_typed_dict(data, data_type, f"Component '{self.name}'", "data")
|
||||
|
||||
|
||||
class ComponentNode(BaseNode):
|
||||
"""Django.template.Node subclass that renders a django-components component"""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue