Skip to content

Commit bcae270

Browse files
authored
Merge branch 'main' into main
2 parents 95f59a1 + dcd30c2 commit bcae270

File tree

15 files changed

+398
-127
lines changed

15 files changed

+398
-127
lines changed

.pre-commit-config.yaml

-4
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ repos:
33
rev: 22.1.0
44
hooks:
55
- id: black
6-
- repo: https://github.com/PyCQA/flake8
7-
rev: 3.7.9
8-
hooks:
9-
- id: flake8
106
- repo: https://github.com/pycqa/isort
117
rev: 5.6.3
128
hooks:

requirements/pkg-extras.txt

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ uvicorn[standard] >=0.13.4
1212

1313
# extra=flask
1414
flask<2.0
15+
markupsafe<2.1
1516
flask-cors
1617
flask-sockets
1718

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ testpaths = tests
2424
xfail_strict = True
2525
markers =
2626
slow: marks tests as slow (deselect with '-m "not slow"')
27+
python_files = assert_*.py test_*.py
2728

2829
[coverage:report]
2930
fail_under = 100

src/client/packages/idom-client-react/src/components.js

+14-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ export function Layout({ saveUpdateHook, sendEvent, loadImportSource }) {
2020

2121
React.useEffect(() => saveUpdateHook(patchModel), [patchModel]);
2222

23+
if (!Object.keys(model).length) {
24+
return html`<${React.Fragment} />`;
25+
}
26+
2327
return html`
2428
<${LayoutContext.Provider} value=${{ sendEvent, loadImportSource }}>
2529
<${Element} model=${model} />
@@ -28,7 +32,7 @@ export function Layout({ saveUpdateHook, sendEvent, loadImportSource }) {
2832
}
2933

3034
export function Element({ model }) {
31-
if (!model.tagName) {
35+
if (model.error !== undefined) {
3236
if (model.error) {
3337
return html`<pre>${model.error}</pre>`;
3438
} else {
@@ -45,11 +49,19 @@ export function Element({ model }) {
4549

4650
function StandardElement({ model }) {
4751
const layoutContext = React.useContext(LayoutContext);
52+
53+
let type;
54+
if (model.tagName == "") {
55+
type = React.Fragment;
56+
} else {
57+
type = model.tagName;
58+
}
59+
4860
// Use createElement here to avoid warning about variable numbers of children not
4961
// having keys. Warning about this must now be the responsibility of the server
5062
// providing the models instead of the client rendering them.
5163
return React.createElement(
52-
model.tagName,
64+
type,
5365
createElementAttributes(model, layoutContext.sendEvent),
5466
...createElementChildren(
5567
model,

src/idom/core/component.py

+19-27
Original file line numberDiff line numberDiff line change
@@ -8,68 +8,60 @@
88

99

1010
def component(
11-
function: Callable[..., Union[ComponentType, VdomDict]]
11+
function: Callable[..., Union[ComponentType, VdomDict | None]]
1212
) -> Callable[..., "Component"]:
13-
"""A decorator for defining an :class:`Component`.
13+
"""A decorator for defining a new component.
1414
1515
Parameters:
16-
function: The function that will render a :class:`VdomDict`.
16+
function: The component's :meth:`idom.core.proto.ComponentType.render` function.
1717
"""
1818
sig = inspect.signature(function)
19-
key_is_kwarg = "key" in sig.parameters and sig.parameters["key"].kind in (
19+
20+
if "key" in sig.parameters and sig.parameters["key"].kind in (
2021
inspect.Parameter.KEYWORD_ONLY,
2122
inspect.Parameter.POSITIONAL_OR_KEYWORD,
22-
)
23-
if key_is_kwarg:
23+
):
2424
raise TypeError(
2525
f"Component render function {function} uses reserved parameter 'key'"
2626
)
2727

2828
@wraps(function)
2929
def constructor(*args: Any, key: Optional[Any] = None, **kwargs: Any) -> Component:
30-
if key_is_kwarg:
31-
kwargs["key"] = key
32-
return Component(function, key, args, kwargs)
30+
return Component(function, key, args, kwargs, sig)
3331

3432
return constructor
3533

3634

3735
class Component:
3836
"""An object for rending component models."""
3937

40-
__slots__ = "__weakref__", "_func", "_args", "_kwargs", "key"
38+
__slots__ = "__weakref__", "_func", "_args", "_kwargs", "_sig", "key", "type"
4139

4240
def __init__(
4341
self,
44-
function: Callable[..., Union[ComponentType, VdomDict]],
42+
function: Callable[..., ComponentType | VdomDict | None],
4543
key: Optional[Any],
4644
args: Tuple[Any, ...],
4745
kwargs: Dict[str, Any],
46+
sig: inspect.Signature,
4847
) -> None:
48+
self.key = key
49+
self.type = function
4950
self._args = args
50-
self._func = function
5151
self._kwargs = kwargs
52-
self.key = key
53-
54-
@property
55-
def definition_id(self) -> int:
56-
return id(self._func)
52+
self._sig = sig
5753

58-
def render(self) -> VdomDict:
59-
model = self._func(*self._args, **self._kwargs)
60-
if isinstance(model, ComponentType):
61-
model = {"tagName": "div", "children": [model]}
62-
return model
54+
def render(self) -> VdomDict | ComponentType | None:
55+
return self.type(*self._args, **self._kwargs)
6356

6457
def __repr__(self) -> str:
65-
sig = inspect.signature(self._func)
6658
try:
67-
args = sig.bind(*self._args, **self._kwargs).arguments
59+
args = self._sig.bind(*self._args, **self._kwargs).arguments
6860
except TypeError:
69-
return f"{self._func.__name__}(...)"
61+
return f"{self.type.__name__}(...)"
7062
else:
7163
items = ", ".join(f"{k}={v!r}" for k, v in args.items())
7264
if items:
73-
return f"{self._func.__name__}({id(self):02x}, {items})"
65+
return f"{self.type.__name__}({id(self):02x}, {items})"
7466
else:
75-
return f"{self._func.__name__}({id(self):02x})"
67+
return f"{self.type.__name__}({id(self):02x})"

src/idom/core/layout.py

+24-16
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
Set,
1919
Tuple,
2020
TypeVar,
21+
cast,
2122
)
2223
from uuid import uuid4
2324
from weakref import ref as weakref
@@ -31,7 +32,7 @@
3132

3233
from ._event_proxy import _wrap_in_warning_event_proxies
3334
from .hooks import LifeCycleHook
34-
from .proto import ComponentType, EventHandlerDict, VdomJson
35+
from .proto import ComponentType, EventHandlerDict, VdomDict, VdomJson
3536
from .vdom import validate_vdom_json
3637

3738

@@ -199,7 +200,15 @@ def _render_component(
199200
raw_model = component.render()
200201
finally:
201202
life_cycle_hook.unset_current()
202-
self._render_model(old_state, new_state, raw_model)
203+
204+
# wrap the model in a fragment (i.e. tagName="") to ensure components have
205+
# a separate node in the model state tree. This could be removed if this
206+
# components are given a node in the tree some other way
207+
wrapper_model: VdomDict = {"tagName": ""}
208+
if raw_model is not None:
209+
wrapper_model["children"] = [raw_model]
210+
211+
self._render_model(old_state, new_state, wrapper_model)
203212
except Exception as error:
204213
logger.exception(f"Failed to render {component}")
205214
new_state.model.current = {
@@ -233,15 +242,6 @@ def _render_model(
233242
new_state.key = new_state.model.current["key"] = raw_model["key"]
234243
if "importSource" in raw_model:
235244
new_state.model.current["importSource"] = raw_model["importSource"]
236-
237-
if old_state is not None and old_state.key != new_state.key:
238-
self._unmount_model_states([old_state])
239-
if new_state.is_component_state:
240-
self._model_states_by_life_cycle_state_id[
241-
new_state.life_cycle_state.id
242-
] = new_state
243-
old_state = None
244-
245245
self._render_model_attributes(old_state, new_state, raw_model)
246246
self._render_model_children(old_state, new_state, raw_model.get("children", []))
247247

@@ -371,6 +371,7 @@ def _render_model_children(
371371
new_children.append(new_child_state.model.current)
372372
new_state.children_by_key[key] = new_child_state
373373
elif child_type is _COMPONENT_TYPE:
374+
child = cast(ComponentType, child)
374375
old_child_state = old_state.children_by_key.get(key)
375376
if old_child_state is None:
376377
new_child_state = _make_component_model_state(
@@ -381,8 +382,7 @@ def _render_model_children(
381382
self._rendering_queue.put,
382383
)
383384
elif old_child_state.is_component_state and (
384-
old_child_state.life_cycle_state.component.definition_id
385-
!= child.definition_id
385+
old_child_state.life_cycle_state.component.type != child.type
386386
):
387387
self._unmount_model_states([old_child_state])
388388
old_child_state = None
@@ -411,10 +411,18 @@ def _render_model_children(
411411
def _render_model_children_without_old_state(
412412
self, new_state: _ModelState, raw_children: List[Any]
413413
) -> None:
414+
child_type_key_tuples = list(_process_child_type_and_key(raw_children))
415+
416+
new_keys = {item[2] for item in child_type_key_tuples}
417+
if len(new_keys) != len(raw_children):
418+
key_counter = Counter(item[2] for item in child_type_key_tuples)
419+
duplicate_keys = [key for key, count in key_counter.items() if count > 1]
420+
raise ValueError(
421+
f"Duplicate keys {duplicate_keys} at {new_state.patch_path or '/'!r}"
422+
)
423+
414424
new_children = new_state.model.current["children"] = []
415-
for index, (child, child_type, key) in enumerate(
416-
_process_child_type_and_key(raw_children)
417-
):
425+
for index, (child, child_type, key) in enumerate(child_type_key_tuples):
418426
if child_type is _DICT_TYPE:
419427
child_state = _make_element_model_state(new_state, index, key)
420428
self._render_model(None, child_state, child)

src/idom/core/proto.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,14 @@ class ComponentType(Protocol):
3232
key: Key | None
3333
"""An identifier which is unique amongst a component's immediate siblings"""
3434

35-
@property
36-
def definition_id(self) -> int:
37-
"""A globally unique identifier for this component definition.
35+
type: type[Any] | Callable[..., Any]
36+
"""The function or class defining the behavior of this component
3837
39-
Usually the :func:`id` of this class or an underlying function.
40-
"""
38+
This is used to see if two component instances share the same definition.
39+
"""
4140

42-
def render(self) -> VdomDict:
43-
"""Render the component's :class:`VdomDict`."""
41+
def render(self) -> VdomDict | ComponentType | None:
42+
"""Render the component's view model."""
4443

4544

4645
_Self = TypeVar("_Self")
@@ -88,7 +87,7 @@ class _VdomDictOptional(TypedDict, total=False):
8887
children: Sequence[
8988
# recursive types are not allowed yet:
9089
# https://github.com/python/mypy/issues/731
91-
Union[ComponentType, Dict[str, Any], str]
90+
Union[ComponentType, Dict[str, Any], str, Any]
9291
]
9392
attributes: VdomAttributes
9493
eventHandlers: EventHandlerDict # noqa

src/idom/html.py

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
"""
2+
**Fragment**
3+
4+
- :func:`_`
5+
26
**Dcument metadata**
37
48
- :func:`base`
@@ -148,6 +152,8 @@
148152
149153
- :func:`slot`
150154
- :func:`template`
155+
156+
.. autofunction:: _
151157
"""
152158

153159
from __future__ import annotations
@@ -158,6 +164,14 @@
158164
from .core.vdom import coalesce_attributes_and_children, make_vdom_constructor
159165

160166

167+
def _(*children: Any) -> VdomDict:
168+
"""An HTML fragment - this element will not appear in the DOM"""
169+
attributes, coalesced_children = coalesce_attributes_and_children(children)
170+
if attributes:
171+
raise TypeError("Fragments cannot have attributes")
172+
return {"tagName": "", "children": coalesced_children}
173+
174+
161175
# Dcument metadata
162176
base = make_vdom_constructor("base")
163177
head = make_vdom_constructor("head")

temp.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import idom
2+
3+
4+
@idom.component
5+
def Demo():
6+
return idom.vdom("", idom.html.h1("hello"))
7+
8+
9+
idom.run(Demo)

tests/assert_utils.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
def assert_same_items(left, right):
2+
"""Check that two unordered sequences are equal (only works if reprs are equal)"""
3+
sorted_left = list(sorted(left, key=repr))
4+
sorted_right = list(sorted(right, key=repr))
5+
assert sorted_left == sorted_right

tests/general_utils.py

-22
This file was deleted.

tests/test_core/test_dispatcher.py

+13-8
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,20 @@ async def recv():
5757
def make_events_and_expected_model():
5858
events = [LayoutEvent(STATIC_EVENT_HANDLER.target, [])] * 4
5959
expected_model = {
60-
"tagName": "div",
61-
"attributes": {"count": 4},
62-
"eventHandlers": {
63-
EVENT_NAME: {
64-
"target": STATIC_EVENT_HANDLER.target,
65-
"preventDefault": False,
66-
"stopPropagation": False,
60+
"tagName": "",
61+
"children": [
62+
{
63+
"tagName": "div",
64+
"attributes": {"count": 4},
65+
"eventHandlers": {
66+
EVENT_NAME: {
67+
"target": STATIC_EVENT_HANDLER.target,
68+
"preventDefault": False,
69+
"stopPropagation": False,
70+
}
71+
},
6772
}
68-
},
73+
],
6974
}
7075
return events, expected_model
7176

0 commit comments

Comments
 (0)