Skip to content

Commit

Permalink
Expose invoke_callback() and broadcast_callback() as Gui methods.
Browse files Browse the repository at this point in the history
  • Loading branch information
FabienLelaquais committed Jun 25, 2024
1 parent 1378062 commit 824c9fa
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 51 deletions.
76 changes: 53 additions & 23 deletions taipy/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from taipy.logger._taipy_logger import _TaipyLogger

if util.find_spec("pyngrok"):
from pyngrok import ngrok
from pyngrok import ngrok # type: ignore

from ._default_config import _default_stylekit, default_config
from ._page import _Page
Expand Down Expand Up @@ -435,7 +435,7 @@ def __process_content_provider(self, state: State, path: str, query: t.Dict[str,
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

if isinstance(content, PlotlyFigure):

Expand Down Expand Up @@ -1448,8 +1448,8 @@ def __call_function_with_args(self, **kwargs):
_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:
cp_args = args.copy()
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):
Expand All @@ -1463,39 +1463,69 @@ def _call_function_with_state(self, user_function: t.Callable, args: t.List[t.An
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.
"""
try:
with self.get_flask_app().app_context():
self.__set_client_id_in_context(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, args)
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,
)
return None

Check failure on line 1504 in taipy/gui/gui.py

View workflow job for this annotation

GitHub Actions / partial-tests / linter

Argument 2 to "_call_function_with_state" of "Gui" has incompatible type "Sequence[Any] | None"; expected "list[Any] | None" [arg-type]
def _call_broadcast_callback(
self, user_callback: t.Callable, args: t.List[t.Any], module_context: t.Optional[str]
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]:
# get scopes
"""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._call_user_callback(id, user_callback, args, module_context)
ret = self.invoke_callback(id, callback, args, module_context)
res[id] = ret
return res

Expand Down Expand Up @@ -2065,7 +2095,7 @@ def __init_libs(self):
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)
Expand All @@ -2078,7 +2108,7 @@ def __init_route(self):
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)
Expand Down
30 changes: 13 additions & 17 deletions taipy/gui/gui_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.
Expand All @@ -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(
Expand Down Expand Up @@ -393,16 +391,14 @@ def callback_on_status(
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,
(
Expand Down
2 changes: 1 addition & 1 deletion taipy/gui_core/_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def submission_status_callback(self, submission_id: t.Optional[str] = None, even
# callback
submission_name = submission.properties.get("on_submission")
if submission_name:
self.gui._call_user_callback(
self.gui.invoke_callback(
client_id,
submission_name,
[
Expand Down
2 changes: 1 addition & 1 deletion tests/gui/actions/test_invoke_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ def user_callback(state: State):
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, [])
gui.invoke_callback(cid, user_callback, [])
assert gui._Gui__state.val == 10 # type: ignore[attr-defined]
18 changes: 9 additions & 9 deletions tests/gui/gui_specific/test_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,28 @@ def test__get_user_instance(gui: Gui):
gui._get_user_instance("", type(None))


def test__call_broadcast_callback(gui: Gui):
def test__broadcast_callback(gui: Gui):
gui.run(run_server=False)

res = gui._call_broadcast_callback(lambda s, t: t, ["Hello World"], "mine")
res = gui.broadcast_callback(lambda _, t: t, ["Hello World"], "mine")
assert isinstance(res, dict)
assert not res

gui._bindings()._get_or_create_scope("mon scope")
gui._bindings()._get_or_create_scope("test scope")

res = gui._call_broadcast_callback(lambda s, t: t, ["Hello World"], "mine")
res = gui.broadcast_callback(lambda _, t: t, ["Hello World"], "mine")
assert len(res) == 1
assert res.get("mon scope", None) == "Hello World"
assert res.get("test scope", None) == "Hello World"

with pytest.warns(UserWarning):
res = gui._call_broadcast_callback(print, ["Hello World"], "mine")
res = gui.broadcast_callback(print, ["Hello World"], "mine")
assert isinstance(res, dict)
assert res.get("mon scope", "Hello World") is None
assert res.get("test scope", "Hello World") is None

gui._bindings()._get_or_create_scope("another scope")
res = gui._call_broadcast_callback(lambda s, t: t, ["Hello World"], "mine")
res = gui.broadcast_callback(lambda s, t: t, ["Hello World"], "mine")
assert len(res) == 2
assert res.get("mon scope", None) == "Hello World"
assert res.get("test scope", None) == "Hello World"
assert res.get("another scope", None) == "Hello World"


Expand Down

0 comments on commit 824c9fa

Please sign in to comment.