mirror of
https://github.com/django-components/django-components.git
synced 2025-08-13 18:58:01 +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
|
@ -1,5 +1,6 @@
|
|||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, List, Sequence, Union
|
||||
from typing import Any, Callable, List, Mapping, Sequence, Tuple, Union, get_type_hints
|
||||
|
||||
from django.utils.autoreload import autoreload_started
|
||||
|
||||
|
@ -36,3 +37,86 @@ def watch_files_for_autoreload(watch_list: Sequence[Union[str, Path]]) -> None:
|
|||
watch(Path(file))
|
||||
|
||||
autoreload_started.connect(autoreload_hook)
|
||||
|
||||
|
||||
# NOTE: tuple_type is a _GenericAlias - See https://stackoverflow.com/questions/74412803
|
||||
def validate_typed_tuple(
|
||||
value: Tuple[Any, ...],
|
||||
tuple_type: Any,
|
||||
prefix: str,
|
||||
kind: str,
|
||||
) -> None:
|
||||
# `Any` type is the signal that we should skip validation
|
||||
if tuple_type == Any:
|
||||
return
|
||||
|
||||
# We do two kinds of validation with the given Tuple type:
|
||||
# 1. We check whether there are any extra / missing positional args
|
||||
# 2. We look at the members of the Tuple (which are types themselves),
|
||||
# and check if our concrete list / tuple has correct types under correct indices.
|
||||
expected_pos_args = len(tuple_type.__args__)
|
||||
actual_pos_args = len(value)
|
||||
if expected_pos_args > actual_pos_args:
|
||||
# Generate errors like below (listed for searchability)
|
||||
# `Component 'name' expected 3 positional arguments, got 2`
|
||||
raise TypeError(f"{prefix} expected {expected_pos_args} {kind}s, got {actual_pos_args}")
|
||||
|
||||
for index, arg_type in enumerate(tuple_type.__args__):
|
||||
arg = value[index]
|
||||
if not isinstance(arg, arg_type):
|
||||
# Generate errors like below (listed for searchability)
|
||||
# `Component 'name' expected positional argument at index 0 to be <class 'int'>, got 123.5 of type <class 'float'>` # noqa: E501
|
||||
raise TypeError(
|
||||
f"{prefix} expected {kind} at index {index} to be {arg_type}, got {arg} of type {type(arg)}"
|
||||
)
|
||||
|
||||
|
||||
# NOTE:
|
||||
# - `dict_type` can be a `TypedDict` or `Any` as the types themselves
|
||||
# - `value` is expected to be TypedDict, the base `TypedDict` type cannot be used
|
||||
# in function signature (only its subclasses can), so we specify the type as Mapping.
|
||||
# See https://stackoverflow.com/questions/74412803
|
||||
def validate_typed_dict(value: Mapping[str, Any], dict_type: Any, prefix: str, kind: str) -> None:
|
||||
# `Any` type is the signal that we should skip validation
|
||||
if dict_type == Any:
|
||||
return
|
||||
|
||||
# See https://stackoverflow.com/a/76527675
|
||||
# And https://stackoverflow.com/a/71231688
|
||||
required_kwargs = dict_type.__required_keys__
|
||||
unseen_keys = set(value.keys())
|
||||
|
||||
# For each entry in the TypedDict, we do two kinds of validation:
|
||||
# 1. We check whether there are any extra / missing keys
|
||||
# 2. We look at the values of TypedDict entries (which are types themselves),
|
||||
# and check if our concrete dict has correct types under correct keys.
|
||||
for key, kwarg_type in get_type_hints(dict_type).items():
|
||||
if key not in value:
|
||||
if key in required_kwargs:
|
||||
# Generate errors like below (listed for searchability)
|
||||
# `Component 'name' is missing a required keyword argument 'key'`
|
||||
# `Component 'name' is missing a required slot argument 'key'`
|
||||
# `Component 'name' is missing a required data argument 'key'`
|
||||
raise TypeError(f"{prefix} is missing a required {kind} '{key}'")
|
||||
else:
|
||||
unseen_keys.remove(key)
|
||||
kwarg = value[key]
|
||||
|
||||
# NOTE: `isinstance()` cannot be used with the version of TypedDict prior to 3.11.
|
||||
# So we do type validation for TypedDicts only in 3.11 and later.
|
||||
if sys.version_info >= (3, 11) and not isinstance(kwarg, kwarg_type):
|
||||
# Generate errors like below (listed for searchability)
|
||||
# `Component 'name' expected keyword argument 'key' to be <class 'int'>, got 123.4 of type <class 'float'>` # noqa: E501
|
||||
# `Component 'name' expected slot 'key' to be <class 'int'>, got 123.4 of type <class 'float'>`
|
||||
# `Component 'name' expected data 'key' to be <class 'int'>, got 123.4 of type <class 'float'>`
|
||||
raise TypeError(
|
||||
f"{prefix} expected {kind} '{key}' to be {kwarg_type}, got {kwarg} of type {type(kwarg)}"
|
||||
)
|
||||
|
||||
if unseen_keys:
|
||||
formatted_keys = ", ".join([f"'{key}'" for key in unseen_keys])
|
||||
# Generate errors like below (listed for searchability)
|
||||
# `Component 'name' got unexpected keyword argument keys 'invalid_key'`
|
||||
# `Component 'name' got unexpected slot keys 'invalid_key'`
|
||||
# `Component 'name' got unexpected data keys 'invalid_key'`
|
||||
raise TypeError(f"{prefix} got unexpected {kind} keys {formatted_keys}")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue