Skip to content

Commit d177842

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

File tree

2 files changed

+130
-6
lines changed

2 files changed

+130
-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, set_state
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+
"set_state",
3232
"vdom",
3333
"web",
3434
]

src/idom/core/component.py

Lines changed: 128 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 set_state(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,93 @@ 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__") and "__dict__" not in cls.__slots__:
135+
missing_slots = list({"key", "state", "_set_state"}.difference(cls.__slots__))
136+
if missing_slots:
137+
raise ValueError("Component classes requires __slots__ for {missing_slots}")
138+
139+
old_init = getattr(cls, "__init__", object.__init__)
140+
old_render = cls.render
141+
142+
@wraps(old_init)
143+
def new_init(self: Any, *args: Any, key: Any | None = None, **kwargs: Any) -> None:
144+
self.key = key
145+
self.state = None
146+
old_init(self, *args, **kwargs)
147+
148+
@wraps(old_render)
149+
def new_render(self: Any) -> Any:
150+
self.state, self._set_state = use_state(self.state) # noqa: ROH101
151+
use_effect(getattr(self, "effect", None), args=[]) # noqa: ROH101
152+
return old_render(self)
153+
154+
# wrap the original methods
155+
cls.__init__ = _OwnerInheritorDescriptor(new_init, old_init)
156+
cls.render = _OwnerInheritorDescriptor(new_render, old_render)
157+
# manually set up descriptor
158+
cls.__init__.__set_name__(cls, "__init__")
159+
cls.render.__set_name__(cls, "render")
160+
161+
return cls
162+
163+
164+
class _OwnerInheritorDescriptor:
165+
"""Show one value for the owner of this descriptor and another for the owner's subclass
166+
167+
Example:
168+
.. code-block::
169+
170+
class Owner:
171+
method = _OwnerInheritorDescriptor(
172+
own_method=lambda self: 1,
173+
inherited_method=lambda self: 2,
174+
)
175+
176+
class Inheritor(Owner):
177+
def method(self):
178+
return super().method()
179+
180+
assert Owner().method() == 1
181+
assert Inheritor().method() == 2
182+
"""
183+
184+
owner: type[Any]
185+
186+
def __init__(
187+
self,
188+
own_method: Any,
189+
inherited_method: Any,
190+
) -> None:
191+
self.own_method = own_method
192+
self.inherited_method = inherited_method
193+
194+
def __set_name__(self, cls: type[Any], name: str) -> None:
195+
self.owner = cls
196+
197+
def __get__(self, obj: Any | None, cls: type[Any]) -> Any:
198+
if obj is None:
199+
return self
200+
elif cls is self.owner:
201+
return self.own_method.__get__(obj, cls)
202+
else:
203+
return self.inherited_method.__get__(obj, cls)

0 commit comments

Comments
 (0)