From d1a69f676be43da15c9d2c7c419f55e4acb10ef5 Mon Sep 17 00:00:00 2001 From: Ryan Morshead Date: Thu, 12 Jan 2023 00:20:16 -0800 Subject: [PATCH] Deprecate hotswap (#876) * use non-editable install * deprecate hotswap --- docs/source/about/changelog.rst | 7 +- pyproject.toml | 2 +- scripts/one_example.py | 4 +- src/idom/__init__.py | 25 ++++---- src/idom/testing/backend.py | 86 ++++++++++++++++++++++++- src/idom/widgets.py | 109 ++++---------------------------- tests/test_testing.py | 49 +++++++++++++- tests/test_widgets.py | 45 ------------- 8 files changed, 163 insertions(+), 164 deletions(-) diff --git a/docs/source/about/changelog.rst b/docs/source/about/changelog.rst index a187e0922..f793a8fda 100644 --- a/docs/source/about/changelog.rst +++ b/docs/source/about/changelog.rst @@ -23,14 +23,17 @@ more info, see the :ref:`Contributor Guide `. 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 diff --git a/pyproject.toml b/pyproject.toml index 32f0fb5dd..cc0199bcd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/scripts/one_example.py b/scripts/one_example.py index 745265e7a..706dd5182 100644 --- a/scripts/one_example.py +++ b/scripts/one_example.py @@ -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() @@ -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}") diff --git a/src/idom/__init__.py b/src/idom/__init__.py index 6bd7a1070..f1023a6ca 100644 --- a/src/idom/__init__.py +++ b/src/idom/__init__.py @@ -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, @@ -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" @@ -32,7 +31,6 @@ "create_context", "event", "hooks", - "hotswap", "html_to_vdom", "html", "Layout", @@ -57,4 +55,5 @@ "vdom_to_html", "vdom", "web", + "widgets", ] diff --git a/src/idom/testing/backend.py b/src/idom/testing/backend.py index 3376f8439..58b116b56 100644 --- a/src/idom/testing/backend.py +++ b/src/idom/testing/backend.py @@ -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 @@ -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: @@ -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 diff --git a/src/idom/widgets.py b/src/idom/widgets.py index d71192923..6e114aff2 100644 --- a/src/idom/widgets.py +++ b/src/idom/widgets.py @@ -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( @@ -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) diff --git a/tests/test_testing.py b/tests/test_testing.py index 902648536..27afa980a 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -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(): @@ -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") diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 2df28c656..cd6f9b2c2 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -9,51 +9,6 @@ HERE = Path(__file__).parent -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 idom.html.div({"id": f"hotswap-{count.current}"}, count.current) - - return constructor - - @idom.component - def ButtonSwapsDivs(): - count = idom.Ref(0) - - async def on_click(event): - mount(make_next_count_constructor(count)) - - incr = idom.html.button({"onClick": on_click, "id": "incr-button"}, "incr") - - mount, make_hostswap = idom.widgets.hotswap(update_on_change=True) - mount(make_next_count_constructor(count)) - hotswap_view = make_hostswap() - - return idom.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") - - IMAGE_SRC_BYTES = b"""