1
+ # pyright: reportIncompatibleMethodOverride=false
1
2
from __future__ import annotations
2
3
3
4
import json
4
- from collections .abc import Mapping , Sequence
5
- from functools import wraps
6
- from typing import Any , Callable , Protocol , cast
5
+ from collections .abc import Iterable , Mapping , Sequence
6
+ from typing import (
7
+ Any ,
8
+ Callable ,
9
+ Unpack ,
10
+ cast ,
11
+ overload ,
12
+ )
7
13
8
14
from fastjsonschema import compile as compile_json_schema
9
15
12
18
from reactpy .core ._f_back import f_module_name
13
19
from reactpy .core .events import EventHandler , to_event_handler_function
14
20
from reactpy .types import (
21
+ ALLOWED_VDOM_KEYS ,
15
22
ComponentType ,
23
+ CustomVdomConstructor ,
24
+ EllipsisRepr ,
16
25
EventHandlerDict ,
17
26
EventHandlerType ,
18
- ImportSourceDict ,
19
- Key ,
20
27
VdomAttributes ,
21
- VdomChild ,
22
28
VdomChildren ,
23
- VdomDict ,
24
- VdomDictConstructor ,
25
29
VdomJson ,
30
+ _VdomDict ,
26
31
)
27
32
28
33
VDOM_JSON_SCHEMA = {
@@ -124,98 +129,83 @@ def is_vdom(value: Any) -> bool:
124
129
)
125
130
126
131
127
- def vdom (tag : str , * attributes_and_children : VdomAttributes | VdomChildren ) -> VdomDict :
128
- """A helper function for creating VDOM elements.
129
-
130
- Parameters:
131
- tag:
132
- The type of element (e.g. 'div', 'h1', 'img')
133
- attributes_and_children:
134
- An optional attribute mapping followed by any number of children or
135
- iterables of children. The attribute mapping **must** precede the children,
136
- or children which will be merged into their respective parts of the model.
137
- key:
138
- A string indicating the identity of a particular element. This is significant
139
- to preserve event handlers across updates - without a key, a re-render would
140
- cause these handlers to be deleted, but with a key, they would be redirected
141
- to any newly defined handlers.
142
- event_handlers:
143
- Maps event types to coroutines that are responsible for handling those events.
144
- import_source:
145
- (subject to change) specifies javascript that, when evaluated returns a
146
- React component.
147
- """
148
- model : VdomDict = {"tagName" : tag }
132
+ class Vdom :
133
+ """Class that follows VDOM spec, and exposes the user API that can create VDOM elements."""
149
134
150
- if not attributes_and_children :
151
- return model
135
+ def __init__ (
136
+ self ,
137
+ / ,
138
+ allow_children : bool = True ,
139
+ custom_constructor : CustomVdomConstructor | None = None ,
140
+ ** kwargs : Unpack [_VdomDict ],
141
+ ) -> None :
142
+ """This init method is used to declare the VDOM dictionary default values, as well as configurable properties
143
+ related to the construction of VDOM dictionaries."""
144
+ if "tagName" not in kwargs :
145
+ msg = "You must specify a 'tagName' for a VDOM element."
146
+ raise ValueError (msg )
147
+ self ._validate_keys (kwargs .keys ())
148
+ self .allow_children = allow_children
149
+ self .custom_constructor = custom_constructor
150
+ self .default_values = kwargs
151
+
152
+ # Configure Python debugger attributes
153
+ self .__name__ = kwargs ["tagName" ]
154
+ module_name = f_module_name (1 )
155
+ if module_name :
156
+ self .__module__ = module_name
157
+ self .__qualname__ = f"{ module_name } .{ kwargs ['tagName' ]} "
158
+
159
+ @overload
160
+ def __call__ (
161
+ self , attributes : VdomAttributes , / , * children : VdomChildren
162
+ ) -> _VdomDict : ...
152
163
153
- attributes , children = separate_attributes_and_children (attributes_and_children )
154
- key = attributes .pop ("key" , None )
155
- attributes , event_handlers = separate_attributes_and_event_handlers (attributes )
164
+ @overload
165
+ def __call__ (self , * children : VdomChildren ) -> _VdomDict : ...
156
166
157
- if attributes :
167
+ def __call__ (
168
+ self , * attributes_and_children : VdomAttributes | VdomChildren
169
+ ) -> _VdomDict :
170
+ """The entry point for the VDOM API, for example reactpy.html(<WE_ARE_HERE>)."""
171
+ attributes , children = separate_attributes_and_children (attributes_and_children )
172
+ key = attributes .pop ("key" , None )
173
+ attributes , event_handlers = separate_attributes_and_event_handlers (attributes )
158
174
if REACTPY_CHECK_JSON_ATTRS .current :
159
175
json .dumps (attributes )
160
- model ["attributes" ] = attributes
161
-
162
- if children :
163
- model ["children" ] = children
164
-
165
- if key is not None :
166
- model ["key" ] = key
167
176
168
- if event_handlers :
169
- model ["eventHandlers" ] = event_handlers
170
-
171
- return model
172
-
173
-
174
- def make_vdom_constructor (
175
- tag : str , allow_children : bool = True , import_source : ImportSourceDict | None = None
176
- ) -> VdomDictConstructor :
177
- """Return a constructor for VDOM dictionaries with the given tag name.
178
-
179
- The resulting callable will have the same interface as :func:`vdom` but without its
180
- first ``tag`` argument.
181
- """
182
-
183
- def constructor (* attributes_and_children : Any , ** kwargs : Any ) -> VdomDict :
184
- model = vdom (tag , * attributes_and_children , ** kwargs )
185
- if not allow_children and "children" in model :
186
- msg = f"{ tag !r} nodes cannot have children."
177
+ # Run custom constructor, if defined
178
+ if self .custom_constructor :
179
+ result = self .custom_constructor (
180
+ key = key ,
181
+ children = children ,
182
+ attributes = attributes ,
183
+ event_handlers = event_handlers ,
184
+ )
185
+ # Otherwise, use the default constructor
186
+ else :
187
+ result = {
188
+ ** ({"key" : key } if key is not None else {}),
189
+ ** ({"children" : children } if children else {}),
190
+ ** ({"attributes" : attributes } if attributes else {}),
191
+ ** ({"eventHandlers" : event_handlers } if event_handlers else {}),
192
+ }
193
+
194
+ # Validate the result
195
+ if children and not self .allow_children :
196
+ msg = f"{ self .default_values .get ('tagName' )!r} nodes cannot have children."
187
197
raise TypeError (msg )
188
- if import_source :
189
- model ["importSource" ] = import_source
190
- return model
191
-
192
- # replicate common function attributes
193
- constructor .__name__ = tag
194
- constructor .__doc__ = (
195
- "Return a new "
196
- f"`<{ tag } > <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/{ tag } >`__ "
197
- "element represented by a :class:`VdomDict`."
198
- )
199
-
200
- module_name = f_module_name (1 )
201
- if module_name :
202
- constructor .__module__ = module_name
203
- constructor .__qualname__ = f"{ module_name } .{ tag } "
198
+ if REACTPY_DEBUG .current :
199
+ self ._validate_keys (result .keys ())
204
200
205
- return cast (VdomDictConstructor , constructor )
201
+ return cast (_VdomDict , self . default_values | result )
206
202
207
-
208
- def custom_vdom_constructor (func : _CustomVdomDictConstructor ) -> VdomDictConstructor :
209
- """Cast function to VdomDictConstructor"""
210
-
211
- @wraps (func )
212
- def wrapper (* attributes_and_children : Any ) -> VdomDict :
213
- attributes , children = separate_attributes_and_children (attributes_and_children )
214
- key = attributes .pop ("key" , None )
215
- attributes , event_handlers = separate_attributes_and_event_handlers (attributes )
216
- return func (attributes , children , key , event_handlers )
217
-
218
- return cast (VdomDictConstructor , wrapper )
203
+ @staticmethod
204
+ def _validate_keys (keys : Sequence [str ] | Iterable [str ]) -> None :
205
+ invalid_keys = set (keys ) - ALLOWED_VDOM_KEYS
206
+ if invalid_keys :
207
+ msg = f"Invalid keys { invalid_keys } provided."
208
+ raise ValueError (msg )
219
209
220
210
221
211
def separate_attributes_and_children (
@@ -224,48 +214,53 @@ def separate_attributes_and_children(
224
214
if not values :
225
215
return {}, []
226
216
227
- attributes : VdomAttributes
217
+ _attributes : VdomAttributes
228
218
children_or_iterables : Sequence [Any ]
229
219
if _is_attributes (values [0 ]):
230
- attributes , * children_or_iterables = values
220
+ _attributes , * children_or_iterables = values
231
221
else :
232
- attributes = {}
222
+ _attributes = {}
233
223
children_or_iterables = values
234
224
235
- children : list [Any ] = []
236
- for child in children_or_iterables :
237
- if _is_single_child (child ):
238
- children .append (child )
239
- else :
240
- children .extend (child )
225
+ _children : list [Any ] = _flatten_children (children_or_iterables )
241
226
242
- return attributes , children
227
+ return _attributes , _children
243
228
244
229
245
230
def separate_attributes_and_event_handlers (
246
231
attributes : Mapping [str , Any ],
247
232
) -> tuple [VdomAttributes , EventHandlerDict ]:
248
- separated_attributes : VdomAttributes = {}
249
- separated_event_handlers : dict [str , EventHandlerType ] = {}
233
+ _attributes : VdomAttributes = {}
234
+ _event_handlers : dict [str , EventHandlerType ] = {}
250
235
251
236
for k , v in attributes .items ():
252
237
handler : EventHandlerType
253
238
254
239
if callable (v ):
255
240
handler = EventHandler (to_event_handler_function (v ))
256
241
elif (
257
- # isinstance check on protocols is slow - use function attr pre-check as a
258
- # quick filter before actually performing slow EventHandlerType type check
242
+ # ` isinstance` check on `Protocol` types is slow. We use pre-checks as an optimization
243
+ # before actually performing slow EventHandlerType type check
259
244
hasattr (v , "function" ) and isinstance (v , EventHandlerType )
260
245
):
261
246
handler = v
262
247
else :
263
- separated_attributes [k ] = v
248
+ _attributes [k ] = v
264
249
continue
265
250
266
- separated_event_handlers [k ] = handler
251
+ _event_handlers [k ] = handler
252
+
253
+ return _attributes , _event_handlers
267
254
268
- return separated_attributes , separated_event_handlers
255
+
256
+ def _flatten_children (children : Sequence [Any ]) -> list [Any ]:
257
+ _children : list [VdomChildren ] = []
258
+ for child in children :
259
+ if _is_single_child (child ):
260
+ _children .append (child )
261
+ else :
262
+ _children .extend (_flatten_children (child ))
263
+ return _children
269
264
270
265
271
266
def _is_attributes (value : Any ) -> bool :
@@ -292,20 +287,5 @@ def _validate_child_key_integrity(value: Any) -> None:
292
287
warn (f"Key not specified for child in list { child } " , UserWarning )
293
288
elif isinstance (child , Mapping ) and "key" not in child :
294
289
# remove 'children' to reduce log spam
295
- child_copy = {** child , "children" : _EllipsisRepr ()}
290
+ child_copy = {** child , "children" : EllipsisRepr ()}
296
291
warn (f"Key not specified for child in list { child_copy } " , UserWarning )
297
-
298
-
299
- class _CustomVdomDictConstructor (Protocol ):
300
- def __call__ (
301
- self ,
302
- attributes : VdomAttributes ,
303
- children : Sequence [VdomChild ],
304
- key : Key | None ,
305
- event_handlers : EventHandlerDict ,
306
- ) -> VdomDict : ...
307
-
308
-
309
- class _EllipsisRepr :
310
- def __repr__ (self ) -> str :
311
- return "..."
0 commit comments