Skip to content

Commit

Permalink
Deprecate hotswap (#876)
Browse files Browse the repository at this point in the history
* use non-editable install

* deprecate hotswap
  • Loading branch information
rmorshea authored Jan 12, 2023
1 parent b0ea49a commit d1a69f6
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 164 deletions.
7 changes: 5 additions & 2 deletions docs/source/about/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@ more info, see the :ref:`Contributor Guide <Creating a Changelog Entry>`.
Unreleased
----------

No changes.
**Deprecated**

- :pull:`876` - ``idom.widgets.hotswap``. The function has no clear uses outside of some
internal applications. For this reason it has been deprecated.


v0.43.0
-------
:octicon:`milestone` *released on 2023-01-09*

**Removed**
**Deprecated**

- :pull:`870` - ``ComponentType.should_render()``. This method was implemented based on
reading the React/Preact source code. As it turns out though it seems like it's mostly
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ max-complexity = 18
select = ["B", "C", "E", "F", "W", "T4", "B9", "N", "ROH"]
exclude = ["**/node_modules/*", ".eggs/*", ".tox/*"]
# -- flake8-tidy-imports --
ban-relative-imports = "parents"
ban-relative-imports = "true"
4 changes: 2 additions & 2 deletions scripts/one_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import idom
from docs.examples import all_example_names, get_example_files_by_name, load_one_example
from idom.widgets import hotswap
from idom.widgets import _hotswap


EXAMPLE_NAME_SET = all_example_names()
Expand All @@ -32,7 +32,7 @@ def watch_for_change():
def main():
ex_name = _example_name_input()

mount, component = hotswap(update_on_change=True)
mount, component = _hotswap()

def update_component():
print(f"Loading example: {ex_name!r}")
Expand Down
25 changes: 12 additions & 13 deletions src/idom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from . import backend, config, html, logging, sample, svg, types, web
from .backend.hooks import use_connection, use_location, use_scope
from .backend.utils import run
from .core import hooks
from .core.component import component
from .core.events import event
from .core.hooks import (
from idom import backend, config, html, logging, sample, svg, types, web, widgets
from idom.backend.hooks import use_connection, use_location, use_scope
from idom.backend.utils import run
from idom.core import hooks
from idom.core.component import component
from idom.core.events import event
from idom.core.hooks import (
create_context,
use_callback,
use_context,
Expand All @@ -15,11 +15,10 @@
use_ref,
use_state,
)
from .core.layout import Layout
from .core.serve import Stop
from .core.vdom import vdom
from .utils import Ref, html_to_vdom, vdom_to_html
from .widgets import hotswap
from idom.core.layout import Layout
from idom.core.serve import Stop
from idom.core.vdom import vdom
from idom.utils import Ref, html_to_vdom, vdom_to_html


__author__ = "idom-team"
Expand All @@ -32,7 +31,6 @@
"create_context",
"event",
"hooks",
"hotswap",
"html_to_vdom",
"html",
"Layout",
Expand All @@ -57,4 +55,5 @@
"vdom_to_html",
"vdom",
"web",
"widgets",
]
86 changes: 83 additions & 3 deletions src/idom/testing/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
import logging
from contextlib import AsyncExitStack
from types import TracebackType
from typing import Any, Optional, Tuple, Type, Union
from typing import Any, Callable, Optional, Tuple, Type, Union
from urllib.parse import urlencode, urlunparse

from idom.backend import default as default_server
from idom.backend.types import BackendImplementation
from idom.backend.utils import find_available_port
from idom.widgets import hotswap
from idom.core.component import component
from idom.core.hooks import use_callback, use_effect, use_state
from idom.core.types import ComponentConstructor
from idom.utils import Ref

from .logs import LogAssertionError, capture_idom_logs, list_logged_exceptions

Expand Down Expand Up @@ -41,7 +44,7 @@ def __init__(
) -> None:
self.host = host
self.port = port or find_available_port(host, allow_reuse_waiting_ports=False)
self.mount, self._root_component = hotswap()
self.mount, self._root_component = _hotswap()

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

return None


_MountFunc = Callable[["Callable[[], Any] | None"], None]


def _hotswap(update_on_change: bool = False) -> Tuple[_MountFunc, ComponentConstructor]:
"""Swap out components from a layout on the fly.
Since you can't change the component functions used to create a layout
in an imperative manner, you can use ``hotswap`` to do this so
long as you set things up ahead of time.
Parameters:
update_on_change: Whether or not all views of the layout should be udpated on a swap.
Example:
.. code-block:: python
import idom
show, root = idom.hotswap()
PerClientStateServer(root).run_in_thread("localhost", 8765)
@idom.component
def DivOne(self):
return {"tagName": "div", "children": [1]}
show(DivOne)
# displaying the output now will show DivOne
@idom.component
def DivTwo(self):
return {"tagName": "div", "children": [2]}
show(DivTwo)
# displaying the output now will show DivTwo
"""
constructor_ref: Ref[Callable[[], Any]] = Ref(lambda: None)

if update_on_change:
set_constructor_callbacks: set[Callable[[Callable[[], Any]], None]] = set()

@component
def HotSwap() -> Any:
# new displays will adopt the latest constructor and arguments
constructor, _set_constructor = use_state(lambda: constructor_ref.current)
set_constructor = use_callback(lambda new: _set_constructor(lambda _: new))

def add_callback() -> Callable[[], None]:
set_constructor_callbacks.add(set_constructor)
return lambda: set_constructor_callbacks.remove(set_constructor)

use_effect(add_callback)

return constructor()

def swap(constructor: Callable[[], Any] | None) -> None:
constructor = constructor_ref.current = constructor or (lambda: None)

for set_constructor in set_constructor_callbacks:
set_constructor(constructor)

return None

else:

@component
def HotSwap() -> Any:
return constructor_ref.current()

def swap(constructor: Callable[[], Any] | None) -> None:
constructor_ref.current = constructor or (lambda: None)
return None

return swap, HotSwap
109 changes: 12 additions & 97 deletions src/idom/widgets.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,16 @@
from __future__ import annotations

from base64 import b64encode
from typing import (
Any,
Callable,
Dict,
List,
Optional,
Sequence,
Set,
Tuple,
TypeVar,
Union,
)
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, TypeVar, Union
from warnings import warn

from typing_extensions import Protocol

import idom

from . import html
from .core import hooks
from .core.component import component
from .core.types import ComponentConstructor, VdomDict
from .utils import Ref
from .testing.backend import _hotswap, _MountFunc


def image(
Expand Down Expand Up @@ -107,85 +95,12 @@ def __call__(self, value: str) -> _CastTo:
...


MountFunc = Callable[["Callable[[], Any] | None"], None]


def hotswap(update_on_change: bool = False) -> Tuple[MountFunc, ComponentConstructor]:
"""Swap out components from a layout on the fly.
Since you can't change the component functions used to create a layout
in an imperative manner, you can use ``hotswap`` to do this so
long as you set things up ahead of time.
Parameters:
update_on_change: Whether or not all views of the layout should be udpated on a swap.
Example:
.. code-block:: python
import idom
show, root = idom.hotswap()
PerClientStateServer(root).run_in_thread("localhost", 8765)
@idom.component
def DivOne(self):
return {"tagName": "div", "children": [1]}
show(DivOne)
# displaying the output now will show DivOne
@idom.component
def DivTwo(self):
return {"tagName": "div", "children": [2]}
show(DivTwo)
# displaying the output now will show DivTwo
"""
constructor_ref: Ref[Callable[[], Any]] = Ref(lambda: None)

if update_on_change:
set_constructor_callbacks: Set[Callable[[Callable[[], Any]], None]] = set()

@component
def HotSwap() -> Any:
# new displays will adopt the latest constructor and arguments
constructor, set_constructor = _use_callable(constructor_ref.current)

def add_callback() -> Callable[[], None]:
set_constructor_callbacks.add(set_constructor)
return lambda: set_constructor_callbacks.remove(set_constructor)

hooks.use_effect(add_callback)

return constructor()

def swap(constructor: Callable[[], Any] | None) -> None:
constructor = constructor_ref.current = constructor or (lambda: None)

for set_constructor in set_constructor_callbacks:
set_constructor(constructor)

return None

else:

@component
def HotSwap() -> Any:
return constructor_ref.current()

def swap(constructor: Callable[[], Any] | None) -> None:
constructor_ref.current = constructor or (lambda: None)
return None

return swap, HotSwap


_Func = Callable[..., Any]


def _use_callable(initial_func: _Func) -> Tuple[_Func, Callable[[_Func], None]]:
state, set_state = hooks.use_state(lambda: initial_func)
return state, lambda new: set_state(lambda old: new)
def hotswap(
update_on_change: bool = False,
) -> Tuple[_MountFunc, ComponentConstructor]: # pragma: no cover
warn(
"The 'hotswap' function is deprecated and will be removed in a future release",
DeprecationWarning,
stacklevel=2,
)
return _hotswap(update_on_change)
49 changes: 48 additions & 1 deletion tests/test_testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@

import pytest

from idom import testing
from idom import Ref, component, html, testing
from idom.backend import starlette as starlette_implementation
from idom.logging import ROOT_LOGGER
from idom.sample import SampleApp as SampleApp
from idom.testing.backend import _hotswap
from idom.testing.display import DisplayFixture


def test_assert_idom_logged_does_not_supress_errors():
Expand Down Expand Up @@ -162,3 +164,48 @@ def test_list_logged_excptions():

logged_errors = testing.logs.list_logged_exceptions(records)
assert logged_errors == [the_error]


async def test_hostwap_update_on_change(display: DisplayFixture):
"""Ensure shared hotswapping works
This basically means that previously rendered views of a hotswap component get updated
when a new view is mounted, not just the next time it is re-displayed
In this test we construct a scenario where clicking a button will cause a pre-existing
hotswap component to be updated
"""

def make_next_count_constructor(count):
"""We need to construct a new function so they're different when we set_state"""

def constructor():
count.current += 1
return html.div({"id": f"hotswap-{count.current}"}, count.current)

return constructor

@component
def ButtonSwapsDivs():
count = Ref(0)

async def on_click(event):
mount(make_next_count_constructor(count))

incr = html.button({"onClick": on_click, "id": "incr-button"}, "incr")

mount, make_hostswap = _hotswap(update_on_change=True)
mount(make_next_count_constructor(count))
hotswap_view = make_hostswap()

return html.div(incr, hotswap_view)

await display.show(ButtonSwapsDivs)

client_incr_button = await display.page.wait_for_selector("#incr-button")

await display.page.wait_for_selector("#hotswap-1")
await client_incr_button.click()
await display.page.wait_for_selector("#hotswap-2")
await client_incr_button.click()
await display.page.wait_for_selector("#hotswap-3")
Loading

0 comments on commit d1a69f6

Please sign in to comment.