Skip to content

Commit 6568f86

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

File tree

3 files changed

+176
-8
lines changed

3 files changed

+176
-8
lines changed

src/idom/__init__.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1+
from dataclasses import replace
2+
13
from . import config, html, log, web
24
from .core import hooks
3-
from .core.component import Component, component
5+
from .core.component import component
46
from .core.dispatcher import Stop
57
from .core.events import EventHandler, event
68
from .core.layout import Layout
79
from .core.vdom import vdom
810
from .server.prefab import run
9-
from .utils import Ref, html_to_vdom
11+
from .utils import Ref, html_to_vdom, state
1012
from .widgets import hotswap, multiview
1113

1214

@@ -15,7 +17,6 @@
1517

1618
__all__ = [
1719
"component",
18-
"Component",
1920
"config",
2021
"event",
2122
"EventHandler",
@@ -27,7 +28,9 @@
2728
"log",
2829
"multiview",
2930
"Ref",
31+
"replace",
3032
"run",
33+
"state",
3134
"Stop",
3235
"vdom",
3336
"web",

src/idom/core/component.py

Lines changed: 132 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,40 @@
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 _wrap_function(
43+
function: Callable[..., ComponentType | VdomDict]
44+
) -> Callable[..., Component]:
2445
sig = inspect.signature(function)
2546
key_is_kwarg = "key" in sig.parameters and sig.parameters["key"].kind in (
2647
inspect.Parameter.KEYWORD_ONLY,
@@ -77,3 +98,110 @@ def __repr__(self) -> str:
7798
return f"{self._func.__name__}({id(self)}, {items})"
7899
else:
79100
return f"{self._func.__name__}({id(self)})"
101+
102+
103+
_Wrapped = TypeVar("_Wrapped", bound=Any)
104+
105+
106+
def _wrap_class(cls: type[_Wrapped]) -> type[_Wrapped]:
107+
"""Modifies the given class such that it can operate as a stateful component
108+
109+
Adds the following attributes to the class:
110+
111+
- ``key``
112+
- ``state``
113+
- ``_set_state``
114+
115+
And wraps the following methods with extra logic that is opaque to the user:
116+
117+
- ``__init__``
118+
- ``render``
119+
"""
120+
121+
if hasattr(cls, "__slots__"):
122+
raise ValueError("Component classes cannot have __slots__")
123+
124+
original_render = cls.render
125+
original_init = getattr(cls, "__init__", object.__init__)
126+
127+
def __init__( # noqa: N807
128+
self: Any,
129+
*args: Any,
130+
key: Optional[Any] = None,
131+
**kwargs: Any,
132+
) -> None:
133+
self.key = key
134+
# initialize with a no-op set state callback
135+
self._set_state = lambda _: None
136+
cls.__init__.inherited_method(self)
137+
if not hasattr(self, "state"):
138+
raise AttributeError(f"{self} did not initialize a 'state' attribute")
139+
140+
def render(self: Any) -> Any:
141+
self.state, self._set_state = use_state(self.state) # noqa: ROH101
142+
use_effect(getattr(self, "effect", None), args=[]) # noqa: ROH101
143+
return cls.render.inherited_method(self)
144+
145+
cls.__init__ = _OwnerInheritorDescriptor(__init__, original_init)
146+
cls.render = _OwnerInheritorDescriptor(render, original_render)
147+
148+
# need to manually set up descriptor
149+
cls.__init__.__set_name__(cls, "__init__")
150+
cls.render.__set_name__(cls, "render")
151+
152+
cls.state = _StateDescriptor()
153+
154+
return cls
155+
156+
157+
class _OwnerInheritorDescriptor:
158+
"""Show one value for the owner of this descriptor and another for the owner's subclass
159+
160+
Example:
161+
.. code-block::
162+
163+
class Owner:
164+
method = _OwnerInheritorDescriptor(
165+
own_method=lambda self: 1,
166+
inherited_method=lambda self: 2,
167+
)
168+
169+
class Inheritor(Owner):
170+
def method(self):
171+
return super().method()
172+
173+
assert Owner().method() == 1
174+
assert Inheritor().method() == 2
175+
"""
176+
177+
owner: type[Any]
178+
179+
def __init__(
180+
self,
181+
own_method: Any,
182+
inherited_method: Any,
183+
) -> None:
184+
self.own_method = own_method
185+
self.inherited_method = inherited_method
186+
187+
def __set_name__(self, cls: type[Any], name: str) -> None:
188+
self.owner = cls
189+
190+
def __get__(self, obj: Any | None, cls: type[Any]) -> Any:
191+
if obj is None:
192+
return self
193+
elif cls is self.owner:
194+
return self.own_method.__get__(obj, cls)
195+
else:
196+
return self.inherited_method.__get__(obj, cls)
197+
198+
199+
class _StateDescriptor:
200+
"""Simple descriptor that call ``obj._set_state`` of the value changes"""
201+
202+
def __get__(self, obj: Any, cls: Any) -> Any:
203+
return self if obj is None else obj._state
204+
205+
def __set__(self, obj: Any, new: Any) -> None:
206+
obj._set_state(new)
207+
obj._state = new

src/idom/utils.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,45 @@
33
=================
44
"""
55

6+
from dataclasses import dataclass
67
from html.parser import HTMLParser as _HTMLParser
7-
from typing import Any, Callable, Dict, Generic, List, Optional, Tuple, TypeVar
8+
from typing import (
9+
Any,
10+
Callable,
11+
Dict,
12+
Generic,
13+
List,
14+
Optional,
15+
Tuple,
16+
Type,
17+
TypeVar,
18+
overload,
19+
)
20+
21+
22+
_Class = TypeVar("_Class", bound=Type[Any])
23+
24+
25+
@overload
26+
def state(cls: _Class) -> _Class:
27+
...
28+
29+
30+
@overload
31+
def state(cls: None) -> Callable[[_Class], _Class]:
32+
...
33+
34+
35+
@overload
36+
def state(**kwargs: Any) -> Callable[[_Class], _Class]:
37+
...
38+
39+
40+
def state(cls: Optional[_Class] = None, **kwargs: Any) -> Any:
41+
if cls is not None:
42+
return dataclass(frozen=True, **kwargs)(cls)
43+
else:
44+
return dataclass(frozen=True, **kwargs)
845

946

1047
_RefValue = TypeVar("_RefValue")

0 commit comments

Comments
 (0)