|
1 | 1 | import logging |
2 | 2 | import re |
| 3 | +from functools import wraps |
3 | 4 | from types import TracebackType |
4 | 5 | from typing import ( |
5 | 6 | Any, |
|
12 | 13 | Type, |
13 | 14 | TypeVar, |
14 | 15 | Union, |
| 16 | + overload, |
15 | 17 | ) |
16 | 18 | from urllib.parse import urlencode, urlunparse |
| 19 | +from weakref import ref |
17 | 20 |
|
18 | 21 | from selenium.webdriver import Chrome |
19 | 22 | from selenium.webdriver.remote.webdriver import WebDriver |
| 23 | +from typing_extensions import Literal |
20 | 24 |
|
| 25 | +from idom.core.events import EventHandler |
| 26 | +from idom.core.hooks import LifeCycleHook, current_hook |
| 27 | +from idom.core.utils import hex_id |
21 | 28 | from idom.server.base import AbstractRenderServer |
22 | 29 | from idom.server.prefab import hotswap_server |
23 | 30 | from idom.server.utils import find_available_port, find_builtin_server_type |
@@ -167,3 +174,107 @@ def __init__(self) -> None: |
167 | 174 |
|
168 | 175 | def handle(self, record: logging.LogRecord) -> None: |
169 | 176 | self.records.append(record) |
| 177 | + |
| 178 | + |
| 179 | +class HookCatcher: |
| 180 | + """Utility for capturing a LifeCycleHook from a component |
| 181 | +
|
| 182 | + Example: |
| 183 | + .. code-block:: |
| 184 | +
|
| 185 | + hooks = HookCatcher(index_by_kwarg="key") |
| 186 | +
|
| 187 | + @idom.component |
| 188 | + @hooks.capture |
| 189 | + def MyComponent(key): |
| 190 | + ... |
| 191 | +
|
| 192 | + ... # render the component |
| 193 | +
|
| 194 | + # grab the last render of where MyComponent(key='some_key') |
| 195 | + hooks.index["some_key"] |
| 196 | + # or grab the hook from the component's last render |
| 197 | + hooks.latest |
| 198 | +
|
| 199 | + After the first render of ``MyComponent`` the ``HookCatcher`` will have |
| 200 | + captured the component's ``LifeCycleHook``. |
| 201 | + """ |
| 202 | + |
| 203 | + latest: LifeCycleHook |
| 204 | + |
| 205 | + def __init__(self, index_by_kwarg: Optional[str] = None): |
| 206 | + self.index_by_kwarg = index_by_kwarg |
| 207 | + self.index: Dict[Any, LifeCycleHook] = {} |
| 208 | + |
| 209 | + def capture(self, render_function: Callable[..., Any]) -> Callable[..., Any]: |
| 210 | + """Decorator for capturing a ``LifeCycleHook`` on each render of a component""" |
| 211 | + |
| 212 | + # The render function holds a reference to `self` and, via the `LifeCycleHook`, |
| 213 | + # the component. Some tests check whether components are garbage collected, thus |
| 214 | + # we must use a `ref` here to ensure these checks pass once the catcher itself |
| 215 | + # has been collected. |
| 216 | + self_ref = ref(self) |
| 217 | + |
| 218 | + @wraps(render_function) |
| 219 | + def wrapper(*args: Any, **kwargs: Any) -> Any: |
| 220 | + self = self_ref() |
| 221 | + assert self is not None, "Hook catcher has been garbage collected" |
| 222 | + |
| 223 | + hook = current_hook() |
| 224 | + if self.index_by_kwarg is not None: |
| 225 | + self.index[kwargs[self.index_by_kwarg]] = hook |
| 226 | + self.latest = hook |
| 227 | + return render_function(*args, **kwargs) |
| 228 | + |
| 229 | + return wrapper |
| 230 | + |
| 231 | + |
| 232 | +class StaticEventHandlers: |
| 233 | + """Utility for capturing the target of a static set of event handlers |
| 234 | +
|
| 235 | + Example: |
| 236 | + .. code-block:: |
| 237 | +
|
| 238 | + static_handlers = StaticEventHandlers("first", "second") |
| 239 | +
|
| 240 | + @idom.component |
| 241 | + def MyComponent(key): |
| 242 | + state, set_state = idom.hooks.use_state(0) |
| 243 | + handler = static_handlers.use(key, lambda event: set_state(state + 1)) |
| 244 | + return idom.html.button({"onClick": handler}, "Click me!") |
| 245 | +
|
| 246 | + # gives the target ID for onClick where MyComponent(key="first") |
| 247 | + first_target = static_handlers.targets["first"] |
| 248 | + """ |
| 249 | + |
| 250 | + def __init__(self, *index: Any) -> None: |
| 251 | + if not index: |
| 252 | + raise ValueError("Static set of index keys are required") |
| 253 | + self._handlers: Dict[Any, EventHandler] = {i: EventHandler() for i in index} |
| 254 | + self.targets: Dict[Any, str] = {i: hex_id(h) for i, h in self._handlers.items()} |
| 255 | + |
| 256 | + @overload |
| 257 | + def use( |
| 258 | + self, |
| 259 | + index: Any, |
| 260 | + function: Literal[None] = ..., |
| 261 | + ) -> Callable[[Callable[..., Any]], EventHandler]: |
| 262 | + ... |
| 263 | + |
| 264 | + @overload |
| 265 | + def use(self, index: Any, function: Callable[..., Any]) -> EventHandler: |
| 266 | + ... |
| 267 | + |
| 268 | + def use(self, index: Any, function: Optional[Callable[..., Any]] = None) -> Any: |
| 269 | + """Decorator for capturing an event handler function""" |
| 270 | + |
| 271 | + def setup(function: Callable[..., Any]) -> EventHandler: |
| 272 | + handler = self._handlers[index] |
| 273 | + handler.clear() |
| 274 | + handler.add(function) |
| 275 | + return handler |
| 276 | + |
| 277 | + if function is not None: |
| 278 | + return setup(function) |
| 279 | + else: |
| 280 | + return setup |
0 commit comments