state.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. # Copyright 2021-2025 Avaiga Private Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  4. # the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  9. # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  10. # specific language governing permissions and limitations under the License.
  11. import inspect
  12. import typing as t
  13. from abc import ABCMeta, abstractmethod
  14. from contextlib import nullcontext
  15. from operator import attrgetter
  16. from pathlib import Path
  17. from types import FrameType, SimpleNamespace
  18. from flask import has_app_context
  19. from .utils import _get_module_name_from_frame, _is_in_notebook
  20. from .utils._attributes import _attrsetter
  21. if t.TYPE_CHECKING:
  22. from .gui import Gui
  23. class State(SimpleNamespace, metaclass=ABCMeta):
  24. """Accessor to the bound variables from callbacks.
  25. `State` is used when you need to access the value of variables
  26. bound to visual elements (see [Binding](../../../../../userman/gui/binding.md)).<br/>
  27. Because each browser connected to the application server may represent and
  28. modify any variable at any moment as the result of user interaction, each
  29. connection holds its own set of variables along with their values. We call
  30. the set of these the application variables the application _state_, as seen
  31. by a given client.
  32. Each callback (see [Callbacks](../../../../../userman/gui/callbacks.md)) receives a specific
  33. instance of the `State` class, where you can find all the variables bound to
  34. visual elements in your application.
  35. Note that `State` also is a Python Context Manager: In situations where you
  36. have several variables to update, it is more clear and more efficient to assign
  37. the variable values in a `with` construct:
  38. ```py
  39. def my_callback(state, ...):
  40. ...
  41. with state as s:
  42. s.var1 = value1
  43. s.var2 = value2
  44. ...
  45. ```
  46. You cannot set a variable in the context of a lambda function because Python
  47. prevents any use of the assignment operator.<br/>
  48. You can, however, use the `assign()` method on the state that the lambda function
  49. receives, so you can work around this limitation:
  50. Here is how you could define a button that changes the value of a variable
  51. directly in a page expressed using Markdown:
  52. ```
  53. <|Set variable|button|on_action={lambda s: s.assign("var_name", new_value)}|>
  54. ```
  55. This would be strictly similar to the Markdown line:
  56. ```
  57. <|Set variable|button|on_action=change_variable|>
  58. ```
  59. with the Python code:
  60. ```py
  61. def change_variable(state):
  62. state.var_name = new_value
  63. ```
  64. """
  65. def __init__(self) -> None:
  66. self._gui: "Gui"
  67. @abstractmethod
  68. def get_gui(self) -> "Gui":
  69. """Return the Gui instance for this state object.
  70. Returns:
  71. Gui: The Gui instance for this state object.
  72. """
  73. ...
  74. def assign(self, name: str, value: t.Any) -> t.Any:
  75. """Assign a value to a state variable.
  76. This should be used only from within a lambda function used
  77. as a callback in a visual element.
  78. Arguments:
  79. name (str): The variable name to assign to.
  80. value (Any): The new variable value.
  81. Returns:
  82. Any: The previous value of the variable.
  83. """
  84. val = attrgetter(name)(self)
  85. _attrsetter(self, name, value)
  86. return val
  87. def refresh(self, name: str):
  88. """Refresh a state variable.
  89. This allows to re-sync the user interface with a variable value.
  90. Arguments:
  91. name (str): The variable name to refresh.
  92. """
  93. val = attrgetter(name)(self)
  94. _attrsetter(self, name, val)
  95. @abstractmethod
  96. def broadcast(self, name: str, value: t.Any):
  97. """Update a variable on all clients.
  98. All connected clients will receive an update of the variable called *name* with the
  99. provided value, even if it is not shared.
  100. Arguments:
  101. name (str): The variable name to update.
  102. value (Any): The new variable value.
  103. """
  104. ...
  105. def __enter__(self):
  106. self._gui.__enter__()
  107. return self
  108. def __exit__(self, exc_type, exc_value, traceback):
  109. return self._gui.__exit__(exc_type, exc_value, traceback)
  110. def set_favicon(self, favicon_path: t.Union[str, Path]):
  111. """Change the favicon for the client of this state.
  112. This function dynamically changes the favicon (the icon associated with the application's
  113. pages) of Taipy GUI pages for the specific client of this state.
  114. Note that the *favicon* parameter to `(Gui.)run()^` can also be used to change
  115. the favicon when the application starts.
  116. Arguments:
  117. favicon_path: The path to the image file to use.<br/>
  118. This can be expressed as a path name or a URL (relative or not).
  119. """
  120. self._gui.set_favicon(favicon_path, self)
  121. @abstractmethod
  122. def __getitem__(self, key: str) -> "State":
  123. ...
  124. class _GuiState(State):
  125. __gui_attr = "_gui"
  126. __attrs = (
  127. __gui_attr,
  128. "_user_var_list",
  129. "_context_list",
  130. )
  131. __methods = (
  132. "assign",
  133. "broadcast",
  134. "get_gui",
  135. "refresh",
  136. "set_favicon",
  137. "_set_context",
  138. "_notebook_context",
  139. "_get_placeholder",
  140. "_set_placeholder",
  141. "_get_gui_attr",
  142. "_get_placeholder_attrs",
  143. "_add_attribute",
  144. )
  145. __placeholder_attrs = ("_taipy_p1", "_current_context", "__state_id")
  146. __excluded_attrs = __attrs + __methods + __placeholder_attrs
  147. def __init__(self, gui: "Gui", var_list: t.Iterable[str], context_list: t.Iterable[str]) -> None:
  148. super().__setattr__(
  149. _GuiState.__attrs[1], list(_GuiState.__filter_var_list(var_list, _GuiState.__excluded_attrs))
  150. )
  151. super().__setattr__(_GuiState.__attrs[2], list(context_list))
  152. super().__setattr__(_GuiState.__attrs[0], gui)
  153. super().__init__()
  154. @staticmethod
  155. def __filter_var_list(var_list: t.Iterable[str], excluded_attrs: t.Iterable[str]) -> t.Iterable[str]:
  156. return filter(lambda n: n not in excluded_attrs, var_list)
  157. def get_gui(self) -> "Gui":
  158. return super().__getattribute__(_GuiState.__gui_attr)
  159. def broadcast(self, name: str, value: t.Any):
  160. with self._set_context(self._gui):
  161. encoded_name = self._gui._bind_var(name)
  162. self._gui._broadcast_all_clients(encoded_name, value)
  163. def __getattribute__(self, name: str) -> t.Any:
  164. if name == "__class__":
  165. return _GuiState
  166. if name in _GuiState.__methods:
  167. return super().__getattribute__(name)
  168. gui: "Gui" = self.get_gui()
  169. if name == _GuiState.__gui_attr:
  170. return gui
  171. if name in _GuiState.__excluded_attrs:
  172. raise AttributeError(f"Variable '{name}' is protected and is not accessible.")
  173. if gui._is_in_brdcst_callback() and (
  174. name not in gui._get_shared_variables() and not gui._bindings()._is_single_client()
  175. ):
  176. raise AttributeError(f"Variable '{name}' is not available to be accessed in shared callback.")
  177. if not name.startswith("__") and name not in super().__getattribute__(_GuiState.__attrs[1]):
  178. raise AttributeError(f"Variable '{name}' is not defined.")
  179. with self._notebook_context(gui), self._set_context(gui):
  180. encoded_name = gui._bind_var(name)
  181. return getattr(gui._bindings(), encoded_name)
  182. def __setattr__(self, name: str, value: t.Any) -> None:
  183. gui: "Gui" = super().__getattribute__(_GuiState.__gui_attr)
  184. if gui._is_in_brdcst_callback() and (
  185. name not in gui._get_shared_variables() and not gui._bindings()._is_single_client()
  186. ):
  187. raise AttributeError(f"Variable '{name}' is not available to be accessed in shared callback.")
  188. if not name.startswith("__") and name not in super().__getattribute__(_GuiState.__attrs[1]):
  189. raise AttributeError(f"Variable '{name}' is not accessible.")
  190. with self._notebook_context(gui), self._set_context(gui):
  191. encoded_name = gui._bind_var(name)
  192. setattr(gui._bindings(), encoded_name, value)
  193. def __getitem__(self, key: str):
  194. context = key if key in super().__getattribute__(_GuiState.__attrs[2]) else None
  195. if context is None:
  196. gui: "Gui" = super().__getattribute__(_GuiState.__gui_attr)
  197. page_ctx = gui._get_page_context(key)
  198. context = page_ctx if page_ctx is not None else None
  199. if context is None:
  200. raise RuntimeError(f"Can't resolve context '{key}' from state object")
  201. self._set_placeholder(_GuiState.__placeholder_attrs[1], context)
  202. return self
  203. def _set_context(self, gui: "Gui") -> t.ContextManager[None]:
  204. if (pl_ctx := self._get_placeholder(_GuiState.__placeholder_attrs[1])) is not None:
  205. self._set_placeholder(_GuiState.__placeholder_attrs[1], None)
  206. if pl_ctx != gui._get_locals_context():
  207. return gui._set_locals_context(pl_ctx)
  208. if len(inspect.stack()) > 1:
  209. ctx = _get_module_name_from_frame(t.cast(FrameType, t.cast(FrameType, inspect.stack()[2].frame)))
  210. current_context = gui._get_locals_context()
  211. # ignore context if the current one starts with the new one (to resolve for class modules)
  212. if ctx != current_context and not current_context.startswith(str(ctx)):
  213. return gui._set_locals_context(ctx)
  214. return nullcontext()
  215. def _notebook_context(self, gui: "Gui"):
  216. return gui.get_flask_app().app_context() if not has_app_context() and _is_in_notebook() else nullcontext()
  217. def _get_placeholder(self, name: str):
  218. if name in _GuiState.__placeholder_attrs:
  219. try:
  220. return super().__getattribute__(name)
  221. except AttributeError:
  222. return None
  223. return None
  224. def _set_placeholder(self, name: str, value: t.Any):
  225. if name in _GuiState.__placeholder_attrs:
  226. super().__setattr__(name, value)
  227. def _get_placeholder_attrs(self):
  228. return _GuiState.__placeholder_attrs
  229. def _add_attribute(self, name: str, default_value: t.Optional[t.Any] = None) -> bool:
  230. attrs: t.List[str] = super().__getattribute__(_GuiState.__attrs[1])
  231. if name not in attrs:
  232. attrs.append(name)
  233. gui = super().__getattribute__(_GuiState.__gui_attr)
  234. return gui._bind_var_val(name, default_value)
  235. return False
  236. class _AsyncState(_GuiState):
  237. def __init__(self, state: State) -> None:
  238. super().__init__(state.get_gui(), [], [])
  239. self._set_placeholder("__state_id", state.get_gui()._get_client_id())
  240. @staticmethod
  241. def __set_var_in_state(state: State, var_name: str, value: t.Any):
  242. setattr(state, var_name, value)
  243. @staticmethod
  244. def __get_var_from_state(state: State, var_name: str):
  245. return getattr(state, var_name)
  246. def __setattr__(self, var_name: str, var_value: t.Any) -> None:
  247. self.get_gui().invoke_callback(
  248. t.cast(str, self._get_placeholder("__state_id")), _AsyncState.__set_var_in_state, [var_name, var_value]
  249. )
  250. def __getattr__(self, var_name: str) -> t.Any:
  251. return self.get_gui().invoke_callback(
  252. t.cast(str, self._get_placeholder("__state_id")), _AsyncState.__get_var_from_state, [var_name]
  253. )