Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a mock State class that can be instantiated and queried while testing #2101

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,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 @@ -2260,7 +2260,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 @@ -2777,7 +2779,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
201 changes: 108 additions & 93 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 abstractmethod
from contextlib import nullcontext
from operator import attrgetter
from pathlib import Path
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)
10 changes: 10 additions & 0 deletions taipy/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.
10 changes: 10 additions & 0 deletions taipy/mock/gui/__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.
51 changes: 51 additions & 0 deletions taipy/mock/gui/mock_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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 ...gui import Gui, State
from ...gui.utils import _MapDict


class MockState(State):
__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
Loading
Loading