Skip to content

Commit 5cfed3e

Browse files
committed
initial work to unify vdom constructor interface
1 parent cc9518d commit 5cfed3e

File tree

13 files changed

+119
-141
lines changed

13 files changed

+119
-141
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
session-name: test_python_suite
2424
session-arguments: --maxfail=3 --no-cov
2525
runs-on-array: '["ubuntu-latest", "macos-latest", "windows-latest"]'
26-
python-version-array: '["3.7", "3.8", "3.9", "3.10"]'
26+
python-version-array: '["3.8", "3.9", "3.10", "3.11"]'
2727
docs:
2828
uses: ./.github/workflows/.nox-session.yml
2929
with:

src/client/packages/idom-client-react/src/element-utils.js

+11-7
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,16 @@ export function createElementAttributes(model, sendEvent) {
2222

2323
if (model.eventHandlers) {
2424
for (const [eventName, eventSpec] of Object.entries(model.eventHandlers)) {
25-
attributes[eventName] = createEventHandler(
26-
eventName,
27-
sendEvent,
28-
eventSpec
29-
);
25+
attributes[eventName] = createEventHandler(sendEvent, eventSpec);
3026
}
3127
}
3228

33-
return attributes;
29+
return Object.fromEntries(
30+
Object.entries(attributes).map(([key, value]) => [snakeToCamel(key), value])
31+
);
3432
}
3533

36-
function createEventHandler(eventName, sendEvent, eventSpec) {
34+
function createEventHandler(sendEvent, eventSpec) {
3735
return function () {
3836
const data = Array.from(arguments).map((value) => {
3937
if (typeof value === "object" && value.nativeEvent) {
@@ -54,3 +52,9 @@ function createEventHandler(eventName, sendEvent, eventSpec) {
5452
});
5553
};
5654
}
55+
56+
function snakeToCamel(str) {
57+
return str
58+
.toLowerCase()
59+
.replace(/([-_][a-z])/g, (group) => group.toUpperCase().replace("_", ""));
60+
}

src/idom/core/component.py

+40-21
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,84 @@
11
from __future__ import annotations
22

33
import inspect
4+
from collections.abc import Sequence
45
from functools import wraps
5-
from typing import Any, Callable, Dict, Optional, Tuple
6+
from typing import Any, Callable
67

7-
from .types import ComponentType, VdomDict
8+
from .types import Key, RenderResult
9+
from .vdom import flatten_children
810

911

10-
def component(
11-
function: Callable[..., ComponentType | VdomDict | str | None]
12-
) -> Callable[..., Component]:
12+
def component(function: Callable[..., RenderResult]) -> Callable[..., Component]:
1313
"""A decorator for defining a new component.
1414
1515
Parameters:
1616
function: The component's :meth:`idom.core.proto.ComponentType.render` function.
1717
"""
1818
sig = inspect.signature(function)
1919

20-
if "key" in sig.parameters and sig.parameters["key"].kind in (
21-
inspect.Parameter.KEYWORD_ONLY,
22-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
23-
):
20+
if [
21+
param.name
22+
for param in sig.parameters.values()
23+
if param.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD
24+
or param.kind is inspect.Parameter.POSITIONAL_ONLY
25+
]:
26+
raise TypeError(
27+
"Position-only and positional-or-keyword parameters are disallowed - "
28+
"use variable positional arguments to define whether this component has "
29+
"children and keyword-only to define its attributes."
30+
)
31+
32+
if "key" in sig.parameters:
2433
raise TypeError(
2534
f"Component render function {function} uses reserved parameter 'key'"
2635
)
2736

2837
@wraps(function)
29-
def constructor(*args: Any, key: Optional[Any] = None, **kwargs: Any) -> Component:
30-
return Component(function, key, args, kwargs, sig)
38+
def constructor(
39+
*children: Any, key: Key | None = None, **attributes: Any
40+
) -> Component:
41+
return Component(function, key, flatten_children(children), attributes, sig)
3142

3243
return constructor
3344

3445

3546
class Component:
3647
"""An object for rending component models."""
3748

38-
__slots__ = "__weakref__", "_func", "_args", "_kwargs", "_sig", "key", "type"
49+
__slots__ = (
50+
"__weakref__",
51+
"_func",
52+
"_children",
53+
"_attributes",
54+
"_sig",
55+
"key",
56+
"type",
57+
)
3958

4059
def __init__(
4160
self,
42-
function: Callable[..., ComponentType | VdomDict | str | None],
43-
key: Optional[Any],
44-
args: Tuple[Any, ...],
45-
kwargs: Dict[str, Any],
61+
function: Callable[..., RenderResult],
62+
key: Key | None,
63+
children: Sequence[Any],
64+
attributes: dict[str, Any],
4665
sig: inspect.Signature,
4766
) -> None:
4867
self.key = key
4968
self.type = function
50-
self._args = args
51-
self._kwargs = kwargs
69+
self._children = children
70+
self._attributes = attributes
5271
self._sig = sig
5372

54-
def render(self) -> ComponentType | VdomDict | str | None:
55-
return self.type(*self._args, **self._kwargs)
73+
def render(self) -> RenderResult:
74+
return self.type(*self._children, **self._attributes)
5675

5776
def should_render(self, new: Component) -> bool:
5877
return True
5978

6079
def __repr__(self) -> str:
6180
try:
62-
args = self._sig.bind(*self._args, **self._kwargs).arguments
81+
args = self._sig.bind(*self._children, **self._attributes).arguments
6382
except TypeError:
6483
return f"{self.type.__name__}(...)"
6584
else:

src/idom/core/events.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def event(
4141
4242
.. code-block:: python
4343
44-
element = idom.html.button({"onClick": my_callback})
44+
element = idom.html.button(on_click=my_callback)
4545
4646
You may want the ability to prevent the default action associated with the event
4747
from taking place, or stoping the event from propagating up the DOM. This decorator
@@ -53,7 +53,7 @@ def event(
5353
def my_callback(*data):
5454
...
5555
56-
element = idom.html.button({"onClick": my_callback})
56+
element = idom.html.button(on_click=my_callback)
5757
5858
Parameters:
5959
function:

src/idom/core/types.py

+9-13
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,16 @@ class ComponentType(Protocol):
6262
This is used to see if two component instances share the same definition.
6363
"""
6464

65-
def render(self) -> VdomDict | ComponentType | str | None:
65+
def render(self) -> RenderResult:
6666
"""Render the component's view model."""
6767

6868
def should_render(self: _OwnType, new: _OwnType) -> bool:
6969
"""Whether the new component instance should be rendered."""
7070

7171

72+
RenderResult = Union["VdomDict", ComponentType, str, None]
73+
74+
7275
_Render = TypeVar("_Render", covariant=True)
7376
_Event = TypeVar("_Event", contravariant=True)
7477

@@ -104,13 +107,6 @@ async def __aexit__(
104107
VdomChildren = Sequence[VdomChild]
105108
"""Describes a series of :class:`VdomChild` elements"""
106109

107-
VdomAttributesAndChildren = Union[
108-
Mapping[str, Any], # this describes both VdomDict and VdomAttributes
109-
Iterable[VdomChild],
110-
VdomChild,
111-
]
112-
"""Useful for the ``*attributes_and_children`` parameter in :func:`idom.core.vdom.vdom`"""
113-
114110

115111
class _VdomDictOptional(TypedDict, total=False):
116112
key: Key | None
@@ -207,12 +203,12 @@ class EventHandlerType(Protocol):
207203

208204

209205
class VdomDictConstructor(Protocol):
210-
"""Standard function for constructing a :class:`VdomDict`"""
211-
212206
def __call__(
213207
self,
214-
*attributes_and_children: VdomAttributesAndChildren,
215-
key: str = ...,
216-
event_handlers: Optional[EventHandlerMapping] = ...,
208+
*children: VdomChild,
209+
key: str | int | None = None,
210+
event_handlers: EventHandlerMapping | None = None,
211+
import_source: ImportSourceDict | None = None,
212+
**attributes: Any,
217213
) -> VdomDict:
218214
...

src/idom/core/vdom.py

+35-64
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
EventHandlerMapping,
1818
EventHandlerType,
1919
ImportSourceDict,
20-
VdomAttributesAndChildren,
20+
Key,
21+
VdomChild,
2122
VdomDict,
23+
VdomDictConstructor,
2224
VdomJson,
2325
)
2426

@@ -129,43 +131,47 @@ def is_vdom(value: Any) -> bool:
129131

130132
def vdom(
131133
tag: str,
132-
*attributes_and_children: VdomAttributesAndChildren,
133-
key: str | int | None = None,
134-
event_handlers: Optional[EventHandlerMapping] = None,
135-
import_source: Optional[ImportSourceDict] = None,
134+
/,
135+
*children: VdomChild,
136+
key: Key | None = None,
137+
event_handlers: EventHandlerMapping | None = None,
138+
import_source: ImportSourceDict | None = None,
139+
**attributes: Any,
136140
) -> VdomDict:
137141
"""A helper function for creating VDOM dictionaries.
138142
139143
Parameters:
140144
tag:
141145
The type of element (e.g. 'div', 'h1', 'img')
142-
attributes_and_children:
143-
An optional attribute mapping followed by any number of children or
144-
iterables of children. The attribute mapping **must** precede the children,
145-
or children which will be merged into their respective parts of the model.
146+
children:
147+
String, compoennts, or other VDOM elements that are this element's children.
146148
key:
147-
A string idicating the identity of a particular element. This is significant
148-
to preserve event handlers across updates - without a key, a re-render would
149-
cause these handlers to be deleted, but with a key, they would be redirected
150-
to any newly defined handlers.
149+
A string or integer idicating the identity of a particular element. This is
150+
significant to preserve event handlers across updates - without a key, a
151+
re-render would cause these handlers to be deleted, but with a key, they
152+
would be redirected to any newly defined handlers.
151153
event_handlers:
152-
Maps event types to coroutines that are responsible for handling those events.
154+
Maps event types to coroutines that are responsible for handling those
155+
events.
153156
import_source:
154157
(subject to change) specifies javascript that, when evaluated returns a
155158
React component.
159+
attributes:
160+
Remaining attributes of this element.
156161
"""
157162
model: VdomDict = {"tagName": tag}
158163

159-
attributes, children = coalesce_attributes_and_children(attributes_and_children)
160164
attributes, event_handlers = separate_attributes_and_event_handlers(
161-
attributes, event_handlers or {}
165+
attributes, event_handlers
162166
)
163167

164168
if attributes:
169+
if "cls" in attributes:
170+
attributes["class"] = attributes.pop("cls")
165171
model["attributes"] = attributes
166172

167173
if children:
168-
model["children"] = children
174+
model["children"] = flatten_children(children)
169175

170176
if event_handlers:
171177
model["eventHandlers"] = event_handlers
@@ -179,39 +185,15 @@ def vdom(
179185
return model
180186

181187

182-
class _VdomDictConstructor(Protocol):
183-
def __call__(
184-
self,
185-
*attributes_and_children: VdomAttributesAndChildren,
186-
key: str | int | None = ...,
187-
event_handlers: Optional[EventHandlerMapping] = ...,
188-
import_source: Optional[ImportSourceDict] = ...,
189-
) -> VdomDict:
190-
...
191-
192-
193-
def make_vdom_constructor(
194-
tag: str, allow_children: bool = True
195-
) -> _VdomDictConstructor:
188+
def make_vdom_constructor(tag: str, allow_children: bool = True) -> VdomDictConstructor:
196189
"""Return a constructor for VDOM dictionaries with the given tag name.
197190
198191
The resulting callable will have the same interface as :func:`vdom` but without its
199192
first ``tag`` argument.
200193
"""
201194

202-
def constructor(
203-
*attributes_and_children: VdomAttributesAndChildren,
204-
key: str | int | None = None,
205-
event_handlers: Optional[EventHandlerMapping] = None,
206-
import_source: Optional[ImportSourceDict] = None,
207-
) -> VdomDict:
208-
model = vdom(
209-
tag,
210-
*attributes_and_children,
211-
key=key,
212-
event_handlers=event_handlers,
213-
import_source=import_source,
214-
)
195+
def constructor(*args: Any, **kwargs: Any) -> VdomDict:
196+
model = vdom(tag, *args, **kwargs)
215197
if not allow_children and "children" in model:
216198
raise TypeError(f"{tag!r} nodes cannot have children.")
217199
return model
@@ -232,35 +214,24 @@ def constructor(
232214
return constructor
233215

234216

235-
def coalesce_attributes_and_children(
236-
values: Sequence[Any],
237-
) -> Tuple[Mapping[str, Any], List[Any]]:
238-
if not values:
239-
return {}, []
240-
241-
children_or_iterables: Sequence[Any]
242-
attributes, *children_or_iterables = values
243-
if not _is_attributes(attributes):
244-
attributes = {}
245-
children_or_iterables = values
246-
247-
children: List[Any] = []
248-
for child in children_or_iterables:
217+
def flatten_children(children: Sequence[VdomChild]) -> Sequence[VdomChild]:
218+
child_list: list[VdomChild] = []
219+
for child in children:
249220
if _is_single_child(child):
250-
children.append(child)
221+
child_list.append(child)
251222
else:
252-
children.extend(child)
253-
254-
return attributes, children
223+
child_list.extend(child)
224+
return child_list
255225

256226

257227
def separate_attributes_and_event_handlers(
258-
attributes: Mapping[str, Any], event_handlers: EventHandlerMapping
228+
attributes: Mapping[str, Any],
229+
event_handlers: EventHandlerMapping | None = None,
259230
) -> Tuple[Dict[str, Any], EventHandlerDict]:
260231
separated_attributes = {}
261232
separated_event_handlers: Dict[str, List[EventHandlerType]] = {}
262233

263-
for k, v in event_handlers.items():
234+
for k, v in (event_handlers or {}).items():
264235
separated_event_handlers[k] = [v]
265236

266237
for k, v in attributes.items():

0 commit comments

Comments
 (0)