Skip to content

Commit

Permalink
Adding a mock State class that can be instantiated and queried while …
Browse files Browse the repository at this point in the history
…testing (#2101)

* Adding a testing State class that can be instantiated and queried during tests
I hoped it would not have any impact ont the real code but it has. Any discussion is welcome before merge.
resolves #2098

* tricky to get gui :-)

* consistency

* linter

* test => mock
more tests

* following Long comments

* Fab's right

Co-authored-by: Fabien Lelaquais <[email protected]>

---------

Co-authored-by: Fred Lefévère-Laoide <[email protected]>
Co-authored-by: Fabien Lelaquais <[email protected]>
  • Loading branch information
3 people authored Oct 25, 2024
1 parent 5e58fd1 commit 691af71
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 99 deletions.
10 changes: 7 additions & 3 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
from .page import Page
from .partial import Partial
from .server import _Server
from .state import State
from .state import State, _GuiState
from .types import _WsType
from .utils import (
_delscopeattr,
Expand Down Expand Up @@ -2292,7 +2292,9 @@ def _hold_actions(
if isinstance(callback, str)
else _get_lambda_id(t.cast(LambdaType, callback))
if _is_unnamed_function(callback)
else callback.__name__ if callback is not None else None
else callback.__name__
if callback is not None
else None
)
func = self.__get_on_cancel_block_ui(action_name)
def_action_name = func.__name__
Expand Down Expand Up @@ -2809,7 +2811,9 @@ def run(
self.__var_dir.set_default(self.__frame)

if self.__state is None or is_reloading:
self.__state = State(self, self.__locals_context.get_all_keys(), self.__locals_context.get_all_context())
self.__state = _GuiState(
self, self.__locals_context.get_all_keys(), self.__locals_context.get_all_context()
)

if _is_in_notebook():
# Allow gui.state.x in notebook mode
Expand Down
10 changes: 10 additions & 0 deletions taipy/gui/mock/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Copyright 2021-2024 Avaiga Private Limited
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
62 changes: 62 additions & 0 deletions taipy/gui/mock/mock_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright 2021-2024 Avaiga Private Limited
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
# specific language governing permissions and limitations under the License.
import typing as t

from .. import Gui, State
from ..utils import _MapDict


class MockState(State):
"""A Mock implementation for `State`.
TODO
example of use:
```py
def test_callback():
ms = MockState(Gui(""), a = 1)
on_action(ms) # function to test
assert ms.a == 2
```
"""

__VARS = "vars"

def __init__(self, gui: Gui, **kwargs) -> None:
super().__setattr__(MockState.__VARS, {k: _MapDict(v) if isinstance(v, dict) else v for k, v in kwargs.items()})
self._gui = gui
super().__init__()

def get_gui(self) -> Gui:
return self._gui

def __getattribute__(self, name: str) -> t.Any:
if attr := t.cast(dict, super().__getattribute__(MockState.__VARS)).get(name):
return attr
try:
return super().__getattribute__(name)
except Exception:
return None

def __setattr__(self, name: str, value: t.Any) -> None:
t.cast(dict, super().__getattribute__(MockState.__VARS))[name] = (
_MapDict(value) if isinstance(value, dict) else value
)

def __getitem__(self, key: str):
return self

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
return True

def broadcast(self, name: str, value: t.Any):
pass
203 changes: 109 additions & 94 deletions taipy/gui/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import inspect
import typing as t
from abc import ABC, abstractmethod
from contextlib import nullcontext
from operator import attrgetter
from pathlib import Path
Expand All @@ -25,7 +26,7 @@
from .gui import Gui


class State:
class State(ABC):
"""Accessor to the bound variables from callbacks.
`State` is used when you need to access the value of variables
Expand Down Expand Up @@ -73,6 +74,87 @@ def change_variable(state):
```
"""

def __init__(self) -> None:
self._gui: "Gui"

@abstractmethod
def get_gui(self) -> "Gui":
"""Return the Gui instance for this state object.
Returns:
Gui: The Gui instance for this state object.
"""
raise NotImplementedError

def assign(self, name: str, value: t.Any) -> t.Any:
"""Assign a value to a state variable.
This should be used only from within a lambda function used
as a callback in a visual element.
Arguments:
name (str): The variable name to assign to.
value (Any): The new variable value.
Returns:
Any: The previous value of the variable.
"""
val = attrgetter(name)(self)
_attrsetter(self, name, value)
return val

def refresh(self, name: str):
"""Refresh a state variable.
This allows to re-sync the user interface with a variable value.
Arguments:
name (str): The variable name to refresh.
"""
val = attrgetter(name)(self)
_attrsetter(self, name, val)

def _set_context(self, gui: "Gui") -> t.ContextManager[None]:
return nullcontext()

def broadcast(self, name: str, value: t.Any):
"""Update a variable on all clients.
All connected clients will receive an update of the variable called *name* with the
provided value, even if it is not shared.
Arguments:
name (str): The variable name to update.
value (Any): The new variable value.
"""
with self._set_context(self._gui):
encoded_name = self._gui._bind_var(name)
self._gui._broadcast_all_clients(encoded_name, value)

def __enter__(self):
self._gui.__enter__()
return self

def __exit__(self, exc_type, exc_value, traceback):
return self._gui.__exit__(exc_type, exc_value, traceback)

def set_favicon(self, favicon_path: t.Union[str, Path]):
"""Change the favicon for the client of this state.
This function dynamically changes the favicon (the icon associated with the application's
pages) of Taipy GUI pages for the specific client of this state.
Note that the *favicon* parameter to `(Gui.)run()^` can also be used to change
the favicon when the application starts.
Arguments:
favicon_path: The path to the image file to use.<br/>
This can be expressed as a path name or a URL (relative or not).
"""
self._gui.set_favicon(favicon_path, self)


class _GuiState(State):
__gui_attr = "_gui"
__attrs = (
__gui_attr,
Expand Down Expand Up @@ -100,68 +182,66 @@ def change_variable(state):
__excluded_attrs = __attrs + __methods + __placeholder_attrs

def __init__(self, gui: "Gui", var_list: t.Iterable[str], context_list: t.Iterable[str]) -> None:
super().__setattr__(State.__attrs[1], list(State.__filter_var_list(var_list, State.__excluded_attrs)))
super().__setattr__(State.__attrs[2], list(context_list))
super().__setattr__(State.__attrs[0], gui)

def get_gui(self) -> "Gui":
"""Return the Gui instance for this state object.
Returns:
Gui: The Gui instance for this state object.
"""
return super().__getattribute__(State.__gui_attr)
super().__setattr__(
_GuiState.__attrs[1], list(_GuiState.__filter_var_list(var_list, _GuiState.__excluded_attrs))
)
super().__setattr__(_GuiState.__attrs[2], list(context_list))
super().__setattr__(_GuiState.__attrs[0], gui)
super().__init__()

@staticmethod
def __filter_var_list(var_list: t.Iterable[str], excluded_attrs: t.Iterable[str]) -> t.Iterable[str]:
return filter(lambda n: n not in excluded_attrs, var_list)

def get_gui(self) -> "Gui":
return super().__getattribute__(_GuiState.__gui_attr)

def __getattribute__(self, name: str) -> t.Any:
if name == "__class__":
return State
if name in State.__methods:
return _GuiState
if name in _GuiState.__methods:
return super().__getattribute__(name)
gui: "Gui" = self.get_gui()
if name == State.__gui_attr:
if name == _GuiState.__gui_attr:
return gui
if name in State.__excluded_attrs:
if name in _GuiState.__excluded_attrs:
raise AttributeError(f"Variable '{name}' is protected and is not accessible.")
if gui._is_in_brdcst_callback() and (
name not in gui._get_shared_variables() and not gui._bindings()._is_single_client()
):
raise AttributeError(f"Variable '{name}' is not available to be accessed in shared callback.")
if not name.startswith("__") and name not in super().__getattribute__(State.__attrs[1]):
if not name.startswith("__") and name not in super().__getattribute__(_GuiState.__attrs[1]):
raise AttributeError(f"Variable '{name}' is not defined.")
with self._notebook_context(gui), self._set_context(gui):
encoded_name = gui._bind_var(name)
return getattr(gui._bindings(), encoded_name)

def __setattr__(self, name: str, value: t.Any) -> None:
gui: "Gui" = super().__getattribute__(State.__gui_attr)
gui: "Gui" = super().__getattribute__(_GuiState.__gui_attr)
if gui._is_in_brdcst_callback() and (
name not in gui._get_shared_variables() and not gui._bindings()._is_single_client()
):
raise AttributeError(f"Variable '{name}' is not available to be accessed in shared callback.")
if not name.startswith("__") and name not in super().__getattribute__(State.__attrs[1]):
if not name.startswith("__") and name not in super().__getattribute__(_GuiState.__attrs[1]):
raise AttributeError(f"Variable '{name}' is not accessible.")
with self._notebook_context(gui), self._set_context(gui):
encoded_name = gui._bind_var(name)
setattr(gui._bindings(), encoded_name, value)

def __getitem__(self, key: str):
context = key if key in super().__getattribute__(State.__attrs[2]) else None
context = key if key in super().__getattribute__(_GuiState.__attrs[2]) else None
if context is None:
gui: "Gui" = super().__getattribute__(State.__gui_attr)
gui: "Gui" = super().__getattribute__(_GuiState.__gui_attr)
page_ctx = gui._get_page_context(key)
context = page_ctx if page_ctx is not None else None
if context is None:
raise RuntimeError(f"Can't resolve context '{key}' from state object")
self._set_placeholder(State.__placeholder_attrs[1], context)
self._set_placeholder(_GuiState.__placeholder_attrs[1], context)
return self

def _set_context(self, gui: "Gui") -> t.ContextManager[None]:
if (pl_ctx := self._get_placeholder(State.__placeholder_attrs[1])) is not None:
self._set_placeholder(State.__placeholder_attrs[1], None)
if (pl_ctx := self._get_placeholder(_GuiState.__placeholder_attrs[1])) is not None:
self._set_placeholder(_GuiState.__placeholder_attrs[1], None)
if pl_ctx != gui._get_locals_context():
return gui._set_locals_context(pl_ctx)
if len(inspect.stack()) > 1:
Expand All @@ -176,89 +256,24 @@ def _notebook_context(self, gui: "Gui"):
return gui.get_flask_app().app_context() if not has_app_context() and _is_in_notebook() else nullcontext()

def _get_placeholder(self, name: str):
if name in State.__placeholder_attrs:
if name in _GuiState.__placeholder_attrs:
try:
return super().__getattribute__(name)
except AttributeError:
return None
return None

def _set_placeholder(self, name: str, value: t.Any):
if name in State.__placeholder_attrs:
if name in _GuiState.__placeholder_attrs:
super().__setattr__(name, value)

def _get_placeholder_attrs(self):
return State.__placeholder_attrs
return _GuiState.__placeholder_attrs

def _add_attribute(self, name: str, default_value: t.Optional[t.Any] = None) -> bool:
attrs: t.List[str] = super().__getattribute__(State.__attrs[1])
attrs: t.List[str] = super().__getattribute__(_GuiState.__attrs[1])
if name not in attrs:
attrs.append(name)
gui = super().__getattribute__(State.__gui_attr)
gui = super().__getattribute__(_GuiState.__gui_attr)
return gui._bind_var_val(name, default_value)
return False

def assign(self, name: str, value: t.Any) -> t.Any:
"""Assign a value to a state variable.
This should be used only from within a lambda function used
as a callback in a visual element.
Arguments:
name (str): The variable name to assign to.
value (Any): The new variable value.
Returns:
Any: The previous value of the variable.
"""
val = attrgetter(name)(self)
_attrsetter(self, name, value)
return val

def refresh(self, name: str):
"""Refresh a state variable.
This allows to re-sync the user interface with a variable value.
Arguments:
name (str): The variable name to refresh.
"""
val = attrgetter(name)(self)
_attrsetter(self, name, val)

def broadcast(self, name: str, value: t.Any):
"""Update a variable on all clients.
All connected clients will receive an update of the variable called *name* with the
provided value, even if it is not shared.
Arguments:
name (str): The variable name to update.
value (Any): The new variable value.
"""
gui: "Gui" = super().__getattribute__(State.__gui_attr)
with self._set_context(gui):
encoded_name = gui._bind_var(name)
gui._broadcast_all_clients(encoded_name, value)

def __enter__(self):
super().__getattribute__(State.__attrs[0]).__enter__()
return self

def __exit__(self, exc_type, exc_value, traceback):
return super().__getattribute__(State.__attrs[0]).__exit__(exc_type, exc_value, traceback)

def set_favicon(self, favicon_path: t.Union[str, Path]):
"""Change the favicon for the client of this state.
This function dynamically changes the favicon (the icon associated with the application's
pages) of Taipy GUI pages for the specific client of this state.
Note that the *favicon* parameter to `(Gui.)run()^` can also be used to change
the favicon when the application starts.
Arguments:
favicon_path: The path to the image file to use.<br/>
This can be expressed as a path name or a URL (relative or not).
"""
super().__getattribute__(State.__gui_attr).set_favicon(favicon_path, self)
Loading

0 comments on commit 691af71

Please sign in to comment.