Skip to content

Commit b7e7292

Browse files
committed
fix 417 and 413
no more use of the id() function to generate unique IDs for component and event handlers within the layout. also includes a minor change to use a protocol (rather than inheritance) for the component base-class. isinstance() will not be a perfect check of type compatibility, but it should be close enough
1 parent ded5052 commit b7e7292

File tree

14 files changed

+89
-96
lines changed

14 files changed

+89
-96
lines changed

Diff for: docs/source/auto/api-reference.rst

-3
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,6 @@ API Reference
5858
Misc Modules
5959
------------
6060

61-
.. automodule:: idom.core.utils
62-
:members:
63-
6461
.. automodule:: idom.server.proto
6562
:members:
6663

Diff for: noxfile.py

-1
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ def test_suite(session: Session) -> None:
122122
session.log("Coverage won't be checked")
123123
session.install(".[all]")
124124
else:
125-
session.log("Coverage will be checked")
126125
posargs += ["--cov=src/idom", "--cov-report", "term"]
127126
install_idom_dev(session, extras="all")
128127

Diff for: src/idom/core/component.py

+17-15
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
import inspect
1010
from functools import wraps
1111
from typing import Any, Callable, Dict, Optional, Tuple, Union
12+
from uuid import uuid4
13+
14+
from typing_extensions import Protocol, runtime_checkable
1215

13-
from .utils import hex_id
1416
from .vdom import VdomDict
1517

1618

17-
ComponentConstructor = Callable[..., "AbstractComponent"]
18-
ComponentRenderFunction = Callable[..., Union["AbstractComponent", VdomDict]]
19+
ComponentConstructor = Callable[..., "ComponentType"]
20+
ComponentRenderFunction = Callable[..., Union["ComponentType", VdomDict]]
1921

2022

2123
def component(function: ComponentRenderFunction) -> Callable[..., "Component"]:
@@ -32,23 +34,22 @@ def constructor(*args: Any, key: Optional[Any] = None, **kwargs: Any) -> Compone
3234
return constructor
3335

3436

35-
class AbstractComponent(abc.ABC):
36-
37-
__slots__ = ["key"]
38-
if not hasattr(abc.ABC, "__weakref__"):
39-
__slots__.append("__weakref__") # pragma: no cover
37+
@runtime_checkable
38+
class ComponentType(Protocol):
39+
"""The expected interface for all component-like objects"""
4040

41+
id: str
4142
key: Optional[Any]
4243

4344
@abc.abstractmethod
4445
def render(self) -> VdomDict:
4546
"""Render the component's :class:`VdomDict`."""
4647

4748

48-
class Component(AbstractComponent):
49+
class Component:
4950
"""An object for rending component models."""
5051

51-
__slots__ = "_func", "_args", "_kwargs"
52+
__slots__ = "__weakref__", "_func", "_args", "_kwargs", "id", "key"
5253

5354
def __init__(
5455
self,
@@ -57,16 +58,17 @@ def __init__(
5758
args: Tuple[Any, ...],
5859
kwargs: Dict[str, Any],
5960
) -> None:
60-
self.key = key
61-
self._func = function
6261
self._args = args
62+
self._func = function
6363
self._kwargs = kwargs
64+
self.id = uuid4().hex
65+
self.key = key
6466
if key is not None:
6567
kwargs["key"] = key
6668

6769
def render(self) -> VdomDict:
6870
model = self._func(*self._args, **self._kwargs)
69-
if isinstance(model, AbstractComponent):
71+
if isinstance(model, ComponentType):
7072
model = {"tagName": "div", "children": [model]}
7173
return model
7274

@@ -79,6 +81,6 @@ def __repr__(self) -> str:
7981
else:
8082
items = ", ".join(f"{k}={v!r}" for k, v in args.items())
8183
if items:
82-
return f"{self._func.__name__}({hex_id(self)}, {items})"
84+
return f"{self._func.__name__}({self.id}, {items})"
8385
else:
84-
return f"{self._func.__name__}({hex_id(self)})"
86+
return f"{self._func.__name__}({self.id})"

Diff for: src/idom/core/events.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Optional,
1515
Union,
1616
)
17+
from uuid import uuid4
1718

1819
from anyio import create_task_group
1920

@@ -149,17 +150,19 @@ class EventHandler:
149150
"_func_handlers",
150151
"prevent_default",
151152
"stop_propagation",
153+
"target",
152154
)
153155

154156
def __init__(
155157
self,
156158
stop_propagation: bool = False,
157159
prevent_default: bool = False,
158160
) -> None:
159-
self.stop_propagation = stop_propagation
160-
self.prevent_default = prevent_default
161161
self._coro_handlers: List[Callable[..., Coroutine[Any, Any, Any]]] = []
162162
self._func_handlers: List[Callable[..., Any]] = []
163+
self.prevent_default = prevent_default
164+
self.stop_propagation = stop_propagation
165+
self.target = uuid4().hex
163166

164167
def add(self, function: Callable[..., Any]) -> "EventHandler":
165168
"""Add a callback function or coroutine to the event handler.
@@ -205,3 +208,8 @@ def __contains__(self, function: Any) -> bool:
205208
return function in self._coro_handlers
206209
else:
207210
return function in self._func_handlers
211+
212+
def __repr__(self) -> str:
213+
public_names = [name for name in self.__slots__ if not name.startswith("_")]
214+
items = ", ".join([f"{n}={getattr(self, n)!r}" for n in public_names])
215+
return f"{type(self).__name__}({items})"

Diff for: src/idom/core/hooks.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import idom
3232
from idom.utils import Ref
3333

34-
from .component import AbstractComponent
34+
from .component import ComponentType
3535

3636

3737
__all__ = [
@@ -393,7 +393,7 @@ class LifeCycleHook:
393393
def __init__(
394394
self,
395395
layout: idom.core.layout.Layout,
396-
component: AbstractComponent,
396+
component: ComponentType,
397397
) -> None:
398398
self.component = component
399399
self._layout = weakref.ref(layout)

Diff for: src/idom/core/layout.py

+27-28
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@
1717

1818
from idom.config import IDOM_DEBUG_MODE, IDOM_FEATURE_INDEX_AS_DEFAULT_KEY
1919

20-
from .component import AbstractComponent
20+
from .component import ComponentType
2121
from .events import EventHandler
2222
from .hooks import LifeCycleHook
23-
from .utils import hex_id
2423
from .vdom import validate_vdom
2524

2625

@@ -67,24 +66,24 @@ class Layout:
6766
if not hasattr(abc.ABC, "__weakref__"): # pragma: no cover
6867
__slots__.append("__weakref__")
6968

70-
def __init__(self, root: "AbstractComponent") -> None:
69+
def __init__(self, root: "ComponentType") -> None:
7170
super().__init__()
72-
if not isinstance(root, AbstractComponent):
73-
raise TypeError("Expected an AbstractComponent, not %r" % root)
71+
if not isinstance(root, ComponentType):
72+
raise TypeError("Expected an ComponentType, not %r" % root)
7473
self.root = root
7574

7675
def __enter__(self: _Self) -> _Self:
7776
# create attributes here to avoid access before entering context manager
7877
self._event_handlers: Dict[str, EventHandler] = {}
7978
self._rendering_queue = _ComponentQueue()
80-
self._model_state_by_component_id: Dict[int, _ModelState] = {
81-
id(self.root): _ModelState(None, -1, "", LifeCycleHook(self, self.root))
79+
self._model_state_by_component_id: Dict[str, _ModelState] = {
80+
self.root.id: _ModelState(None, -1, "", LifeCycleHook(self, self.root))
8281
}
8382
self._rendering_queue.put(self.root)
8483
return self
8584

8685
def __exit__(self, *exc: Any) -> None:
87-
root_state = self._model_state_by_component_id[id(self.root)]
86+
root_state = self._model_state_by_component_id[self.root.id]
8887
self._unmount_model_states([root_state])
8988

9089
# delete attributes here to avoid access after exiting context manager
@@ -94,7 +93,7 @@ def __exit__(self, *exc: Any) -> None:
9493

9594
return None
9695

97-
def update(self, component: "AbstractComponent") -> None:
96+
def update(self, component: "ComponentType") -> None:
9897
"""Schedule a re-render of a component in the layout"""
9998
self._rendering_queue.put(component)
10099
return None
@@ -121,7 +120,7 @@ async def render(self) -> LayoutUpdate:
121120
"""Await the next available render. This will block until a component is updated"""
122121
while True:
123122
component = await self._rendering_queue.get()
124-
if id(component) in self._model_state_by_component_id:
123+
if component.id in self._model_state_by_component_id:
125124
return self._create_layout_update(component)
126125
else:
127126
logger.info(
@@ -139,11 +138,11 @@ async def render(self) -> LayoutUpdate:
139138
async def render(self) -> LayoutUpdate:
140139
# Ensure that the model is valid VDOM on each render
141140
result = await self._debug_render()
142-
validate_vdom(self._model_state_by_component_id[id(self.root)].model)
141+
validate_vdom(self._model_state_by_component_id[self.root.id].model)
143142
return result
144143

145-
def _create_layout_update(self, component: AbstractComponent) -> LayoutUpdate:
146-
old_state = self._model_state_by_component_id[id(component)]
144+
def _create_layout_update(self, component: ComponentType) -> LayoutUpdate:
145+
old_state = self._model_state_by_component_id[component.id]
147146
new_state = old_state.new(None, component)
148147

149148
self._render_component(old_state, new_state, component)
@@ -160,7 +159,7 @@ def _render_component(
160159
self,
161160
old_state: Optional[_ModelState],
162161
new_state: _ModelState,
163-
component: AbstractComponent,
162+
component: ComponentType,
164163
) -> None:
165164
life_cycle_hook = new_state.life_cycle_hook
166165
life_cycle_hook.component_will_render()
@@ -176,8 +175,8 @@ def _render_component(
176175
new_state.model = {"tagName": "__error__", "children": [str(error)]}
177176

178177
if old_state is not None and old_state.component is not component:
179-
del self._model_state_by_component_id[id(old_state.component)]
180-
self._model_state_by_component_id[id(component)] = new_state
178+
del self._model_state_by_component_id[old_state.component.id]
179+
self._model_state_by_component_id[component.id] = new_state
181180

182181
try:
183182
parent = new_state.parent
@@ -249,7 +248,7 @@ def _render_model_attributes(
249248

250249
model_event_handlers = new_state.model["eventHandlers"] = {}
251250
for event, handler in handlers_by_event.items():
252-
target = old_state.targets_by_event.get(event, hex_id(handler))
251+
target = old_state.targets_by_event.get(event, handler.target)
253252
new_state.targets_by_event[event] = target
254253
self._event_handlers[target] = handler
255254
model_event_handlers[event] = {
@@ -270,7 +269,7 @@ def _render_model_event_handlers_without_old_state(
270269

271270
model_event_handlers = new_state.model["eventHandlers"] = {}
272271
for event, handler in handlers_by_event.items():
273-
target = hex_id(handler)
272+
target = handler.target
274273
new_state.targets_by_event[event] = target
275274
self._event_handlers[target] = handler
276275
model_event_handlers[event] = {
@@ -362,7 +361,7 @@ def _unmount_model_states(self, old_states: List[_ModelState]) -> None:
362361
if hasattr(state, "life_cycle_hook"):
363362
hook = state.life_cycle_hook
364363
hook.component_will_unmount()
365-
del self._model_state_by_component_id[id(hook.component)]
364+
del self._model_state_by_component_id[hook.component.id]
366365
to_unmount.extend(state.children_by_key.values())
367366

368367
def __repr__(self) -> str:
@@ -387,7 +386,7 @@ class _ModelState:
387386
model: _ModelVdom
388387
life_cycle_hook: LifeCycleHook
389388
patch_path: str
390-
component: AbstractComponent
389+
component: ComponentType
391390

392391
def __init__(
393392
self,
@@ -423,7 +422,7 @@ def parent(self) -> _ModelState:
423422
def new(
424423
self,
425424
new_parent: Optional[_ModelState],
426-
component: Optional[AbstractComponent],
425+
component: Optional[ComponentType],
427426
) -> _ModelState:
428427
if new_parent is None:
429428
new_parent = getattr(self, "parent", None)
@@ -452,19 +451,19 @@ class _ComponentQueue:
452451

453452
def __init__(self) -> None:
454453
self._loop = asyncio.get_event_loop()
455-
self._queue: "asyncio.Queue[AbstractComponent]" = asyncio.Queue()
456-
self._pending: Set[int] = set()
454+
self._queue: "asyncio.Queue[ComponentType]" = asyncio.Queue()
455+
self._pending: Set[str] = set()
457456

458-
def put(self, component: AbstractComponent) -> None:
459-
component_id = id(component)
457+
def put(self, component: ComponentType) -> None:
458+
component_id = component.id
460459
if component_id not in self._pending:
461460
self._pending.add(component_id)
462461
self._loop.call_soon_threadsafe(self._queue.put_nowait, component)
463462
return None
464463

465-
async def get(self) -> AbstractComponent:
464+
async def get(self) -> ComponentType:
466465
component = await self._queue.get()
467-
self._pending.remove(id(component))
466+
self._pending.remove(component.id)
468467
return component
469468

470469

@@ -475,7 +474,7 @@ def _process_child_type_and_key(
475474
if isinstance(child, dict):
476475
child_type = _DICT_TYPE
477476
key = child.get("key")
478-
elif isinstance(child, AbstractComponent):
477+
elif isinstance(child, ComponentType):
479478
child_type = _COMPONENT_TYPE
480479
key = getattr(child, "key", None)
481480
else:

Diff for: src/idom/core/utils.py

-5
This file was deleted.

Diff for: src/idom/core/vdom.py

+12-20
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,18 @@ def constructor(
198198
raise TypeError(f"{tag!r} nodes cannot have children.")
199199
return model
200200

201-
module = _get_module_name_by_frame_index(1)
202-
if module is None:
203-
qualname = None
204-
else:
205-
qualname = module + "." + tag
206-
201+
# replicate common function attributes
207202
constructor.__name__ = tag
208-
constructor.__module__ = module
209-
constructor.__qualname__ = qualname
210203
constructor.__doc__ = f"Return a new ``<{tag}/>`` :class:`VdomDict` element"
204+
205+
frame = inspect.currentframe()
206+
if frame is not None and frame.f_back is not None and frame.f_back is not None:
207+
module = frame.f_back.f_globals.get("__name__") # module in outer frame
208+
if module is not None:
209+
qualname = module + "." + tag
210+
constructor.__module__ = module
211+
constructor.__qualname__ = qualname
212+
211213
return constructor
212214

213215

@@ -260,7 +262,7 @@ def _is_single_child(value: Any) -> bool:
260262
if _debug_is_single_child(value):
261263
return True
262264

263-
from .component import AbstractComponent
265+
from .component import ComponentType
264266

265267
if hasattr(value, "__iter__") and not hasattr(value, "__len__"):
266268
logger.error(
@@ -269,19 +271,9 @@ def _is_single_child(value: Any) -> bool:
269271
)
270272
else:
271273
for child in value:
272-
if (isinstance(child, AbstractComponent) and child.key is None) or (
274+
if (isinstance(child, ComponentType) and child.key is None) or (
273275
isinstance(child, Mapping) and "key" not in child
274276
):
275277
logger.error(f"Key not specified for dynamic child {child}")
276278

277279
return False
278-
279-
280-
def _get_module_name_by_frame_index(index: int) -> Optional[str]:
281-
frame = inspect.currentframe()
282-
for i in range(index + 1): # add one to ignore this frame
283-
if frame is None:
284-
return None
285-
else:
286-
frame = frame.f_back
287-
return frame.f_globals.get("__name__")

0 commit comments

Comments
 (0)