123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- # Copyright 2021-2025 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 inspect
- import typing as t
- from abc import ABCMeta, abstractmethod
- from contextlib import nullcontext
- from operator import attrgetter
- from pathlib import Path
- from types import FrameType, SimpleNamespace
- from flask import has_app_context
- from .utils import _get_module_name_from_frame, _is_in_notebook
- from .utils._attributes import _attrsetter
- if t.TYPE_CHECKING:
- from .gui import Gui
- class State(SimpleNamespace, metaclass=ABCMeta):
- """Accessor to the bound variables from callbacks.
- `State` is used when you need to access the value of variables
- bound to visual elements (see [Binding](../../../../../userman/gui/binding.md)).<br/>
- Because each browser connected to the application server may represent and
- modify any variable at any moment as the result of user interaction, each
- connection holds its own set of variables along with their values. We call
- the set of these the application variables the application _state_, as seen
- by a given client.
- Each callback (see [Callbacks](../../../../../userman/gui/callbacks.md)) receives a specific
- instance of the `State` class, where you can find all the variables bound to
- visual elements in your application.
- Note that `State` also is a Python Context Manager: In situations where you
- have several variables to update, it is more clear and more efficient to assign
- the variable values in a `with` construct:
- ```py
- def my_callback(state, ...):
- ...
- with state as s:
- s.var1 = value1
- s.var2 = value2
- ...
- ```
- You cannot set a variable in the context of a lambda function because Python
- prevents any use of the assignment operator.<br/>
- You can, however, use the `assign()` method on the state that the lambda function
- receives, so you can work around this limitation:
- Here is how you could define a button that changes the value of a variable
- directly in a page expressed using Markdown:
- ```
- <|Set variable|button|on_action={lambda s: s.assign("var_name", new_value)}|>
- ```
- This would be strictly similar to the Markdown line:
- ```
- <|Set variable|button|on_action=change_variable|>
- ```
- with the Python code:
- ```py
- def change_variable(state):
- state.var_name = new_value
- ```
- """
- 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.
- """
- ...
- 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)
- @abstractmethod
- 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.
- """
- ...
- 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)
- @abstractmethod
- def __getitem__(self, key: str) -> "State":
- ...
- class _GuiState(State):
- __gui_attr = "_gui"
- __attrs = (
- __gui_attr,
- "_user_var_list",
- "_context_list",
- )
- __methods = (
- "assign",
- "broadcast",
- "get_gui",
- "refresh",
- "set_favicon",
- "_set_context",
- "_notebook_context",
- "_get_placeholder",
- "_set_placeholder",
- "_get_gui_attr",
- "_get_placeholder_attrs",
- "_add_attribute",
- )
- __placeholder_attrs = ("_taipy_p1", "_current_context", "__state_id")
- __excluded_attrs = __attrs + __methods + __placeholder_attrs
- def __init__(self, gui: "Gui", var_list: t.Iterable[str], context_list: t.Iterable[str]) -> None:
- 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 broadcast(self, name: str, value: t.Any):
- with self._set_context(self._gui):
- encoded_name = self._gui._bind_var(name)
- self._gui._broadcast_all_clients(encoded_name, value)
- def __getattribute__(self, name: str) -> t.Any:
- if name == "__class__":
- return _GuiState
- if name in _GuiState.__methods:
- return super().__getattribute__(name)
- gui: "Gui" = self.get_gui()
- if name == _GuiState.__gui_attr:
- return gui
- 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__(_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__(_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__(_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__(_GuiState.__attrs[2]) else None
- if context is None:
- 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(_GuiState.__placeholder_attrs[1], context)
- return self
- def _set_context(self, gui: "Gui") -> t.ContextManager[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:
- ctx = _get_module_name_from_frame(t.cast(FrameType, t.cast(FrameType, inspect.stack()[2].frame)))
- current_context = gui._get_locals_context()
- # ignore context if the current one starts with the new one (to resolve for class modules)
- if ctx != current_context and not current_context.startswith(str(ctx)):
- return gui._set_locals_context(ctx)
- return nullcontext()
- 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 _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 _GuiState.__placeholder_attrs:
- super().__setattr__(name, value)
- def _get_placeholder_attrs(self):
- return _GuiState.__placeholder_attrs
- def _add_attribute(self, name: str, default_value: t.Optional[t.Any] = None) -> bool:
- attrs: t.List[str] = super().__getattribute__(_GuiState.__attrs[1])
- if name not in attrs:
- attrs.append(name)
- gui = super().__getattribute__(_GuiState.__gui_attr)
- return gui._bind_var_val(name, default_value)
- return False
- class _AsyncState(_GuiState):
- def __init__(self, state: State) -> None:
- super().__init__(state.get_gui(), [], [])
- self._set_placeholder("__state_id", state.get_gui()._get_client_id())
- @staticmethod
- def __set_var_in_state(state: State, var_name: str, value: t.Any):
- setattr(state, var_name, value)
- @staticmethod
- def __get_var_from_state(state: State, var_name: str):
- return getattr(state, var_name)
- def __setattr__(self, var_name: str, var_value: t.Any) -> None:
- self.get_gui().invoke_callback(
- t.cast(str, self._get_placeholder("__state_id")), _AsyncState.__set_var_in_state, [var_name, var_value]
- )
- def __getattr__(self, var_name: str) -> t.Any:
- return self.get_gui().invoke_callback(
- t.cast(str, self._get_placeholder("__state_id")), _AsyncState.__get_var_from_state, [var_name]
- )
|