Răsfoiți Sursa

Merge branch 'develop' into feature/#1429-number-step-attribute

namnguyen 11 luni în urmă
părinte
comite
04811fdd54

+ 9 - 15
doc/gui/examples/broadcast.py

@@ -15,14 +15,15 @@
 # -----------------------------------------------------------------------------------------
 # Demonstrate how to share variable values across multiple clients.
 # This application creates a thread that increments a value every few seconds.
-# The value is updated for every client using the state.broadcast() method.
-# The text of the button that starts or stops the thread is updated on every client's browser
-# using a direct assignment of the state property because the variable is declared 'shared'.
+# The value is updated for every client in a function invoked by Gui.broadcast_change().
+# The text of the button that starts or stops the thread is updated using the
+# State.assign() method, and udateds on every client's browser because the variable was
+# declared 'shared' by calling Gui.add_shared_variable().
 # -----------------------------------------------------------------------------------------
 from threading import Event, Thread
 from time import sleep
 
-from taipy.gui import Gui, State, broadcast_callback
+from taipy.gui import Gui, State
 
 counter = 0
 
@@ -35,22 +36,16 @@ button_texts = ["Start", "Stop"]
 button_text = button_texts[0]
 
 
-# Invoked by the timer
-def update_counter(state: State, c):
-    # Update all clients
-    state.broadcast("counter", c)
-
-
 def count(event, gui):
     while not event.is_set():
         global counter
         counter = counter + 1
-        broadcast_callback(gui, update_counter, [counter])
+        gui.broadcast_change("counter", counter)
         sleep(2)
 
 
 # Start or stop the timer when the button is pressed
-def start_or_stop(state):
+def start_or_stop(state: State):
     global thread
     if thread:  # Timer is running
         thread_event.set()
@@ -59,9 +54,8 @@ def start_or_stop(state):
         thread_event.clear()
         thread = Thread(target=count, args=[thread_event, state.get_gui()])
         thread.start()
-    # Update button status.
-    # Because "button_text" is shared, all clients are updated
-    state.button_text = button_texts[1 if thread else 0]
+    # Update button status for all states.
+    state.assign("button_text", button_texts[1 if thread else 0])
 
 
 page = """# Broadcasting values

+ 57 - 0
doc/gui/examples/broadcast_callback.py

@@ -0,0 +1,57 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+# Demonstrate how to update the value of a variable across multiple clients.
+# This application creates a thread that sets a variable to the current time.
+# The value is updated for every client when Gui.broadcast_change() is invoked.
+# -----------------------------------------------------------------------------------------
+from datetime import datetime
+from threading import Thread
+from time import sleep
+
+from taipy.gui import Gui
+
+current_time = datetime.now()
+update = False
+
+
+# Update the 'current_time' state variable if 'update' is True
+def update_state(state, updated_time):
+    if state.update:
+        state.current_time = updated_time
+
+
+# The function that executes in its own thread.
+# Call 'update_state()` every second.
+def update_time(gui):
+    while True:
+        gui.broadcast_callback(update_state, [datetime.now()])
+        sleep(1)
+
+
+page = """
+Current time is: <|{current_time}|format=HH:mm:ss|>
+
+Update: <|{update}|toggle|>
+"""
+
+gui = Gui(page)
+
+# Run thread that regularly updates the current time
+thread = Thread(target=update_time, args=[gui], name="clock")
+thread.daemon = True
+thread.start()
+
+gui.run()

+ 48 - 0
doc/gui/examples/broadcast_change.py

@@ -0,0 +1,48 @@
+# 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.
+# -----------------------------------------------------------------------------------------
+# To execute this script, make sure that the taipy-gui package is installed in your
+# Python environment and run:
+#     python <script>
+# -----------------------------------------------------------------------------------------
+# Demonstrate how to invoke a callback for different clients.
+# This application creates a thread that, every second, invokes a callback for every client
+# so the current time may be updated, under a state-dependant condition.
+# -----------------------------------------------------------------------------------------
+from datetime import datetime
+from threading import Thread
+from time import sleep
+
+from taipy.gui import Gui
+
+current_time = datetime.now()
+
+
+# The function that executes in its own thread.
+# Update the current time every second.
+def update_time(gui):
+    while True:
+        gui.broadcast_change("current_time", datetime.now())
+        sleep(1)
+
+
+page = """
+Current time is: <|{current_time}|format=HH:mm:ss|>
+"""
+
+gui = Gui(page)
+
+# Run thread that regularly updates the current time
+thread = Thread(target=update_time, args=[gui], name="clock")
+thread.daemon = True
+thread.start()
+
+gui.run()

+ 9 - 9
doc/gui/examples/charts/advanced-python-lib.py

@@ -25,19 +25,19 @@ from taipy.gui import Gui
 figure = go.Figure()
 
 # Add trace for Normal Distribution
-figure.add_trace(go.Violin(name="Normal",
-                           y=np.random.normal(loc=0, scale=1, size=1000),
-                           box_visible=True, meanline_visible=True))
+figure.add_trace(
+    go.Violin(name="Normal", y=np.random.normal(loc=0, scale=1, size=1000), box_visible=True, meanline_visible=True)
+)
 
 # Add trace for Exponential Distribution
-figure.add_trace(go.Violin(name="Exponential",
-                           y=np.random.exponential(scale=1, size=1000),
-                           box_visible=True, meanline_visible=True))
+figure.add_trace(
+    go.Violin(name="Exponential", y=np.random.exponential(scale=1, size=1000), box_visible=True, meanline_visible=True)
+)
 
 # Add trace for Uniform Distribution
-figure.add_trace(go.Violin(name="Uniform",
-                           y=np.random.uniform(low=0, high=1, size=1000),
-                           box_visible=True, meanline_visible=True))
+figure.add_trace(
+    go.Violin(name="Uniform", y=np.random.uniform(low=0, high=1, size=1000), box_visible=True, meanline_visible=True)
+)
 
 # Updating layout for better visualization
 figure.update_layout(title="Different Probability Distributions")

+ 16 - 3
doc/gui/examples/charts/heatmap-drawing-on-top.py

@@ -31,8 +31,14 @@ def spiral(th):
 
 # Prepare the heatmap x and y cell sizes along the axes
 golden_ratio = (1 + numpy.sqrt(5)) / 2.0  # Golden ratio
-grid_x = [0, 1, 1 + (1 / (golden_ratio ** 4)), 1 + (1 / (golden_ratio ** 3)), golden_ratio]
-grid_y = [0, 1 / (golden_ratio ** 3), 1 / golden_ratio ** 3 + 1 / golden_ratio ** 4, 1 / (golden_ratio ** 2), 1]
+grid_x = [0, 1, 1 + (1 / (golden_ratio**4)), 1 + (1 / (golden_ratio**3)), golden_ratio]
+grid_y = [
+    0,
+    1 / (golden_ratio**3),
+    1 / golden_ratio**3 + 1 / golden_ratio**4,
+    1 / (golden_ratio**2),
+    1,
+]
 
 # Main value is based on the Fibonacci sequence
 z = [[13, 3, 3, 5], [13, 2, 1, 5], [13, 10, 11, 12], [13, 8, 8, 8]]
@@ -50,7 +56,14 @@ data = [
 ]
 
 # Axis template: hide all ticks, lines and labels
-axis = {"range": [0, 2.0], "showgrid": False, "zeroline": False, "showticklabels": False, "ticks": "", "title": ""}
+axis = {
+    "range": [0, 2.0],
+    "showgrid": False,
+    "zeroline": False,
+    "showticklabels": False,
+    "ticks": "",
+    "title": "",
+}
 
 layout = {
     # Use the axis template for both x and y axes

+ 1 - 7
doc/gui/examples/controls/metric-color-map.py

@@ -28,16 +28,10 @@ from taipy.gui import Gui
 # }
 
 value = 50
-color_map = {
-    20: "red",
-    40: None,
-    60: "blue",
-    80: None
-}
+color_map = {20: "red", 40: None, 60: "blue", 80: None}
 
 page = """
 <|{value}|metric|color_map={color_map}|>
 """
 
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-hide-value.py

@@ -21,4 +21,3 @@ page = """
 
 
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-layout.py

@@ -40,4 +40,3 @@ page = """
 """
 
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-range.py

@@ -25,4 +25,3 @@ page = """
 
 
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-type.py

@@ -23,4 +23,3 @@ page = """
 """
 
 Gui(page).run()
-

+ 0 - 1
doc/gui/examples/controls/metric-value-format.py

@@ -27,4 +27,3 @@ page = """
 
 
 Gui(page).run()
-

+ 1 - 1
frontend/taipy-gui/src/components/Taipy/PaginatedTable.tsx

@@ -173,7 +173,7 @@ const PaginatedTable = (props: TaipyPaginatedTableProps) => {
                 }
                 return [colsOrder, baseColumns, styTt.styles, styTt.tooltips, hNan, filter];
             } catch (e) {
-                console.info("PTable.columns: " + ((e as Error).message || e));
+                console.info("PaginatedTable.columns: ", (e as Error).message || e);
             }
         }
         return [

+ 2 - 12
frontend/taipy-gui/src/utils/index.ts

@@ -170,23 +170,13 @@ export const formatWSValue = (
             if (value == "") {
                 return "";
             }
-            try {
-                return getDateTimeString(value.toString(), dataFormat, formatConf);
-            } catch (e) {
-                console.error(`wrong dateformat "${dataFormat || formatConf.dateTime}"`, e);
-            }
-            return getDateTimeString(value.toString(), undefined, formatConf);
+            return getDateTimeString(value.toString(), dataFormat, formatConf);
         case "datetime.date":
         case "date":
             if (value == "") {
                 return "";
             }
-            try {
-                return getDateTimeString(value.toString(), dataFormat, formatConf, undefined, false);
-            } catch (e) {
-                console.error(`wrong dateformat "${dataFormat || formatConf.date}"`, e);
-            }
-            return getDateTimeString(value.toString(), undefined, formatConf, undefined, false);
+            return getDateTimeString(value.toString(), dataFormat, formatConf, undefined, false);
         case "int":
         case "float":
         case "number":

+ 121 - 41
taipy/gui/gui.py

@@ -49,7 +49,7 @@ import __main__  # noqa: F401
 from taipy.logger._taipy_logger import _TaipyLogger
 
 if util.find_spec("pyngrok"):
-    from pyngrok import ngrok
+    from pyngrok import ngrok  # type: ignore[reportMissingImports]
 
 from ._default_config import _default_stylekit, default_config
 from ._page import _Page
@@ -320,7 +320,7 @@ class Gui:
         self.__locals_context = _LocalsContext()
         self.__var_dir = _VariableDirectory(self.__locals_context)
 
-        self.__evaluator: _Evaluator = None  # type: ignore
+        self.__evaluator: _Evaluator = None  # type: ignore[assignment]
         self.__adapter = _Adapter()
         self.__directory_name_of_pages: t.List[str] = []
         self.__favicon: t.Optional[t.Union[str, Path]] = None
@@ -437,7 +437,7 @@ class Gui:
             if provider_fn is None:
                 # try plotly
                 if find_spec("plotly") and find_spec("plotly.graph_objs"):
-                    from plotly.graph_objs import Figure as PlotlyFigure
+                    from plotly.graph_objs import Figure as PlotlyFigure  # type: ignore[reportMissingImports]
 
                     if isinstance(content, PlotlyFigure):
 
@@ -509,6 +509,10 @@ class Gui:
     def _get_shared_variables(self) -> t.List[str]:
         return self.__evaluator.get_shared_variables()
 
+    @staticmethod
+    def _clear_shared_variable() -> None:
+        Gui.__shared_variables.clear()
+
     def __get_content_accessor(self):
         if self.__content_accessor is None:
             self.__content_accessor = _ContentAccessor(self._get_config("data_url_max_size", 50 * 1024))
@@ -1406,7 +1410,7 @@ class Gui:
             try:
                 fd, temp_path = mkstemp(".csv", var_name, text=True)
                 with os.fdopen(fd, "wt", newline="") as csv_file:
-                    df.to_csv(csv_file, index=False)  # type:ignore
+                    df.to_csv(csv_file, index=False)  # type: ignore[union-attr]
                 self._download(temp_path, "data.csv", Gui.__DOWNLOAD_DELETE_ACTION)
             except Exception as e:  # pragma: no cover
                 if not self._call_on_exception("download_csv", e):
@@ -1467,60 +1471,135 @@ class Gui:
                     _warn(f"on_action(): Exception raised in '{action_function.__name__}()'", e)
         return False
 
-    def _call_function_with_state(self, user_function: t.Callable, args: t.List[t.Any]) -> t.Any:
-        args.insert(0, self.__get_state())
+    def _call_function_with_state(self, user_function: t.Callable, args: t.Optional[t.List[t.Any]] = None) -> t.Any:
+        cp_args = [] if args is None else args.copy()
+        cp_args.insert(0, self.__get_state())
         argcount = user_function.__code__.co_argcount
         if argcount > 0 and inspect.ismethod(user_function):
             argcount -= 1
-        if argcount > len(args):
-            args += (argcount - len(args)) * [None]
+        if argcount > len(cp_args):
+            cp_args += (argcount - len(cp_args)) * [None]
         else:
-            args = args[:argcount]
-        return user_function(*args)
+            cp_args = cp_args[:argcount]
+        return user_function(*cp_args)
 
     def _set_module_context(self, module_context: t.Optional[str]) -> t.ContextManager[None]:
         return self._set_locals_context(module_context) if module_context is not None else contextlib.nullcontext()
 
-    def _call_user_callback(
+    def invoke_callback(
         self,
-        state_id: t.Optional[str],
-        user_callback: t.Union[t.Callable, str],
-        args: t.List[t.Any],
-        module_context: t.Optional[str],
+        state_id: str,
+        callback: t.Callable,
+        args: t.Optional[t.Sequence[t.Any]] = None,
+        module_context: t.Optional[str] = None,
     ) -> t.Any:
+        """Invoke a user callback for a given state.
+
+        See the
+        [section on Long Running Callbacks in a Thread](../gui/callbacks.md#long-running-callbacks-in-a-thread)
+        in the User Manual for details on when and how this function can be used.
+
+        Arguments:
+            state_id: The identifier of the state to use, as returned by `get_state_id()^`.
+            callback (Callable[[State^, ...], None]): The user-defined function that is invoked.<br/>
+                The first parameter of this function **must** be a `State^`.
+            args (Optional[Sequence]): The remaining arguments, as a List or a Tuple.
+            module_context (Optional[str]): the name of the module that will be used.
+        """
+        this_sid = None
+        if request:
+            # avoid messing with the client_id => Set(ws id)
+            this_sid = getattr(request, "sid", None)
+            request.sid = None  # type: ignore[attr-defined]
         try:
             with self.get_flask_app().app_context():
-                self.__set_client_id_in_context(state_id)
+                setattr(g, Gui.__ARG_CLIENT_ID, state_id)
                 with self._set_module_context(module_context):
-                    if not callable(user_callback):
-                        user_callback = self._get_user_function(user_callback)
-                    if not callable(user_callback):
-                        _warn(f"invoke_callback(): {user_callback} is not callable.")
+                    if not callable(callback):
+                        callback = self._get_user_function(callback)
+                    if not callable(callback):
+                        _warn(f"invoke_callback(): {callback} is not callable.")
                         return None
-                    return self._call_function_with_state(user_callback, args)
+                    return self._call_function_with_state(callback, list(args) if args else None)
         except Exception as e:  # pragma: no cover
-            if not self._call_on_exception(user_callback.__name__ if callable(user_callback) else user_callback, e):
+            if not self._call_on_exception(callback.__name__ if callable(callback) else callback, e):
                 _warn(
-                    "invoke_callback(): Exception raised in "
-                    + f"'{user_callback.__name__ if callable(user_callback) else user_callback}()'",
+                    "Gui.invoke_callback(): Exception raised in "
+                    + f"'{callback.__name__ if callable(callback) else callback}()'",
                     e,
                 )
+        finally:
+            if this_sid and request:
+                request.sid = this_sid  # type: ignore[attr-defined]
         return None
 
-    def _call_broadcast_callback(
-        self, user_callback: t.Callable, args: t.List[t.Any], module_context: t.Optional[str]
-    ) -> t.Any:
-        @contextlib.contextmanager
-        def _broadcast_callback() -> t.Iterator[None]:
-            try:
-                setattr(g, Gui.__BRDCST_CALLBACK_G_ID, True)
-                yield
-            finally:
-                setattr(g, Gui.__BRDCST_CALLBACK_G_ID, False)
+    def broadcast_callback(
+        self,
+        callback: t.Callable,
+        args: t.Optional[t.Sequence[t.Any]] = None,
+        module_context: t.Optional[str] = None,
+    ) -> t.Dict[str, t.Any]:
+        """Invoke a callback for every client.
+
+        This callback gets invoked for every client connected to the application with the appropriate
+        `State^` instance. You can then perform client-specific tasks, such as updating the state
+        variable reflected in the user interface.
+
+        Arguments:
+            gui (Gui^): The current Gui instance.
+            callback: The user-defined function to be invoked.<br/>
+                The first parameter of this function must be a `State^` object representing the
+                client for which it is invoked.<br/>
+                The other parameters should reflect the ones provided in the *args* collection.
+            args: The parameters to send to *callback*, if any.
+        """
+        # Iterate over all the scopes
+        res = {}
+        for id in [id for id in self.__bindings._get_all_scopes() if id != _DataScopes._GLOBAL_ID]:
+            ret = self.invoke_callback(id, callback, args, module_context)
+            res[id] = ret
+        return res
+
+    def broadcast_change(self, var_name: str, value: t.Any):
+        """Propagates a new value for a given variable to all states.
+
+        This callback gets invoked for every client connected to the application to update the value
+        of the variable called *var_name* to the new value *value*, in their specific `State^`
+        instance. All user interfaces reflect the change.
+
+        Arguments:
+            gui (Gui^): The current Gui instance.
+            var_name: The name of the variable to change.
+            value: The new value for the variable.
+        """
+        self.broadcast_callback(lambda s, n, v: s.assign(n, v), [var_name, value])
+
+    @staticmethod
+    def __broadcast_changes_fn(state: State, values: dict[str, t.Any]) -> None:
+        with state:
+            for n, v in values.items():
+                state.assign(n, v)
+
+    def broadcast_changes(self, values: t.Optional[dict[str, t.Any]] = None, **kwargs):
+        """Propagates new values for several variables to all states.
 
-        with _broadcast_callback():
-            # Use global scopes for broadcast callbacks
-            return self._call_user_callback(_DataScopes._GLOBAL_ID, user_callback, args, module_context)
+        This callback gets invoked for every client connected to the application to update the value
+        of all the variables that are keys in *values*, in their specific `State^` instance. All
+        user interfaces reflect the change.
+
+        Arguments:
+            values: An optional dictionary where each key is the name of a variable to change, and
+                where the associated value is the new value to set for that variable, in each state
+                for the application.
+            **kwargs: A collection of variable name-value pairs that are updated for each state of
+                the application. Name-value pairs overload the ones in *values* if the name exists
+                as a key in the dictionary.
+        """
+        if kwargs:
+            values = values.copy() if values else {}
+            for n, v in kwargs.items():
+                values[n] = v
+        self.broadcast_callback(Gui.__broadcast_changes_fn, [values])
 
     def _is_in_brdcst_callback(self):
         try:
@@ -2094,7 +2173,7 @@ class Gui:
                 if not isinstance(lib, ElementLibrary):
                     continue
                 try:
-                    self._call_function_with_state(lib.on_user_init, [])
+                    self._call_function_with_state(lib.on_user_init)
                 except Exception as e:  # pragma: no cover
                     if not self._call_on_exception(f"{name}.on_user_init", e):
                         _warn(f"Exception raised in {name}.on_user_init()", e)
@@ -2107,7 +2186,7 @@ class Gui:
             self.__init_libs()
             if hasattr(self, "on_init") and callable(self.on_init):
                 try:
-                    self._call_function_with_state(self.on_init, [])
+                    self._call_function_with_state(self.on_init)
                 except Exception as e:  # pragma: no cover
                     if not self._call_on_exception("on_init", e):
                         _warn("Exception raised in on_init()", e)
@@ -2298,7 +2377,7 @@ class Gui:
             config["extensions"] = {}
             for libs in self.__extensions.values():
                 for lib in libs:
-                    config["extensions"][f"./{Gui._EXTENSION_ROOT}/{lib.get_js_module_name()}"] = [  # type: ignore
+                    config["extensions"][f"./{Gui._EXTENSION_ROOT}/{lib.get_js_module_name()}"] = [
                         e._get_js_name(n)
                         for n, e in lib.get_elements().items()
                         if isinstance(e, Element) and not e._is_server_only()
@@ -2553,8 +2632,9 @@ class Gui:
 
         # Base global ctx is TaipyHolder classes + script modules and callables
         glob_ctx: t.Dict[str, t.Any] = {t.__name__: t for t in _TaipyBase.__subclasses__()}
-        glob_ctx.update({k: v for k, v in locals_bind.items() if inspect.ismodule(v) or callable(v)})
         glob_ctx[Gui.__SELF_VAR] = self
+        glob_ctx["state"] = self.__state
+        glob_ctx.update({k: v for k, v in locals_bind.items() if inspect.ismodule(v) or callable(v)})
 
         # Call on_init on each library
         for name, libs in self.__extensions.items():

+ 14 - 18
taipy/gui/gui_actions.py

@@ -267,39 +267,37 @@ def invoke_callback(
     gui: Gui,
     state_id: str,
     callback: t.Callable,
-    args: t.Union[t.Tuple, t.List],
+    args: t.Optional[t.Sequence[t.Any]] = None,
     module_context: t.Optional[str] = None,
 ) -> t.Any:
     """Invoke a user callback for a given state.
 
-    See the
-    [User Manual section on Long Running Callbacks in a Thread](../gui/callbacks.md#long-running-callbacks-in-a-thread)
-    for details on when and how this function can be used.
+    Calling this function is equivalent to calling
+    *gui*.[Gui.]invoke_callback(state_id, callback, args, module_context)^`.
 
     Arguments:
         gui (Gui^): The current Gui instance.
         state_id: The identifier of the state to use, as returned by `get_state_id()^`.
         callback (Callable[[State^, ...], None]): The user-defined function that is invoked.<br/>
             The first parameter of this function **must** be a `State^`.
-        args (Union[Tuple, List]): The remaining arguments, as a List or a Tuple.
+        args (Optional[Sequence]): The remaining arguments, as a List or a Tuple.
         module_context (Optional[str]): the name of the module that will be used.
     """
     if isinstance(gui, Gui):
-        return gui._call_user_callback(state_id, callback, list(args), module_context)
+        return gui.invoke_callback(state_id, callback, args, module_context)
     _warn("'invoke_callback()' must be called with a valid Gui instance.")
 
 
 def broadcast_callback(
     gui: Gui,
     callback: t.Callable,
-    args: t.Optional[t.Union[t.Tuple, t.List]] = None,
+    args: t.Optional[t.Sequence[t.Any]] = None,
     module_context: t.Optional[str] = None,
-) -> t.Any:
+) -> t.Dict[str, t.Any]:
     """Invoke a callback for every client.
 
-    This callback gets invoked for every client connected to the application with the appropriate
-    `State^` instance. You can then perform client-specific tasks, such as updating the state
-    variable reflected in the user interface.
+    Calling this function is equivalent to calling
+    *gui*.[Gui.]broadcast_callback(callback, args, module_context)^`.
 
     Arguments:
         gui (Gui^): The current Gui instance.
@@ -310,13 +308,13 @@ def broadcast_callback(
         args: The parameters to send to *callback*, if any.
     """
     if isinstance(gui, Gui):
-        return gui._call_broadcast_callback(callback, list(args) if args else [], module_context)
+        return gui.broadcast_callback(callback, args, module_context)
     _warn("'broadcast_callback()' must be called with a valid Gui instance.")
 
 
 def invoke_state_callback(gui: Gui, state_id: str, callback: t.Callable, args: t.Union[t.Tuple, t.List]) -> t.Any:
-    _warn("'invoke_state_callback()' was deprecated in Taipy GUI 2.0. Use 'invoke_callback()' instead.")
-    return invoke_callback(gui, state_id, callback, args)
+    _warn("'invoke_state_callback()' was deprecated in Taipy GUI 2.0. Use 'Gui.invoke_callback()' instead.")
+    return gui.invoke_callback(state_id, callback, args)
 
 
 def invoke_long_callback(
@@ -393,16 +391,14 @@ def invoke_long_callback(
         function_result: t.Optional[t.Any] = None,
     ):
         if callable(user_status_function):
-            invoke_callback(
-                this_gui,
+            this_gui.invoke_callback(
                 str(state_id),
                 user_status_function,
                 [status] + list(user_status_function_args) + [function_result],  # type: ignore
                 str(module_context),
             )
         if e:
-            invoke_callback(
-                this_gui,
+            this_gui.invoke_callback(
                 str(state_id),
                 callback_on_exception,
                 (

+ 59 - 59
taipy/gui/viselements.json

@@ -1535,65 +1535,6 @@
                 ]
             }
         ],
-        [
-            "dialog",
-            {
-                "inherits": [
-                    "partial",
-                    "active",
-                    "shared"
-                ],
-                "properties": [
-                    {
-                        "name": "open",
-                        "default_property": true,
-                        "type": "bool",
-                        "default_value": "False",
-                        "doc": "If True, the dialog is visible. If False, it is hidden."
-                    },
-                    {
-                        "name": "on_action",
-                        "type": "Callback",
-                        "doc": "Name of a function triggered when a button is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the dialog.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list where the first element contains the index of the selected label.</li>\n</ul>\n</li>\n</ul>",
-                        "signature": [
-                            [
-                                "state",
-                                "State"
-                            ],
-                            [
-                                "id",
-                                "str"
-                            ],
-                            [
-                                "payload",
-                                "dict"
-                            ]
-                        ]
-                    },
-                    {
-                        "name": "close_label",
-                        "type": "str",
-                        "default_value": "\"Close\"",
-                        "doc": "The tooltip of the top-right close icon button. In the <tt>on_action</tt> callback, args will hold -1."
-                    },
-                    {
-                        "name": "labels",
-                        "type": " str|list[str]",
-                        "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the <tt>on_action</tt> callback (that index is -1 for the <i>close</i> icon)."
-                    },
-                    {
-                        "name": "width",
-                        "type": "str|int|float",
-                        "doc": "The width, in CSS units, of this dialog.<br/>(CSS property)"
-                    },
-                    {
-                        "name": "height",
-                        "type": "str|int|float",
-                        "doc": "The height, in CSS units, of this dialog.<br/>(CSS property)"
-                    }
-                ]
-            }
-        ],
         [
             "tree",
             {
@@ -1691,6 +1632,65 @@
                 ]
             }
         ],
+        [
+            "dialog",
+            {
+                "inherits": [
+                    "partial",
+                    "active",
+                    "shared"
+                ],
+                "properties": [
+                    {
+                        "name": "open",
+                        "default_property": true,
+                        "type": "bool",
+                        "default_value": "False",
+                        "doc": "If True, the dialog is visible. If False, it is hidden."
+                    },
+                    {
+                        "name": "on_action",
+                        "type": "Callback",
+                        "doc": "Name of a function triggered when a button is pressed.<br/>The parameters of that function are all optional:\n<ul>\n<li>state (<code>State^</code>): the state instance.</li>\n<li>id (str): the identifier of the dialog.</li>\n<li>payload (dict): the details on this callback's invocation.<br/>This dictionary has the following keys:\n<ul>\n<li>action: the name of the action that triggered this callback.</li>\n<li>args: a list where the first element contains the index of the selected label.</li>\n</ul>\n</li>\n</ul>",
+                        "signature": [
+                            [
+                                "state",
+                                "State"
+                            ],
+                            [
+                                "id",
+                                "str"
+                            ],
+                            [
+                                "payload",
+                                "dict"
+                            ]
+                        ]
+                    },
+                    {
+                        "name": "close_label",
+                        "type": "str",
+                        "default_value": "\"Close\"",
+                        "doc": "The tooltip of the top-right close icon button. In the <tt>on_action</tt> callback, args will hold -1."
+                    },
+                    {
+                        "name": "labels",
+                        "type": " str|list[str]",
+                        "doc": "A list of labels to show in a row of buttons at the bottom of the dialog. The index of the button in the list is reported as args in the <tt>on_action</tt> callback (that index is -1 for the <i>close</i> icon)."
+                    },
+                    {
+                        "name": "width",
+                        "type": "str|int|float",
+                        "doc": "The width, in CSS units, of this dialog.<br/>(CSS property)"
+                    },
+                    {
+                        "name": "height",
+                        "type": "str|int|float",
+                        "doc": "The height, in CSS units, of this dialog.<br/>(CSS property)"
+                    }
+                ]
+            }
+        ],
         [
             "layout",
             {

+ 1 - 1
taipy/gui_core/_context.py

@@ -182,7 +182,7 @@ class _GuiCoreContext(CoreEventConsumerBase):
                         # callback
                         submission_name = submission.properties.get("on_submission")
                         if submission_name:
-                            self.gui._call_user_callback(
+                            self.gui.invoke_callback(
                                 client_id,
                                 submission_name,
                                 [

+ 46 - 5
tests/gui/actions/test_invoke_callback.py

@@ -9,11 +9,20 @@
 # 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 contextlib
 import inspect
 
 from flask import g
 
-from taipy.gui import Gui, Markdown, State, invoke_callback
+from taipy.gui import Gui, Markdown, State
+
+
+@contextlib.contextmanager
+def get_state(gui: Gui, state_id: str):
+    with gui.get_flask_app().app_context():
+        client_id = gui._bindings()._get_or_create_scope(state_id)[0]
+        gui._Gui__set_client_id_in_context(client_id)  # type: ignore[attr-defined]
+        yield gui._Gui__state  # type: ignore[attr-defined]
 
 
 def test_invoke_callback(gui: Gui, helpers):
@@ -29,13 +38,45 @@ def test_invoke_callback(gui: Gui, helpers):
     gui._set_frame(inspect.currentframe())
 
     gui.add_page("test", Markdown("<|Hello {name}|button|id={btn_id}|>\n<|{val}|>"))
-    gui.run(run_server=False, single_client=True)
+    gui.run(run_server=False)
     flask_client = gui._server.test_client()
     # client id
     cid = helpers.create_scope_and_get_sid(gui)
+    # Get the jsx once so that the page will be evaluated -> variable will be registered
+    flask_client.get(f"/taipy-jsx/test?client_id={cid}")
+
+    gui.invoke_callback(cid, user_callback, [])
+    with get_state(gui, cid) as state:
+        assert state.val == 10
+
+
+def test_invoke_callback_sid(gui: Gui, helpers):
+    name = "World!"  # noqa: F841
+    btn_id = "button1"  # noqa: F841
+
+    val = 1  # noqa: F841
+
+    def user_callback(state: State):
+        state.val = 10
+
+    # set gui frame
+    gui._set_frame(inspect.currentframe())
+
+    gui.add_page("test", Markdown("<|Hello {name}|button|id={btn_id}|>\n<|{val}|>"))
+    gui.run(run_server=False)
+    flask_client = gui._server.test_client()
+    # client id
+    cid = helpers.create_scope_and_get_sid(gui)
+    base_sid, _ = gui._bindings()._get_or_create_scope("base sid")
+
     # Get the jsx once so that the page will be evaluated -> variable will be registered
     flask_client.get(f"/taipy-jsx/test?client_id={cid}")
     with gui.get_flask_app().app_context():
-        g.client_id = cid
-        invoke_callback(gui, cid, user_callback, [])
-        assert gui._Gui__state.val == 10  # type: ignore[attr-defined]
+        g.client_id = base_sid
+        gui.invoke_callback(cid, user_callback, [])
+        assert g.client_id == base_sid
+
+    with get_state(gui, base_sid) as base_state:
+        assert base_state.val == 1
+    with get_state(gui, cid) as a_state:
+        assert a_state.val == 10

+ 10 - 0
tests/gui/builder/control/test_dialog.py

@@ -74,3 +74,13 @@ def test_dialog_labels_builder(gui: Gui, helpers):
         "open={_TpB_tpec_TpExPr_dialog_open_TPMDL_0}",
     ]
     helpers.test_control_builder(gui, page, expected_list)
+
+def test_dialog_builder_block(gui: Gui, helpers):
+    with tgb.dialog(title="Another Dialog") as content:  # type: ignore[attr-defined]
+        tgb.text(value="This is in a dialog")  # type: ignore[attr-defined]
+    expected_list = [
+        "<Dialog",
+        'title="Another Dialog"',
+        "This is in a dialog",
+    ]
+    helpers.test_control_builder(gui, tgb.Page(content, frame=None), expected_list)

+ 138 - 0
tests/gui/gui_specific/test_broadcast.py

@@ -0,0 +1,138 @@
+# 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 contextlib
+import inspect
+
+import pytest
+
+from taipy.gui import Gui
+
+
+@contextlib.contextmanager
+def get_state(gui: Gui, state_id: str):
+    with gui.get_flask_app().app_context():
+        client_id = gui._bindings()._get_or_create_scope(state_id)[0]
+        gui._Gui__set_client_id_in_context(client_id)  # type: ignore[attr-defined]
+        yield gui._Gui__state  # type: ignore[attr-defined]
+
+def test_multiple_scopes(gui: Gui):
+    var = 1  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    gui.add_page("page1", "<|{var}|>")
+    gui.run(run_server=False)
+    with get_state(gui, "s1") as state1:
+        assert state1.var == 1
+    with get_state(gui, "s2") as state2:
+        assert state2.var == 1
+    with get_state(gui, "s1") as state1:
+        state1.var = 2
+        assert state1.var == 2
+    with get_state(gui, "s2") as state2:
+        assert state2.var == 1
+
+
+def test_shared_variable(gui: Gui):
+    var = 1  # noqa: F841
+    s_var = 10  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    gui.add_page("test", "<|{var}|>")
+    gui.add_shared_variable("s_var")
+    gui.run(run_server=False)
+    with get_state(gui, "s1") as state1:
+        assert state1.var == 1
+        assert state1.s_var == 10
+        state1.var = 2
+        state1.s_var = 20
+        assert state1.var == 2
+        assert state1.s_var == 20
+    with get_state(gui, "s2") as state2:
+        assert state2.var == 1
+        assert state1.s_var == 20
+    Gui._clear_shared_variable()
+
+
+def test_broadcast_change(gui: Gui):
+    # Bind test variables
+    v1 = "none"  # noqa: F841
+    v2 = 1  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    gui.add_page("test", " <|{v1}|><|{v2}|>")
+    gui.run(run_server=False)
+    s1, _ = gui._bindings()._get_or_create_scope("s1")
+    s2, _ = gui._bindings()._get_or_create_scope("s2")
+    gui.broadcast_change("v2", 2)
+    with get_state(gui, s1) as state1:
+        assert state1.v1 == "none"
+        assert state1.v2 == 2
+    with get_state(gui, s2) as state2:
+        assert state2.v1 == "none"
+        assert state2.v2 == 2
+
+def test_broadcast_changes(gui: Gui):
+    # Bind test variables
+    v1 = "none"  # noqa: F841
+    v2 = 1  # noqa: F841
+    gui._set_frame(inspect.currentframe())
+    gui.add_page("test", " <|{v1}|><|{v2}|>")
+    gui.run(run_server=False)
+    s1, _ = gui._bindings()._get_or_create_scope("s1")
+    s2, _ = gui._bindings()._get_or_create_scope("s2")
+
+    changes = { "v1": "some", "v2": 2}
+    gui.broadcast_changes(changes)
+    with get_state(gui, s1) as state1:
+        assert state1.v1 == "some"
+        assert state1.v2 == 2
+    with get_state(gui, s2) as state2:
+        assert state2.v1 == "some"
+        assert state2.v2 == 2
+
+    gui.broadcast_changes(v1="more", v2=3)
+    with get_state(gui, s1) as state1:
+        assert state1.v1 == "more"
+        assert state1.v2 == 3
+    with get_state(gui, s2) as state2:
+        assert state2.v1 == "more"
+        assert state2.v2 == 3
+
+    gui.broadcast_changes({ "v1": "more yet"}, v2=4)
+    with get_state(gui, s1) as state1:
+        assert state1.v1 == "more yet"
+        assert state1.v2 == 4
+    with get_state(gui, s2) as state2:
+        assert state2.v1 == "more yet"
+        assert state2.v2 == 4
+
+
+def test_broadcast_callback(gui: Gui):
+    gui.run(run_server=False)
+
+    res = gui.broadcast_callback(lambda _, t: t, ["Hello World"], "mine")
+    assert isinstance(res, dict)
+    assert not res
+
+    gui._bindings()._get_or_create_scope("test scope")
+
+    res = gui.broadcast_callback(lambda _, t: t, ["Hello World"], "mine")
+    assert len(res) == 1
+    assert res.get("test scope", None) == "Hello World"
+
+    with pytest.warns(UserWarning):
+        res = gui.broadcast_callback(print, ["Hello World"], "mine")
+        assert isinstance(res, dict)
+        assert res.get("test scope", "Hello World") is None
+
+    gui._bindings()._get_or_create_scope("another scope")
+    res = gui.broadcast_callback(lambda s, t: t, ["Hello World"], "mine")
+    assert len(res) == 2
+    assert res.get("test scope", None) == "Hello World"
+    assert res.get("another scope", None) == "Hello World"

+ 0 - 12
tests/gui/gui_specific/test_gui.py

@@ -42,18 +42,6 @@ def test__get_user_instance(gui: Gui):
             gui._get_user_instance("", type(None))
 
 
-def test__call_broadcast_callback(gui: Gui):
-    gui.run(run_server=False)
-    with gui.get_flask_app().app_context():
-        res = gui._call_broadcast_callback(lambda s, t: t, ["Hello World"], "mine")
-        assert res == "Hello World"
-
-    with gui.get_flask_app().app_context():
-        with pytest.warns(UserWarning):
-            res = gui._call_broadcast_callback(print, ["Hello World"], "mine")
-            assert res is None
-
-
 def test__refresh_expr(gui: Gui):
     gui.run(run_server=False)
     with gui.get_flask_app().app_context():

+ 6 - 2
tests/gui/gui_specific/test_shared.py

@@ -13,9 +13,13 @@ from taipy.gui import Gui
 
 
 def test_add_shared_variables(gui: Gui):
+    assert len(gui._Gui__shared_variables) == 0  # type: ignore[attr-defined]
+
     Gui.add_shared_variable("var1", "var2")
     assert isinstance(gui._Gui__shared_variables, list)  # type: ignore[attr-defined]
     assert len(gui._Gui__shared_variables) == 2  # type: ignore[attr-defined]
 
-    Gui.add_shared_variables("var1", "var2")
-    assert len(gui._Gui__shared_variables) == 2  # type: ignore[attr-defined]
+    Gui.add_shared_variables("var1", "var3")
+    assert len(gui._Gui__shared_variables) == 3  # type: ignore[attr-defined]
+
+    Gui._clear_shared_variable()