1+ # pyright: reportIncompatibleMethodOverride=false
12from __future__ import annotations
23
34import 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+ )
713
814from fastjsonschema import compile as compile_json_schema
915
1218from reactpy .core ._f_back import f_module_name
1319from reactpy .core .events import EventHandler , to_event_handler_function
1420from reactpy .types import (
21+ ALLOWED_VDOM_KEYS ,
1522 ComponentType ,
23+ CustomVdomConstructor ,
24+ EllipsisRepr ,
1625 EventHandlerDict ,
1726 EventHandlerType ,
18- ImportSourceDict ,
19- Key ,
2027 VdomAttributes ,
21- VdomChild ,
2228 VdomChildren ,
23- VdomDict ,
24- VdomDictConstructor ,
2529 VdomJson ,
30+ _VdomDict ,
2631)
2732
2833VDOM_JSON_SCHEMA = {
@@ -124,98 +129,83 @@ def is_vdom(value: Any) -> bool:
124129 )
125130
126131
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."""
149134
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 : ...
152163
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 : ...
156166
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 )
158174 if REACTPY_CHECK_JSON_ATTRS .current :
159175 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
167176
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."
187197 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 ())
204200
205- return cast (VdomDictConstructor , constructor )
201+ return cast (_VdomDict , self . default_values | result )
206202
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 )
219209
220210
221211def separate_attributes_and_children (
@@ -224,48 +214,53 @@ def separate_attributes_and_children(
224214 if not values :
225215 return {}, []
226216
227- attributes : VdomAttributes
217+ _attributes : VdomAttributes
228218 children_or_iterables : Sequence [Any ]
229219 if _is_attributes (values [0 ]):
230- attributes , * children_or_iterables = values
220+ _attributes , * children_or_iterables = values
231221 else :
232- attributes = {}
222+ _attributes = {}
233223 children_or_iterables = values
234224
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 )
241226
242- return attributes , children
227+ return _attributes , _children
243228
244229
245230def separate_attributes_and_event_handlers (
246231 attributes : Mapping [str , Any ],
247232) -> tuple [VdomAttributes , EventHandlerDict ]:
248- separated_attributes : VdomAttributes = {}
249- separated_event_handlers : dict [str , EventHandlerType ] = {}
233+ _attributes : VdomAttributes = {}
234+ _event_handlers : dict [str , EventHandlerType ] = {}
250235
251236 for k , v in attributes .items ():
252237 handler : EventHandlerType
253238
254239 if callable (v ):
255240 handler = EventHandler (to_event_handler_function (v ))
256241 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
259244 hasattr (v , "function" ) and isinstance (v , EventHandlerType )
260245 ):
261246 handler = v
262247 else :
263- separated_attributes [k ] = v
248+ _attributes [k ] = v
264249 continue
265250
266- separated_event_handlers [k ] = handler
251+ _event_handlers [k ] = handler
252+
253+ return _attributes , _event_handlers
267254
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
269264
270265
271266def _is_attributes (value : Any ) -> bool :
@@ -292,20 +287,5 @@ def _validate_child_key_integrity(value: Any) -> None:
292287 warn (f"Key not specified for child in list { child } " , UserWarning )
293288 elif isinstance (child , Mapping ) and "key" not in child :
294289 # remove 'children' to reduce log spam
295- child_copy = {** child , "children" : _EllipsisRepr ()}
290+ child_copy = {** child , "children" : EllipsisRepr ()}
296291 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