Skip to content

Commit d1a69f6

Browse files
authored
Deprecate hotswap (#876)
* use non-editable install * deprecate hotswap
1 parent b0ea49a commit d1a69f6

File tree

8 files changed

+163
-164
lines changed

8 files changed

+163
-164
lines changed

docs/source/about/changelog.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,17 @@ more info, see the :ref:`Contributor Guide <Creating a Changelog Entry>`.
2323
Unreleased
2424
----------
2525

26-
No changes.
26+
**Deprecated**
27+
28+
- :pull:`876` - ``idom.widgets.hotswap``. The function has no clear uses outside of some
29+
internal applications. For this reason it has been deprecated.
2730

2831

2932
v0.43.0
3033
-------
3134
:octicon:`milestone` *released on 2023-01-09*
3235

33-
**Removed**
36+
**Deprecated**
3437

3538
- :pull:`870` - ``ComponentType.should_render()``. This method was implemented based on
3639
reading the React/Preact source code. As it turns out though it seems like it's mostly

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,4 @@ max-complexity = 18
5252
select = ["B", "C", "E", "F", "W", "T4", "B9", "N", "ROH"]
5353
exclude = ["**/node_modules/*", ".eggs/*", ".tox/*"]
5454
# -- flake8-tidy-imports --
55-
ban-relative-imports = "parents"
55+
ban-relative-imports = "true"

scripts/one_example.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import idom
77
from docs.examples import all_example_names, get_example_files_by_name, load_one_example
8-
from idom.widgets import hotswap
8+
from idom.widgets import _hotswap
99

1010

1111
EXAMPLE_NAME_SET = all_example_names()
@@ -32,7 +32,7 @@ def watch_for_change():
3232
def main():
3333
ex_name = _example_name_input()
3434

35-
mount, component = hotswap(update_on_change=True)
35+
mount, component = _hotswap()
3636

3737
def update_component():
3838
print(f"Loading example: {ex_name!r}")

src/idom/__init__.py

+12-13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
from . import backend, config, html, logging, sample, svg, types, web
2-
from .backend.hooks import use_connection, use_location, use_scope
3-
from .backend.utils import run
4-
from .core import hooks
5-
from .core.component import component
6-
from .core.events import event
7-
from .core.hooks import (
1+
from idom import backend, config, html, logging, sample, svg, types, web, widgets
2+
from idom.backend.hooks import use_connection, use_location, use_scope
3+
from idom.backend.utils import run
4+
from idom.core import hooks
5+
from idom.core.component import component
6+
from idom.core.events import event
7+
from idom.core.hooks import (
88
create_context,
99
use_callback,
1010
use_context,
@@ -15,11 +15,10 @@
1515
use_ref,
1616
use_state,
1717
)
18-
from .core.layout import Layout
19-
from .core.serve import Stop
20-
from .core.vdom import vdom
21-
from .utils import Ref, html_to_vdom, vdom_to_html
22-
from .widgets import hotswap
18+
from idom.core.layout import Layout
19+
from idom.core.serve import Stop
20+
from idom.core.vdom import vdom
21+
from idom.utils import Ref, html_to_vdom, vdom_to_html
2322

2423

2524
__author__ = "idom-team"
@@ -32,7 +31,6 @@
3231
"create_context",
3332
"event",
3433
"hooks",
35-
"hotswap",
3634
"html_to_vdom",
3735
"html",
3836
"Layout",
@@ -57,4 +55,5 @@
5755
"vdom_to_html",
5856
"vdom",
5957
"web",
58+
"widgets",
6059
]

src/idom/testing/backend.py

+83-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
import logging
55
from contextlib import AsyncExitStack
66
from types import TracebackType
7-
from typing import Any, Optional, Tuple, Type, Union
7+
from typing import Any, Callable, Optional, Tuple, Type, Union
88
from urllib.parse import urlencode, urlunparse
99

1010
from idom.backend import default as default_server
1111
from idom.backend.types import BackendImplementation
1212
from idom.backend.utils import find_available_port
13-
from idom.widgets import hotswap
13+
from idom.core.component import component
14+
from idom.core.hooks import use_callback, use_effect, use_state
15+
from idom.core.types import ComponentConstructor
16+
from idom.utils import Ref
1417

1518
from .logs import LogAssertionError, capture_idom_logs, list_logged_exceptions
1619

@@ -41,7 +44,7 @@ def __init__(
4144
) -> None:
4245
self.host = host
4346
self.port = port or find_available_port(host, allow_reuse_waiting_ports=False)
44-
self.mount, self._root_component = hotswap()
47+
self.mount, self._root_component = _hotswap()
4548

4649
if app is not None:
4750
if implementation is None:
@@ -146,3 +149,80 @@ async def __aexit__(
146149
raise LogAssertionError("Unexpected logged exception") from logged_errors[0]
147150

148151
return None
152+
153+
154+
_MountFunc = Callable[["Callable[[], Any] | None"], None]
155+
156+
157+
def _hotswap(update_on_change: bool = False) -> Tuple[_MountFunc, ComponentConstructor]:
158+
"""Swap out components from a layout on the fly.
159+
160+
Since you can't change the component functions used to create a layout
161+
in an imperative manner, you can use ``hotswap`` to do this so
162+
long as you set things up ahead of time.
163+
164+
Parameters:
165+
update_on_change: Whether or not all views of the layout should be udpated on a swap.
166+
167+
Example:
168+
.. code-block:: python
169+
170+
import idom
171+
172+
show, root = idom.hotswap()
173+
PerClientStateServer(root).run_in_thread("localhost", 8765)
174+
175+
@idom.component
176+
def DivOne(self):
177+
return {"tagName": "div", "children": [1]}
178+
179+
show(DivOne)
180+
181+
# displaying the output now will show DivOne
182+
183+
@idom.component
184+
def DivTwo(self):
185+
return {"tagName": "div", "children": [2]}
186+
187+
show(DivTwo)
188+
189+
# displaying the output now will show DivTwo
190+
"""
191+
constructor_ref: Ref[Callable[[], Any]] = Ref(lambda: None)
192+
193+
if update_on_change:
194+
set_constructor_callbacks: set[Callable[[Callable[[], Any]], None]] = set()
195+
196+
@component
197+
def HotSwap() -> Any:
198+
# new displays will adopt the latest constructor and arguments
199+
constructor, _set_constructor = use_state(lambda: constructor_ref.current)
200+
set_constructor = use_callback(lambda new: _set_constructor(lambda _: new))
201+
202+
def add_callback() -> Callable[[], None]:
203+
set_constructor_callbacks.add(set_constructor)
204+
return lambda: set_constructor_callbacks.remove(set_constructor)
205+
206+
use_effect(add_callback)
207+
208+
return constructor()
209+
210+
def swap(constructor: Callable[[], Any] | None) -> None:
211+
constructor = constructor_ref.current = constructor or (lambda: None)
212+
213+
for set_constructor in set_constructor_callbacks:
214+
set_constructor(constructor)
215+
216+
return None
217+
218+
else:
219+
220+
@component
221+
def HotSwap() -> Any:
222+
return constructor_ref.current()
223+
224+
def swap(constructor: Callable[[], Any] | None) -> None:
225+
constructor_ref.current = constructor or (lambda: None)
226+
return None
227+
228+
return swap, HotSwap

src/idom/widgets.py

+12-97
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,16 @@
11
from __future__ import annotations
22

33
from base64 import b64encode
4-
from typing import (
5-
Any,
6-
Callable,
7-
Dict,
8-
List,
9-
Optional,
10-
Sequence,
11-
Set,
12-
Tuple,
13-
TypeVar,
14-
Union,
15-
)
4+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
5+
from warnings import warn
166

177
from typing_extensions import Protocol
188

199
import idom
2010

2111
from . import html
22-
from .core import hooks
23-
from .core.component import component
2412
from .core.types import ComponentConstructor, VdomDict
25-
from .utils import Ref
13+
from .testing.backend import _hotswap, _MountFunc
2614

2715

2816
def image(
@@ -107,85 +95,12 @@ def __call__(self, value: str) -> _CastTo:
10795
...
10896

10997

110-
MountFunc = Callable[["Callable[[], Any] | None"], None]
111-
112-
113-
def hotswap(update_on_change: bool = False) -> Tuple[MountFunc, ComponentConstructor]:
114-
"""Swap out components from a layout on the fly.
115-
116-
Since you can't change the component functions used to create a layout
117-
in an imperative manner, you can use ``hotswap`` to do this so
118-
long as you set things up ahead of time.
119-
120-
Parameters:
121-
update_on_change: Whether or not all views of the layout should be udpated on a swap.
122-
123-
Example:
124-
.. code-block:: python
125-
126-
import idom
127-
128-
show, root = idom.hotswap()
129-
PerClientStateServer(root).run_in_thread("localhost", 8765)
130-
131-
@idom.component
132-
def DivOne(self):
133-
return {"tagName": "div", "children": [1]}
134-
135-
show(DivOne)
136-
137-
# displaying the output now will show DivOne
138-
139-
@idom.component
140-
def DivTwo(self):
141-
return {"tagName": "div", "children": [2]}
142-
143-
show(DivTwo)
144-
145-
# displaying the output now will show DivTwo
146-
"""
147-
constructor_ref: Ref[Callable[[], Any]] = Ref(lambda: None)
148-
149-
if update_on_change:
150-
set_constructor_callbacks: Set[Callable[[Callable[[], Any]], None]] = set()
151-
152-
@component
153-
def HotSwap() -> Any:
154-
# new displays will adopt the latest constructor and arguments
155-
constructor, set_constructor = _use_callable(constructor_ref.current)
156-
157-
def add_callback() -> Callable[[], None]:
158-
set_constructor_callbacks.add(set_constructor)
159-
return lambda: set_constructor_callbacks.remove(set_constructor)
160-
161-
hooks.use_effect(add_callback)
162-
163-
return constructor()
164-
165-
def swap(constructor: Callable[[], Any] | None) -> None:
166-
constructor = constructor_ref.current = constructor or (lambda: None)
167-
168-
for set_constructor in set_constructor_callbacks:
169-
set_constructor(constructor)
170-
171-
return None
172-
173-
else:
174-
175-
@component
176-
def HotSwap() -> Any:
177-
return constructor_ref.current()
178-
179-
def swap(constructor: Callable[[], Any] | None) -> None:
180-
constructor_ref.current = constructor or (lambda: None)
181-
return None
182-
183-
return swap, HotSwap
184-
185-
186-
_Func = Callable[..., Any]
187-
188-
189-
def _use_callable(initial_func: _Func) -> Tuple[_Func, Callable[[_Func], None]]:
190-
state, set_state = hooks.use_state(lambda: initial_func)
191-
return state, lambda new: set_state(lambda old: new)
98+
def hotswap(
99+
update_on_change: bool = False,
100+
) -> Tuple[_MountFunc, ComponentConstructor]: # pragma: no cover
101+
warn(
102+
"The 'hotswap' function is deprecated and will be removed in a future release",
103+
DeprecationWarning,
104+
stacklevel=2,
105+
)
106+
return _hotswap(update_on_change)

tests/test_testing.py

+48-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33

44
import pytest
55

6-
from idom import testing
6+
from idom import Ref, component, html, testing
77
from idom.backend import starlette as starlette_implementation
88
from idom.logging import ROOT_LOGGER
99
from idom.sample import SampleApp as SampleApp
10+
from idom.testing.backend import _hotswap
11+
from idom.testing.display import DisplayFixture
1012

1113

1214
def test_assert_idom_logged_does_not_supress_errors():
@@ -162,3 +164,48 @@ def test_list_logged_excptions():
162164

163165
logged_errors = testing.logs.list_logged_exceptions(records)
164166
assert logged_errors == [the_error]
167+
168+
169+
async def test_hostwap_update_on_change(display: DisplayFixture):
170+
"""Ensure shared hotswapping works
171+
172+
This basically means that previously rendered views of a hotswap component get updated
173+
when a new view is mounted, not just the next time it is re-displayed
174+
175+
In this test we construct a scenario where clicking a button will cause a pre-existing
176+
hotswap component to be updated
177+
"""
178+
179+
def make_next_count_constructor(count):
180+
"""We need to construct a new function so they're different when we set_state"""
181+
182+
def constructor():
183+
count.current += 1
184+
return html.div({"id": f"hotswap-{count.current}"}, count.current)
185+
186+
return constructor
187+
188+
@component
189+
def ButtonSwapsDivs():
190+
count = Ref(0)
191+
192+
async def on_click(event):
193+
mount(make_next_count_constructor(count))
194+
195+
incr = html.button({"onClick": on_click, "id": "incr-button"}, "incr")
196+
197+
mount, make_hostswap = _hotswap(update_on_change=True)
198+
mount(make_next_count_constructor(count))
199+
hotswap_view = make_hostswap()
200+
201+
return html.div(incr, hotswap_view)
202+
203+
await display.show(ButtonSwapsDivs)
204+
205+
client_incr_button = await display.page.wait_for_selector("#incr-button")
206+
207+
await display.page.wait_for_selector("#hotswap-1")
208+
await client_incr_button.click()
209+
await display.page.wait_for_selector("#hotswap-2")
210+
await client_incr_button.click()
211+
await display.page.wait_for_selector("#hotswap-3")

0 commit comments

Comments
 (0)