state.py 10 KB

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