Skip to content

Commit fbcd47b

Browse files
authored
some prepwork to anticipate for removing pydantic from propsbase (#5370)
1 parent cfe0aa2 commit fbcd47b

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed

reflex/components/props.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def dict(self, *args, **kwargs):
3131
"""Convert the object to a dictionary.
3232
3333
Keys will be converted to camelCase.
34+
By default, None values are excluded (exclude_none=True).
3435
3536
Args:
3637
*args: Arguments to pass to the parent class.
@@ -39,6 +40,7 @@ def dict(self, *args, **kwargs):
3940
Returns:
4041
The object as a dictionary.
4142
"""
43+
kwargs.setdefault("exclude_none", True)
4244
return {
4345
format.to_camel_case(key): value
4446
for key, value in super().dict(*args, **kwargs).items()

reflex/components/sonner/toast.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,6 @@ def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
147147
Returns:
148148
The object as a dictionary with ToastAction fields intact.
149149
"""
150-
kwargs.setdefault("exclude_none", True)
151150
d = super().dict(*args, **kwargs)
152151
# Keep these fields as ToastAction so they can be serialized specially
153152
if "action" in d:

tests/units/components/test_props.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22
from pydantic.v1 import ValidationError
33

4-
from reflex.components.props import NoExtrasAllowedProps
4+
from reflex.components.props import NoExtrasAllowedProps, PropsBase
55
from reflex.utils.exceptions import InvalidPropValueError
66

77

@@ -57,3 +57,123 @@ def test_no_extras_allowed_props(props_class, kwargs, should_raise):
5757
else:
5858
props_instance = props_class(**kwargs)
5959
assert isinstance(props_instance, props_class)
60+
61+
62+
# Test class definitions - reused across tests
63+
class MixedCaseProps(PropsBase):
64+
"""Test props with mixed naming conventions."""
65+
66+
# Single word (no case conversion needed)
67+
name: str
68+
# Already camelCase (should stay unchanged)
69+
fontSize: int = 12
70+
# snake_case (should convert to camelCase)
71+
max_length: int = 100
72+
is_active: bool = True
73+
74+
75+
class NestedProps(PropsBase):
76+
"""Test props for nested PropsBase testing."""
77+
78+
user_name: str
79+
max_count: int = 10
80+
81+
82+
class ParentProps(PropsBase):
83+
"""Test props containing nested PropsBase objects."""
84+
85+
title: str
86+
nested_config: NestedProps
87+
is_enabled: bool = True
88+
89+
90+
class OptionalFieldProps(PropsBase):
91+
"""Test props with optional fields to test omission behavior."""
92+
93+
required_field: str
94+
optional_snake_case: str | None = None
95+
optionalCamelCase: int | None = None
96+
97+
98+
@pytest.mark.parametrize(
99+
"props_class, props_kwargs, expected_dict",
100+
[
101+
# Test single word + snake_case conversion
102+
(
103+
MixedCaseProps,
104+
{"name": "test", "max_length": 50},
105+
{"name": "test", "fontSize": 12, "maxLength": 50, "isActive": True},
106+
),
107+
# Test existing camelCase stays unchanged + snake_case converts
108+
(
109+
MixedCaseProps,
110+
{"name": "demo", "fontSize": 16, "is_active": False},
111+
{"name": "demo", "fontSize": 16, "maxLength": 100, "isActive": False},
112+
),
113+
# Test all different case types together
114+
(
115+
MixedCaseProps,
116+
{"name": "full", "fontSize": 20, "max_length": 200, "is_active": False},
117+
{"name": "full", "fontSize": 20, "maxLength": 200, "isActive": False},
118+
),
119+
# Test nested PropsBase conversion
120+
(
121+
ParentProps,
122+
{
123+
"title": "parent",
124+
"nested_config": NestedProps(user_name="nested_user", max_count=5),
125+
},
126+
{
127+
"title": "parent",
128+
"nestedConfig": {"userName": "nested_user", "maxCount": 5},
129+
"isEnabled": True,
130+
},
131+
),
132+
# Test nested with different values
133+
(
134+
ParentProps,
135+
{
136+
"title": "test",
137+
"nested_config": NestedProps(user_name="test_user"),
138+
"is_enabled": False,
139+
},
140+
{
141+
"title": "test",
142+
"nestedConfig": {"userName": "test_user", "maxCount": 10},
143+
"isEnabled": False,
144+
},
145+
),
146+
# Test omitted optional fields appear with None values
147+
(
148+
OptionalFieldProps,
149+
{"required_field": "present"},
150+
{
151+
"requiredField": "present",
152+
},
153+
),
154+
# Test explicit None values for optional fields
155+
(
156+
OptionalFieldProps,
157+
{
158+
"required_field": "test",
159+
"optional_snake_case": None,
160+
"optionalCamelCase": 42,
161+
},
162+
{
163+
"requiredField": "test",
164+
"optionalCamelCase": 42,
165+
},
166+
),
167+
],
168+
)
169+
def test_props_base_dict_conversion(props_class, props_kwargs, expected_dict):
170+
"""Test that dict() handles different naming conventions correctly for both simple and nested props.
171+
172+
Args:
173+
props_class: The PropsBase class to test.
174+
props_kwargs: The keyword arguments to pass to the class constructor.
175+
expected_dict: The expected dictionary output with camelCase keys.
176+
"""
177+
props = props_class(**props_kwargs)
178+
result = props.dict()
179+
assert result == expected_dict

0 commit comments

Comments
 (0)