refactor: remove input validation and link to it (#1082)

* feat: allow to set defaults

* refactor: remove input validation and link to it

* docs: update changelog

* Update typing_and_validation.md

* Update typing_and_validation.md
This commit is contained in:
Juro Oravec 2025-04-05 08:19:19 +02:00 committed by GitHub
parent 5e263ec143
commit 7e74831599
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 209 additions and 747 deletions

View file

@ -4,16 +4,7 @@ For tests focusing on the `component` tag, see `test_templatetags_component.py`
"""
import re
import sys
from typing import Any, Dict, List, Tuple, Union, no_type_check
# See https://peps.python.org/pep-0655/#usage-in-python-3-11
if sys.version_info >= (3, 11):
from typing import NotRequired, TypedDict
else:
from typing_extensions import NotRequired, TypedDict # for Python <3.11 with (Not)Required
from unittest import skipIf
from typing import Any, Dict, no_type_check
import pytest
from django.conf import settings
@ -23,10 +14,9 @@ from django.template import Context, RequestContext, Template, TemplateSyntaxErr
from django.template.base import TextNode
from django.test import Client
from django.urls import path
from django.utils.safestring import SafeString
from pytest_django.asserts import assertHTMLEqual, assertInHTML
from django_components import Component, ComponentView, Slot, SlotFunc, all_components, register, types
from django_components import Component, ComponentView, all_components, register, types
from django_components.slots import SlotRef
from django_components.urls import urlpatterns as dc_urlpatterns
@ -51,34 +41,6 @@ class CustomClient(Client):
super().__init__(*args, **kwargs)
# Component typings
CompArgs = Tuple[int, str]
class CompData(TypedDict):
variable: str
class CompSlots(TypedDict):
my_slot: Union[str, int, Slot]
my_slot2: SlotFunc
if sys.version_info >= (3, 11):
class CompKwargs(TypedDict):
variable: str
another: int
optional: NotRequired[int]
else:
class CompKwargs(TypedDict, total=False):
variable: str
another: int
optional: NotRequired[int]
# TODO_REMOVE_IN_V1 - Superseded by `self.get_template` in v1
@djc_test
class TestComponentOldTemplateApi:
@ -395,354 +357,6 @@ class TestComponent:
Root.render()
@djc_test
class TestComponentValidation:
def test_validate_input_passes(self):
class TestComponent(Component[CompArgs, CompKwargs, CompSlots, CompData, Any, Any]):
def get_context_data(self, var1, var2, variable, another, **attrs):
return {
"variable": variable,
}
template: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
Slot 1: {% slot "my_slot" / %}
Slot 2: {% slot "my_slot2" / %}
"""
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={
"my_slot": SafeString("MY_SLOT"),
"my_slot2": lambda ctx, data, ref: "abc",
},
)
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3e>test</strong>
Slot 1: MY_SLOT
Slot 2: abc
""",
)
@skipIf(sys.version_info < (3, 11), "Requires >= 3.11")
def test_validate_input_fails(self):
class TestComponent(Component[CompArgs, CompKwargs, CompSlots, CompData, Any, Any]):
def get_context_data(self, var1, var2, variable, another, **attrs):
return {
"variable": variable,
}
template: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
Slot 1: {% slot "my_slot" / %}
Slot 2: {% slot "my_slot2" / %}
"""
with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' expected 2 positional arguments, got 1"),
):
TestComponent.render(
kwargs={"variable": 1, "another": "test"}, # type: ignore
args=(123,), # type: ignore
slots={
"my_slot": "MY_SLOT",
"my_slot2": lambda ctx, data, ref: "abc",
},
)
with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' expected 2 positional arguments, got 0"),
):
TestComponent.render(
kwargs={"variable": 1, "another": "test"}, # type: ignore
slots={
"my_slot": "MY_SLOT",
"my_slot2": lambda ctx, data, ref: "abc",
},
)
with pytest.raises(
TypeError,
match=re.escape(
"Component 'TestComponent' expected keyword argument 'variable' to be <class 'str'>, got 1 of type <class 'int'>" # noqa: E501
),
):
TestComponent.render(
kwargs={"variable": 1, "another": "test"}, # type: ignore
args=(123, "abc", 456), # type: ignore
slots={
"my_slot": "MY_SLOT",
"my_slot2": lambda ctx, data, ref: "abc",
},
)
with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' expected 2 positional arguments, got 0"),
):
TestComponent.render()
with pytest.raises(
TypeError,
match=re.escape(
"Component 'TestComponent' expected keyword argument 'variable' to be <class 'str'>, got 1 of type <class 'int'>" # noqa: E501
),
):
TestComponent.render(
kwargs={"variable": 1, "another": "test"}, # type: ignore
args=(123, "str"),
slots={
"my_slot": "MY_SLOT",
"my_slot2": lambda ctx, data, ref: "abc",
},
)
with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' is missing a required keyword argument 'another'"),
):
TestComponent.render(
kwargs={"variable": "abc"}, # type: ignore
args=(123, "str"),
slots={
"my_slot": "MY_SLOT",
"my_slot2": lambda ctx, data, ref: "abc",
},
)
with pytest.raises(
TypeError,
match=re.escape(
"Component 'TestComponent' expected slot 'my_slot' to be typing.Union[str, int, django_components.slots.Slot], got 123.5 of type <class 'float'>" # noqa: E501
),
):
TestComponent.render(
kwargs={"variable": "abc", "another": 1},
args=(123, "str"),
slots={
"my_slot": 123.5, # type: ignore
"my_slot2": lambda ctx, data, ref: "abc",
},
)
with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' is missing a required slot 'my_slot2'"),
):
TestComponent.render(
kwargs={"variable": "abc", "another": 1},
args=(123, "str"),
slots={
"my_slot": "MY_SLOT",
}, # type: ignore
)
def test_validate_input_skipped(self):
class TestComponent(Component[Any, CompKwargs, Any, CompData, Any, Any]):
def get_context_data(self, var1, var2, variable, another, **attrs):
return {
"variable": variable,
}
template: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
Slot 1: {% slot "my_slot" / %}
Slot 2: {% slot "my_slot2" / %}
"""
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=("123", "str"), # NOTE: Normally should raise
slots={
"my_slot": 123.5, # NOTE: Normally should raise
"my_slot2": lambda ctx, data, ref: "abc",
},
)
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3e>test</strong>
Slot 1: 123.5
Slot 2: abc
""",
)
def test_validate_output_passes(self):
class TestComponent(Component[CompArgs, CompKwargs, CompSlots, CompData, Any, Any]):
def get_context_data(self, var1, var2, variable, another, **attrs):
return {
"variable": variable,
}
template: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
Slot 1: {% slot "my_slot" / %}
Slot 2: {% slot "my_slot2" / %}
"""
rendered = TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={
"my_slot": SafeString("MY_SLOT"),
"my_slot2": lambda ctx, data, ref: "abc",
},
)
assertHTMLEqual(
rendered,
"""
Variable: <strong data-djc-id-a1bc3e>test</strong>
Slot 1: MY_SLOT
Slot 2: abc
""",
)
def test_validate_output_fails(self):
class TestComponent(Component[CompArgs, CompKwargs, CompSlots, CompData, Any, Any]):
def get_context_data(self, var1, var2, variable, another, **attrs):
return {
"variable": variable,
"invalid_key": var1,
}
template: types.django_html = """
{% load component_tags %}
Variable: <strong>{{ variable }}</strong>
Slot 1: {% slot "my_slot" / %}
Slot 2: {% slot "my_slot2" / %}
"""
with pytest.raises(
TypeError,
match=re.escape("Component 'TestComponent' got unexpected data keys 'invalid_key'"),
):
TestComponent.render(
kwargs={"variable": "test", "another": 1},
args=(123, "str"),
slots={
"my_slot": SafeString("MY_SLOT"),
"my_slot2": lambda ctx, data, ref: "abc",
},
)
def test_handles_components_in_typing(self):
class InnerKwargs(TypedDict):
one: str
class InnerData(TypedDict):
one: Union[str, int]
self: "InnerComp" # type: ignore[misc]
InnerComp = Component[Any, InnerKwargs, Any, InnerData, Any, Any] # type: ignore[misc]
class Inner(InnerComp):
def get_context_data(self, one):
return {
"self": self,
"one": one,
}
template = ""
TodoArgs = Tuple[Inner] # type: ignore[misc]
class TodoKwargs(TypedDict):
inner: Inner
class TodoData(TypedDict):
one: Union[str, int]
self: "TodoComp" # type: ignore[misc]
inner: str
TodoComp = Component[TodoArgs, TodoKwargs, Any, TodoData, Any, Any] # type: ignore[misc]
# NOTE: Since we're using ForwardRef for "TodoComp" and "InnerComp", we need
# to ensure that the actual types are set as globals, so the ForwardRef class
# can resolve them.
globals()["TodoComp"] = TodoComp
globals()["InnerComp"] = InnerComp
class TestComponent(TodoComp):
def get_context_data(self, var1, inner):
return {
"self": self,
"one": "2123",
# NOTE: All of this is typed
"inner": self.input.kwargs["inner"].render(kwargs={"one": "abc"}),
}
template: types.django_html = """
{% load component_tags %}
Name: <strong>{{ self.name }}</strong>
"""
rendered = TestComponent.render(args=(Inner(),), kwargs={"inner": Inner()})
assertHTMLEqual(
rendered,
"""
Name: <strong data-djc-id-a1bc3e>TestComponent</strong>
""",
)
def test_handles_typing_module(self):
TodoArgs = Tuple[
Union[str, int],
Dict[str, int],
List[str],
Tuple[int, Union[str, int]],
]
class TodoKwargs(TypedDict):
one: Union[str, int]
two: Dict[str, int]
three: List[str]
four: Tuple[int, Union[str, int]]
class TodoData(TypedDict):
one: Union[str, int]
two: Dict[str, int]
three: List[str]
four: Tuple[int, Union[str, int]]
TodoComp = Component[TodoArgs, TodoKwargs, Any, TodoData, Any, Any]
# NOTE: Since we're using ForwardRef for "TodoComp", we need
# to ensure that the actual types are set as globals, so the ForwardRef class
# can resolve them.
globals()["TodoComp"] = TodoComp
class TestComponent(TodoComp):
def get_context_data(self, *args, **kwargs):
return {
**kwargs,
}
template = ""
TestComponent.render(
args=("str", {"str": 123}, ["a", "b", "c"], (123, "123")),
kwargs={
"one": "str",
"two": {"str": 123},
"three": ["a", "b", "c"],
"four": (123, "123"),
},
)
@djc_test
class TestComponentRender:
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)