Skip to content

Commit e9cd7e0

Browse files
Lendemoradhami3310
andauthored
remove pydantic dependency for PropsBase (#5384)
* wip propsbase no pydantic * working version before refactoring * another step * compacting stuff... a bit * clean doc * clean up the PR * fix pre-commit pyi * remove comment * run pyi hashes --------- Co-authored-by: Khaleel Al-Adhami <khaleel.aladhami@gmail.com>
1 parent c23ffce commit e9cd7e0

File tree

5 files changed

+625
-107
lines changed

5 files changed

+625
-107
lines changed

pyi_hashes.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,6 @@
120120
"reflex/components/recharts/general.pyi": "e992c57e3df1dc47b6a544c535d87319",
121121
"reflex/components/recharts/polar.pyi": "cb74280e343562f52f087973eff27389",
122122
"reflex/components/recharts/recharts.pyi": "2ab851e1b2cb63ae690e89144319a02c",
123-
"reflex/components/sonner/toast.pyi": "a9f5529905f171fddd93e78b6e7ad53e",
123+
"reflex/components/sonner/toast.pyi": "1681a04ee927febbe7b015dc224ba00b",
124124
"reflex/components/suneditor/editor.pyi": "447a2c210d7218816236b7e31e6c135d"
125125
}

reflex/components/component.py

Lines changed: 71 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,7 @@
1414
from functools import wraps
1515
from hashlib import md5
1616
from types import SimpleNamespace
17-
from typing import (
18-
TYPE_CHECKING,
19-
Annotated,
20-
Any,
21-
ClassVar,
22-
Generic,
23-
TypeVar,
24-
cast,
25-
get_args,
26-
get_origin,
27-
)
17+
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, cast, get_args, get_origin
2818

2919
from rich.markup import escape
3020
from typing_extensions import dataclass_transform
@@ -33,6 +23,7 @@
3323
from reflex.compiler.templates import STATEFUL_COMPONENT
3424
from reflex.components.core.breakpoints import Breakpoints
3525
from reflex.components.dynamic import load_dynamic_serializer
26+
from reflex.components.field import BaseField, FieldBasedMeta
3627
from reflex.components.tags import Tag
3728
from reflex.constants import (
3829
Dirs,
@@ -75,7 +66,7 @@
7566
FIELD_TYPE = TypeVar("FIELD_TYPE")
7667

7768

78-
class ComponentField(Generic[FIELD_TYPE]):
69+
class ComponentField(BaseField[FIELD_TYPE]):
7970
"""A field for a component."""
8071

8172
def __init__(
@@ -93,30 +84,8 @@ def __init__(
9384
is_javascript: Whether the field is a javascript property.
9485
annotated_type: The annotated type for the field.
9586
"""
96-
self.default = default
97-
self.default_factory = default_factory
87+
super().__init__(default, default_factory, annotated_type)
9888
self.is_javascript = is_javascript
99-
self.outer_type_ = self.annotated_type = annotated_type
100-
type_origin = get_origin(annotated_type) or annotated_type
101-
if type_origin is Annotated:
102-
type_origin = annotated_type.__origin__ # pyright: ignore [reportAttributeAccessIssue]
103-
self.type_ = self.type_origin = type_origin
104-
105-
def default_value(self) -> FIELD_TYPE:
106-
"""Get the default value for the field.
107-
108-
Returns:
109-
The default value for the field.
110-
111-
Raises:
112-
ValueError: If no default value or factory is provided.
113-
"""
114-
if self.default is not MISSING:
115-
return self.default
116-
if self.default_factory is not None:
117-
return self.default_factory()
118-
msg = "No default value or factory provided."
119-
raise ValueError(msg)
12089

12190
def __repr__(self) -> str:
12291
"""Represent the field in a readable format.
@@ -163,7 +132,7 @@ def field(
163132

164133

165134
@dataclass_transform(kw_only_default=True, field_specifiers=(field,))
166-
class BaseComponentMeta(ABCMeta):
135+
class BaseComponentMeta(FieldBasedMeta, ABCMeta):
167136
"""Meta class for BaseComponent."""
168137

169138
if TYPE_CHECKING:
@@ -172,46 +141,24 @@ class BaseComponentMeta(ABCMeta):
172141
_fields: Mapping[str, ComponentField]
173142
_js_fields: Mapping[str, ComponentField]
174143

175-
def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> type:
176-
"""Create a new class.
177-
178-
Args:
179-
name: The name of the class.
180-
bases: The bases of the class.
181-
namespace: The namespace of the class.
182-
183-
Returns:
184-
The new class.
185-
"""
186-
# Add the field to the class
187-
inherited_fields: dict[str, ComponentField] = {}
188-
own_fields: dict[str, ComponentField] = {}
189-
resolved_annotations = types.resolve_annotations(
144+
@classmethod
145+
def _resolve_annotations(
146+
cls, namespace: dict[str, Any], name: str
147+
) -> dict[str, Any]:
148+
return types.resolve_annotations(
190149
namespace.get("__annotations__", {}), namespace["__module__"]
191150
)
192151

193-
for base in bases[::-1]:
194-
if hasattr(base, "_inherited_fields"):
195-
inherited_fields.update(base._inherited_fields)
196-
for base in bases[::-1]:
197-
if hasattr(base, "_own_fields"):
198-
inherited_fields.update(base._own_fields)
199-
200-
for key, value, inherited_field in [
201-
(key, value, inherited_field)
202-
for key, value in namespace.items()
203-
if key not in resolved_annotations
204-
and ((inherited_field := inherited_fields.get(key)) is not None)
205-
]:
206-
new_value = ComponentField(
207-
default=value,
208-
is_javascript=inherited_field.is_javascript,
209-
annotated_type=inherited_field.annotated_type,
210-
)
211-
212-
own_fields[key] = new_value
152+
@classmethod
153+
def _process_annotated_fields(
154+
cls,
155+
namespace: dict[str, Any],
156+
annotations: dict[str, Any],
157+
inherited_fields: dict[str, ComponentField],
158+
) -> dict[str, ComponentField]:
159+
own_fields: dict[str, ComponentField] = {}
213160

214-
for key, annotation in resolved_annotations.items():
161+
for key, annotation in annotations.items():
215162
value = namespace.get(key, MISSING)
216163

217164
if types.is_classvar(annotation):
@@ -244,16 +191,63 @@ def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> ty
244191

245192
own_fields[key] = value
246193

247-
namespace["_own_fields"] = own_fields
248-
namespace["_inherited_fields"] = inherited_fields
249-
all_fields = inherited_fields | own_fields
250-
namespace["_fields"] = all_fields
194+
return own_fields
195+
196+
@classmethod
197+
def _create_field(
198+
cls,
199+
annotated_type: Any,
200+
default: Any = MISSING,
201+
default_factory: Callable[[], Any] | None = None,
202+
) -> ComponentField:
203+
return ComponentField(
204+
annotated_type=annotated_type,
205+
default=default,
206+
default_factory=default_factory,
207+
is_javascript=True, # Default for components
208+
)
209+
210+
@classmethod
211+
def _process_field_overrides(
212+
cls,
213+
namespace: dict[str, Any],
214+
annotations: dict[str, Any],
215+
inherited_fields: dict[str, Any],
216+
) -> dict[str, ComponentField]:
217+
own_fields: dict[str, ComponentField] = {}
218+
219+
for key, value, inherited_field in [
220+
(key, value, inherited_field)
221+
for key, value in namespace.items()
222+
if key not in annotations
223+
and ((inherited_field := inherited_fields.get(key)) is not None)
224+
]:
225+
new_field = ComponentField(
226+
default=value,
227+
is_javascript=inherited_field.is_javascript,
228+
annotated_type=inherited_field.annotated_type,
229+
)
230+
own_fields[key] = new_field
231+
232+
return own_fields
233+
234+
@classmethod
235+
def _finalize_fields(
236+
cls,
237+
namespace: dict[str, Any],
238+
inherited_fields: dict[str, ComponentField],
239+
own_fields: dict[str, ComponentField],
240+
) -> None:
241+
# Call parent implementation
242+
super()._finalize_fields(namespace, inherited_fields, own_fields)
243+
244+
# Add JavaScript fields mapping
245+
all_fields = namespace["_fields"]
251246
namespace["_js_fields"] = {
252247
key: value
253248
for key, value in all_fields.items()
254249
if value.is_javascript is True
255250
}
256-
return super().__new__(cls, name, bases, namespace)
257251

258252

259253
class BaseComponent(metaclass=BaseComponentMeta):

reflex/components/field.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""Shared field infrastructure for components and props."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from dataclasses import _MISSING_TYPE, MISSING
7+
from typing import Annotated, Any, Generic, TypeVar, get_origin
8+
9+
FIELD_TYPE = TypeVar("FIELD_TYPE")
10+
11+
12+
class BaseField(Generic[FIELD_TYPE]):
13+
"""Base field class used by internal metadata classes."""
14+
15+
def __init__(
16+
self,
17+
default: FIELD_TYPE | _MISSING_TYPE = MISSING,
18+
default_factory: Callable[[], FIELD_TYPE] | None = None,
19+
annotated_type: type[Any] | _MISSING_TYPE = MISSING,
20+
) -> None:
21+
"""Initialize the field.
22+
23+
Args:
24+
default: The default value for the field.
25+
default_factory: The default factory for the field.
26+
annotated_type: The annotated type for the field.
27+
"""
28+
self.default = default
29+
self.default_factory = default_factory
30+
self.outer_type_ = self.annotated_type = annotated_type
31+
32+
# Process type annotation
33+
type_origin = get_origin(annotated_type) or annotated_type
34+
if type_origin is Annotated:
35+
type_origin = annotated_type.__origin__ # pyright: ignore [reportAttributeAccessIssue]
36+
# For Annotated types, use the actual type inside the annotation
37+
self.type_ = annotated_type
38+
else:
39+
# For other types (including Union), preserve the original type
40+
self.type_ = annotated_type
41+
self.type_origin = type_origin
42+
43+
def default_value(self) -> FIELD_TYPE:
44+
"""Get the default value for the field.
45+
46+
Returns:
47+
The default value for the field.
48+
49+
Raises:
50+
ValueError: If no default value or factory is provided.
51+
"""
52+
if self.default is not MISSING:
53+
return self.default
54+
if self.default_factory is not None:
55+
return self.default_factory()
56+
msg = "No default value or factory provided."
57+
raise ValueError(msg)
58+
59+
60+
class FieldBasedMeta(type):
61+
"""Shared metaclass for field-based classes like components and props.
62+
63+
Provides common field inheritance and processing logic for both
64+
PropsBaseMeta and BaseComponentMeta.
65+
"""
66+
67+
def __new__(cls, name: str, bases: tuple[type], namespace: dict[str, Any]) -> type:
68+
"""Create a new field-based class.
69+
70+
Args:
71+
name: The name of the class.
72+
bases: The base classes.
73+
namespace: The class namespace.
74+
75+
Returns:
76+
The new class.
77+
"""
78+
# Collect inherited fields from base classes
79+
inherited_fields = cls._collect_inherited_fields(bases)
80+
81+
# Get annotations from the namespace
82+
annotations = cls._resolve_annotations(namespace, name)
83+
84+
# Process field overrides (fields with values but no annotations)
85+
own_fields = cls._process_field_overrides(
86+
namespace, annotations, inherited_fields
87+
)
88+
89+
# Process annotated fields
90+
own_fields.update(
91+
cls._process_annotated_fields(namespace, annotations, inherited_fields)
92+
)
93+
94+
# Finalize fields and store on class
95+
cls._finalize_fields(namespace, inherited_fields, own_fields)
96+
97+
return super().__new__(cls, name, bases, namespace)
98+
99+
@classmethod
100+
def _collect_inherited_fields(cls, bases: tuple[type]) -> dict[str, Any]:
101+
inherited_fields: dict[str, Any] = {}
102+
103+
# Collect inherited fields from base classes
104+
for base in bases[::-1]:
105+
if hasattr(base, "_inherited_fields"):
106+
inherited_fields.update(base._inherited_fields)
107+
for base in bases[::-1]:
108+
if hasattr(base, "_own_fields"):
109+
inherited_fields.update(base._own_fields)
110+
111+
return inherited_fields
112+
113+
@classmethod
114+
def _resolve_annotations(
115+
cls, namespace: dict[str, Any], name: str
116+
) -> dict[str, Any]:
117+
return namespace.get("__annotations__", {})
118+
119+
@classmethod
120+
def _process_field_overrides(
121+
cls,
122+
namespace: dict[str, Any],
123+
annotations: dict[str, Any],
124+
inherited_fields: dict[str, Any],
125+
) -> dict[str, Any]:
126+
own_fields: dict[str, Any] = {}
127+
128+
for key, value in namespace.items():
129+
if key not in annotations and key in inherited_fields:
130+
inherited_field = inherited_fields[key]
131+
new_field = cls._create_field(
132+
annotated_type=inherited_field.annotated_type,
133+
default=value,
134+
default_factory=None,
135+
)
136+
own_fields[key] = new_field
137+
138+
return own_fields
139+
140+
@classmethod
141+
def _process_annotated_fields(
142+
cls,
143+
namespace: dict[str, Any],
144+
annotations: dict[str, Any],
145+
inherited_fields: dict[str, Any],
146+
) -> dict[str, Any]:
147+
raise NotImplementedError
148+
149+
@classmethod
150+
def _create_field(
151+
cls,
152+
annotated_type: Any,
153+
default: Any = MISSING,
154+
default_factory: Callable[[], Any] | None = None,
155+
) -> Any:
156+
raise NotImplementedError
157+
158+
@classmethod
159+
def _finalize_fields(
160+
cls,
161+
namespace: dict[str, Any],
162+
inherited_fields: dict[str, Any],
163+
own_fields: dict[str, Any],
164+
) -> None:
165+
# Combine all fields
166+
all_fields = inherited_fields | own_fields
167+
168+
# Set field names for compatibility
169+
for field_name, field in all_fields.items():
170+
field._name = field_name
171+
172+
# Store field mappings on the class
173+
namespace["_own_fields"] = own_fields
174+
namespace["_inherited_fields"] = inherited_fields
175+
namespace["_fields"] = all_fields

0 commit comments

Comments
 (0)