-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expand file tree
/
Copy pathtest_props.py
More file actions
215 lines (183 loc) ยท 6 KB
/
test_props.py
File metadata and controls
215 lines (183 loc) ยท 6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
from __future__ import annotations
import pytest
from pydantic.v1 import ValidationError
from reflex.components.props import NoExtrasAllowedProps, PropsBase
from reflex.event import (
EventChain,
EventHandler,
event,
no_args_event_spec,
passthrough_event_spec,
)
from reflex.state import State
from reflex.utils.exceptions import InvalidPropValueError
class PropA(NoExtrasAllowedProps):
"""Base prop class."""
foo: str
bar: str
class PropB(NoExtrasAllowedProps):
"""Prop class with nested props."""
foobar: str
foobaz: PropA
@pytest.mark.parametrize(
("props_class", "kwargs", "should_raise"),
[
(PropA, {"foo": "value", "bar": "another_value"}, False),
(PropA, {"fooz": "value", "bar": "another_value"}, True),
(
PropB,
{
"foobaz": {"foo": "value", "bar": "another_value"},
"foobar": "foo_bar_value",
},
False,
),
(
PropB,
{
"fooba": {"foo": "value", "bar": "another_value"},
"foobar": "foo_bar_value",
},
True,
),
(
PropB,
{
"foobaz": {"foobar": "value", "bar": "another_value"},
"foobar": "foo_bar_value",
},
True,
),
],
)
def test_no_extras_allowed_props(props_class, kwargs, should_raise):
if should_raise:
with pytest.raises((ValidationError, InvalidPropValueError)):
props_class(**kwargs)
else:
props_instance = props_class(**kwargs)
assert isinstance(props_instance, props_class)
# Test class definitions - reused across tests
class MixedCaseProps(PropsBase):
"""Test props with mixed naming conventions."""
# Single word (no case conversion needed)
name: str
# Already camelCase (should stay unchanged)
fontSize: int = 12
# snake_case (should convert to camelCase)
max_length: int = 100
is_active: bool = True
class NestedProps(PropsBase):
"""Test props for nested PropsBase testing."""
user_name: str
max_count: int = 10
class ParentProps(PropsBase):
"""Test props containing nested PropsBase objects."""
title: str
nested_config: NestedProps
is_enabled: bool = True
class OptionalFieldProps(PropsBase):
"""Test props with optional fields to test omission behavior."""
required_field: str
optional_snake_case: str | None = None
optionalCamelCase: int | None = None
@pytest.mark.parametrize(
("props_class", "props_kwargs", "expected_dict"),
[
# Test single word + snake_case conversion
(
MixedCaseProps,
{"name": "test", "max_length": 50},
{"name": "test", "fontSize": 12, "maxLength": 50, "isActive": True},
),
# Test existing camelCase stays unchanged + snake_case converts
(
MixedCaseProps,
{"name": "demo", "fontSize": 16, "is_active": False},
{"name": "demo", "fontSize": 16, "maxLength": 100, "isActive": False},
),
# Test all different case types together
(
MixedCaseProps,
{"name": "full", "fontSize": 20, "max_length": 200, "is_active": False},
{"name": "full", "fontSize": 20, "maxLength": 200, "isActive": False},
),
# Test nested PropsBase conversion
(
ParentProps,
{
"title": "parent",
"nested_config": NestedProps(user_name="nested_user", max_count=5),
},
{
"title": "parent",
"nestedConfig": {"userName": "nested_user", "maxCount": 5},
"isEnabled": True,
},
),
# Test nested with different values
(
ParentProps,
{
"title": "test",
"nested_config": NestedProps(user_name="test_user"),
"is_enabled": False,
},
{
"title": "test",
"nestedConfig": {"userName": "test_user", "maxCount": 10},
"isEnabled": False,
},
),
# Test omitted optional fields appear with None values
(
OptionalFieldProps,
{"required_field": "present"},
{
"requiredField": "present",
},
),
# Test explicit None values for optional fields
(
OptionalFieldProps,
{
"required_field": "test",
"optional_snake_case": None,
"optionalCamelCase": 42,
},
{
"requiredField": "test",
"optionalCamelCase": 42,
},
),
],
)
def test_props_base_dict_conversion(props_class, props_kwargs, expected_dict):
"""Test that dict() handles different naming conventions correctly for both simple and nested props.
Args:
props_class: The PropsBase class to test.
props_kwargs: The keyword arguments to pass to the class constructor.
expected_dict: The expected dictionary output with camelCase keys.
"""
props = props_class(**props_kwargs)
result = props.dict()
assert result == expected_dict
class EventProps(PropsBase):
"""Test props with event handler fields."""
on_click: EventHandler[no_args_event_spec]
not_start_with_on: EventHandler[passthrough_event_spec(str)]
def test_event_handler_props():
class FooState(State):
@event
def handle_click(self):
pass
@event
def handle_input(self, value: str):
pass
props = EventProps(
on_click=FooState.handle_click, # pyright: ignore[reportArgumentType]
not_start_with_on=FooState.handle_input, # pyright: ignore[reportArgumentType]
)
props_dict = props.dict()
assert isinstance(props_dict["onClick"], EventChain)
assert isinstance(props_dict["notStartWithOn"], EventChain)