mirror of
https://github.com/django-components/django-components.git
synced 2025-10-17 17:27:13 +00:00
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:
parent
5e263ec143
commit
7e74831599
7 changed files with 209 additions and 747 deletions
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue