Skip to content

Commit dc54bed

Browse files
committed
add class based component implementation
1 parent a0c3740 commit dc54bed

File tree

3 files changed

+180
-6
lines changed

3 files changed

+180
-6
lines changed

src/idom/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from . import config, html, log, web
22
from .core import hooks
3-
from .core.component import Component, component
3+
from .core.component import component, update
44
from .core.dispatcher import Stop
55
from .core.events import EventHandler, event
66
from .core.layout import Layout
@@ -15,7 +15,6 @@
1515

1616
__all__ = [
1717
"component",
18-
"Component",
1918
"config",
2019
"event",
2120
"EventHandler",
@@ -29,6 +28,7 @@
2928
"Ref",
3029
"run",
3130
"Stop",
31+
"update",
3232
"vdom",
3333
"web",
3434
]

src/idom/core/component.py

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,53 @@
88
import inspect
99
import warnings
1010
from functools import wraps
11-
from typing import Any, Callable, Dict, Optional, Tuple, Union
11+
from typing import Any, Callable, Dict, Optional, Tuple, Type, TypeVar, Union, overload
1212

13+
from .hooks import use_effect, use_state
1314
from .proto import ComponentType, VdomDict
1415

1516

16-
def component(
17-
function: Callable[..., Union[ComponentType, VdomDict]]
18-
) -> Callable[..., "Component"]:
17+
_Class = TypeVar("_Class", bound=Type[ComponentType])
18+
19+
20+
@overload
21+
def component(function_or_class: _Class) -> _Class:
22+
...
23+
24+
25+
@overload
26+
def component(function_or_class: Callable[..., Any]) -> Callable[..., ComponentType]:
27+
...
28+
29+
30+
def component(function_or_class: Any) -> Callable[..., ComponentType]:
1931
"""A decorator for defining an :class:`Component`.
2032
2133
Parameters:
2234
function: The function that will render a :class:`VdomDict`.
2335
"""
36+
if not inspect.isclass(function_or_class):
37+
return _wrap_function(function_or_class)
38+
else:
39+
return _wrap_class(function_or_class)
40+
41+
42+
def update(cmpt: Any, state: Any) -> None:
43+
"""A function for re-rendering a class-based component with updated state"""
44+
try:
45+
set_state = cmpt._set_state
46+
except AttributeError:
47+
if isinstance(cmpt, ComponentType):
48+
raise RuntimeError("Cannot update a component that has not rendered yet")
49+
else:
50+
raise TypeError(f"{cmpt} is not a component class")
51+
else:
52+
set_state(state)
53+
54+
55+
def _wrap_function(
56+
function: Callable[..., ComponentType | VdomDict]
57+
) -> Callable[..., Component]:
2458
sig = inspect.signature(function)
2559
key_is_kwarg = "key" in sig.parameters and sig.parameters["key"].kind in (
2660
inspect.Parameter.KEYWORD_ONLY,
@@ -77,3 +111,91 @@ def __repr__(self) -> str:
77111
return f"{self._func.__name__}({id(self)}, {items})"
78112
else:
79113
return f"{self._func.__name__}({id(self)})"
114+
115+
116+
_Wrapped = TypeVar("_Wrapped", bound=Any)
117+
118+
119+
def _wrap_class(cls: type[_Wrapped]) -> type[_Wrapped]:
120+
"""Modifies the given class such that it can operate as a stateful component
121+
122+
Adds the following attributes to the class:
123+
124+
- ``key``
125+
- ``state``
126+
- ``_set_state``
127+
128+
And wraps the following methods with extra logic that is opaque to the user:
129+
130+
- ``__init__``
131+
- ``render``
132+
"""
133+
134+
if hasattr(cls, "__slots__"):
135+
raise ValueError("Component classes cannot have __slots__")
136+
137+
old_init = getattr(cls, "__init__", object.__init__)
138+
old_render = cls.render
139+
140+
@wraps(old_init)
141+
def new_init(self: Any, *args: Any, key: Any | None = None, **kwargs: Any) -> None:
142+
self.key = key
143+
self.state = None
144+
old_init(self, *args, **kwargs)
145+
146+
@wraps(old_render)
147+
def new_render(self: Any) -> Any:
148+
self.state, self._set_state = use_state(self.state) # noqa: ROH101
149+
use_effect(getattr(self, "effect", None), args=[]) # noqa: ROH101
150+
return old_render(self)
151+
152+
# wrap the original methods
153+
cls.__init__ = _OwnerInheritorDescriptor(new_init, old_init)
154+
cls.render = _OwnerInheritorDescriptor(new_render, old_render)
155+
# manually set up descriptor
156+
cls.__init__.__set_name__(cls, "__init__")
157+
cls.render.__set_name__(cls, "render")
158+
159+
return cls
160+
161+
162+
class _OwnerInheritorDescriptor:
163+
"""Show one value for the owner of this descriptor and another for the owner's subclass
164+
165+
Example:
166+
.. code-block::
167+
168+
class Owner:
169+
method = _OwnerInheritorDescriptor(
170+
own_method=lambda self: 1,
171+
inherited_method=lambda self: 2,
172+
)
173+
174+
class Inheritor(Owner):
175+
def method(self):
176+
return super().method()
177+
178+
assert Owner().method() == 1
179+
assert Inheritor().method() == 2
180+
"""
181+
182+
owner: type[Any]
183+
184+
def __init__(
185+
self,
186+
own_method: Any,
187+
inherited_method: Any,
188+
) -> None:
189+
self.own_method = own_method
190+
self.inherited_method = inherited_method
191+
192+
def __set_name__(self, cls: type[Any], name: str) -> None:
193+
self.owner = cls
194+
195+
def __get__(self, obj: Any | None, cls: type[Any]) -> Any:
196+
if obj is None:
197+
return self
198+
elif cls is self.owner:
199+
return self.own_method.__get__(obj, cls)
200+
else:
201+
return self.inherited_method.__get__(obj, cls)

temp.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from typing import NamedTuple
2+
3+
import idom
4+
5+
6+
class GateState(NamedTuple):
7+
left: bool
8+
right: bool
9+
10+
11+
@idom.component
12+
class AndGate:
13+
def __init__(self):
14+
self.state = GateState(False, False)
15+
16+
def render(self):
17+
left, right = self.state
18+
return idom.html.div(
19+
idom.html.input({"type": "checkbox", "onClick": self.toggle_left}),
20+
idom.html.input({"type": "checkbox", "onClick": self.toggle_right}),
21+
idom.html.pre(f"{left} AND {right} = {left and right}"),
22+
)
23+
24+
def toggle_left(self, event):
25+
left, right = self.state
26+
idom.update(self, GateState(not left, right))
27+
28+
def toggle_right(self, event):
29+
left, right = self.state
30+
idom.update(self, GateState(left, not right))
31+
32+
33+
@idom.component
34+
class Counter:
35+
def __init__(self):
36+
self.state = 0
37+
38+
def render(self):
39+
return idom.html.div(
40+
f"Count: {self.state}",
41+
idom.html.button({"onClick": self.increment}, "+"),
42+
idom.html.button({"onClick": self.decrement}, "-"),
43+
)
44+
45+
def increment(self, event):
46+
idom.update(self, self.state + 1)
47+
48+
def decrement(self, event):
49+
idom.update(self, self.state + 1)
50+
51+
52+
idom.run(AndGate, port=8000)

0 commit comments

Comments
 (0)