Skip to content

Commit e65e96f

Browse files
FredLL-AvaigaFred Lefévère-Laoide
andauthored
Manage authorization in events (#2805)
* Manage authorization in events Hide authorization exceptions Closes Enterprise #707 #708 #709 * show warning when debugging + Fab's comment * try another version of networkx * revert * upgrade pandas to 2.2 * remove mongomock duplicate * mongomock 4.2.13 does not exist * mongomock 4.2.13 does not exist * adding moto[s3] * fixing pipfile for mongomock and moto * manage pandas version * pandas versions * no array is pipfile * try something that should work * remove unused * initialize context * more tests --------- Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io>
1 parent 58767da commit e65e96f

File tree

11 files changed

+252
-15
lines changed

11 files changed

+252
-15
lines changed

Pipfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ marshmallow = "==3.20.1"
2323
networkx = "==2.6"
2424
numpy = "<2.0.0"
2525
openpyxl = "==3.1.2"
26-
pandas = "==1.3.5"
26+
pandas = "==2.2"
2727
pyarrow = "==16.0.0"
2828
pymongo = {extras = ["srv"], version = "==4.6.3"}
2929
python-dotenv = "==1.0.0"

taipy/gui/gui.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3063,9 +3063,18 @@ def _get_authorization(self, client_id: t.Optional[str] = None, system: t.Option
30633063
try:
30643064
return _Hooks()._get_authorization(self, client_id, system) or contextlib.nullcontext()
30653065
except Exception as e:
3066-
_warn("Hooks:", e)
3066+
_warn("Hooks._get_authorization()", e)
30673067
return contextlib.nullcontext()
30683068

3069+
def _is_exception_ignored(self, exception: Exception, source: t.Optional[str] = None) -> bool:
3070+
if is_debugging():
3071+
return False
3072+
try:
3073+
return _Hooks()._is_exception_ignored(self, exception, source) or False
3074+
except Exception as e:
3075+
_warn("Hooks._is_exception_ignored()", e)
3076+
return False
3077+
30693078
def set_favicon(self, favicon_path: t.Union[str, Path], state: t.Optional[State] = None):
30703079
"""Change the favicon for all clients.
30713080
@@ -3122,7 +3131,8 @@ def __do_fire_event(
31223131
with self.get_app_context(), self.__event_manager:
31233132
if client_id:
31243133
setattr(get_server_request_accessor(self).get_request_meta(), Gui.__ARG_CLIENT_ID, client_id)
3125-
_Hooks()._fire_event(event_name, client_id, payload)
3134+
with self._get_authorization():
3135+
_Hooks()._fire_event(event_name, client_id, payload)
31263136
finally:
31273137
if this_sid:
31283138
get_server_request_accessor(self).set_sid(this_sid)

taipy/gui/test/mock_state.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,11 @@ def broadcast(self, name: str, value: t.Any):
6666

6767
def _invoke_on_gui(self, method: t.Callable, *args):
6868
return method(self.get_gui(), *args)
69+
70+
def patch(
71+
self,
72+
name: str,
73+
change: t.Optional[t.Dict[t.Union[str, int], t.Any]] = None,
74+
remove: t.Optional[t.Dict[t.Union[str, int], t.Any]] = None,
75+
):
76+
return

taipy/gui/utils/_evaluator.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ def __evaluate_holder(self, gui: Gui, holder: t.Type[_TaipyBase], expr: str) ->
237237
holder_value.set(expr_value)
238238
return holder_value
239239
except Exception as e:
240-
_warn(f"Cannot evaluate expression {holder.__name__}({expr_hash},'{expr_hash}') for {expr}", e)
240+
if not gui._is_exception_ignored(e, "Evaluator"):
241+
_warn(f"Cannot evaluate expression {holder.__name__}({expr_hash},'{expr_hash}') for {expr}", e)
241242
return None
242243

243244
def evaluate_expr(
@@ -277,12 +278,13 @@ def evaluate_expr(
277278
with gui._get_authorization(): # type: ignore[attr-defined]
278279
expr_evaluated = eval(not_encoded_expr if is_edge_case else expr_string, ctx)
279280
except Exception as e:
280-
exception_str = not_encoded_expr if is_edge_case else expr_string
281-
_warn(
282-
f"Cannot evaluate expression '{_Evaluator._clean_exception_expr(exception_str)}'",
283-
e,
284-
always_show=True,
285-
)
281+
if not gui._is_exception_ignored(e, "Evaluator"):
282+
exception_str = not_encoded_expr if is_edge_case else expr_string
283+
_warn(
284+
f"Cannot evaluate expression '{_Evaluator._clean_exception_expr(exception_str)}'",
285+
e,
286+
always_show=True,
287+
)
286288
expr_evaluated = None
287289
if lambda_expr and callable(expr_evaluated):
288290
expr_hash = _get_lambda_id(expr_evaluated, module=module_name) # type: ignore[arg-type]
@@ -313,7 +315,8 @@ def refresh_expr(self, gui: Gui, var_name: str, holder: t.Optional[_TaipyBase]):
313315
if holder is not None:
314316
holder.set(expr_evaluated)
315317
except Exception as e:
316-
_warn(f"Exception raised evaluating {_Evaluator._clean_exception_expr(expr_string)}", e)
318+
if not gui._is_exception_ignored(e, "Evaluator"):
319+
_warn(f"Exception raised evaluating {_Evaluator._clean_exception_expr(expr_string)}", e)
317320

318321
def re_evaluate_expr(self, gui: Gui, var_name: str) -> t.Set[str]: # noqa C901
319322
"""

taipy/gui_core/_context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ def __init__(self, gui: Gui) -> None:
100100
self.submissions_lock = Lock()
101101
# lazy_start
102102
self.__started = False
103-
# Gui event listener
104-
gui._add_event_listener("authorization", self._auth_listener, with_state=True) # type: ignore
105103
# super
106104
super().__init__(reg_id, reg_queue)
107105

108106
def on_user_init(self, state: State):
107+
# Gui event listener
108+
self.gui._add_event_listener("authorization", self._auth_listener, with_state=True) # type: ignore
109109
self.gui._fire_event("authorization", get_state_id(state), {}) # type: ignore
110110

111111
def __lazy_start(self):

tests/gui_core/test_context_crud_scenario.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,26 @@ def mock_is_true(entity_id):
4141

4242

4343
class TestGuiCoreContext_crud_scenario:
44+
def test_crud_scenario_none(self):
45+
gui_core_context = _GuiCoreContext(Mock())
46+
state = MockState(Mock())
47+
48+
mock_core_delete = Mock()
49+
50+
with (
51+
patch("taipy.gui_core._context.core_get", side_effect=mock_core_get),
52+
patch("taipy.gui_core._context.is_deletable", side_effect=mock_is_true),
53+
patch("taipy.gui_core._context.core_delete", side_effect=mock_core_delete),
54+
):
55+
assert (
56+
gui_core_context.crud_scenario(
57+
state,
58+
"id",
59+
t.cast(dict, {}),
60+
)
61+
is None
62+
)
63+
4464
def test_crud_scenario_delete(self):
4565
gui_core_context = _GuiCoreContext(Mock())
4666
state = MockState(Mock())
@@ -58,3 +78,59 @@ def test_crud_scenario_delete(self):
5878
t.cast(dict, {"args": [None, None, None, True, True, {"id": "a_scenario_id"}], "error_id": "error_id"}),
5979
)
6080
mock_core_delete.assert_called_once()
81+
82+
def test_crud_scenario_create_no_scenario(self):
83+
gui_core_context = _GuiCoreContext(Mock())
84+
state = MockState(Mock())
85+
state.assign = Mock()
86+
87+
mock_core_delete = Mock()
88+
89+
with (
90+
patch("taipy.gui_core._context.core_get", side_effect=mock_core_get),
91+
patch("taipy.gui_core._context.is_deletable", side_effect=mock_is_true),
92+
patch("taipy.gui_core._context.core_delete", side_effect=mock_core_delete),
93+
):
94+
gui_core_context.crud_scenario(
95+
state,
96+
"id",
97+
t.cast(
98+
dict, {"args": [None, None, None, False, True, {"id": "a_scenario_id"}], "error_id": "error_id"}
99+
),
100+
)
101+
state.assign.assert_called_once_with("error_id", "Invalid configuration id (None)")
102+
103+
def test_crud_scenario_create_scenario_no_dialog(self):
104+
gui_core_context = _GuiCoreContext(Mock())
105+
state = MockState(Mock())
106+
state.assign = Mock()
107+
108+
mock_core_delete = Mock()
109+
110+
with (
111+
patch("taipy.gui_core._context.core_get", side_effect=mock_core_get),
112+
patch("taipy.gui_core._context.is_deletable", side_effect=mock_is_true),
113+
patch("taipy.gui_core._context.core_delete", side_effect=mock_core_delete),
114+
):
115+
gui_core_context.crud_scenario(
116+
state,
117+
"id",
118+
t.cast(
119+
dict,
120+
{
121+
"args": [
122+
None,
123+
None,
124+
{"config": "a_scenario_config_id"},
125+
False,
126+
True,
127+
{"id": "a_scenario_id"},
128+
False,
129+
],
130+
"error_id": "error_id",
131+
},
132+
),
133+
)
134+
state.assign.assert_called_once_with(
135+
"error_id", "Error creating Scenario: only one scenario config needed (0) found."
136+
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
12+
import typing as t
13+
from unittest.mock import Mock, patch
14+
15+
from taipy import Scope
16+
from taipy.core import DataNode, Scenario
17+
from taipy.core.data.pickle import PickleDataNode
18+
from taipy.gui_core._context import _GuiCoreContext
19+
20+
scenario_a = Scenario("scenario_a_config_id", None, {"a_prop": "a"})
21+
scenario_b = Scenario("scenario_b_config_id", None, {"a_prop": "b"})
22+
scenarios: t.List[t.Union[t.List, Scenario, None]] = [scenario_a, scenario_b]
23+
24+
25+
datanode_a = PickleDataNode("datanode_a_config_id", Scope.SCENARIO)
26+
datanode_b = PickleDataNode("datanode_b_config_id", Scope.SCENARIO)
27+
datanodes: t.List[t.Union[t.List, DataNode, None]] = [datanode_a, datanode_b]
28+
29+
30+
def mock_core_get(entity_id):
31+
if entity_id == datanode_a.id:
32+
return datanode_a
33+
if entity_id == datanode_b.id:
34+
return datanode_b
35+
return None
36+
37+
38+
def mock_is_true(entity_id):
39+
return True
40+
41+
42+
class TestGuiCoreContext_get_datanodes_tree:
43+
def test_crud_scenario_none(self):
44+
gui_core_context = _GuiCoreContext(Mock())
45+
46+
mock_core_delete = Mock()
47+
48+
with (
49+
patch("taipy.gui_core._context.core_get", side_effect=mock_core_get),
50+
patch("taipy.gui_core._context.is_deletable", side_effect=mock_is_true),
51+
patch("taipy.gui_core._context.core_delete", side_effect=mock_core_delete),
52+
):
53+
res = gui_core_context.get_datanodes_tree(None, None, None, None)
54+
assert isinstance(res, list)
55+
assert len(res) == 0
56+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
12+
import typing as t
13+
from unittest.mock import Mock
14+
15+
from taipy.core import Cycle, Scenario
16+
from taipy.gui_core._context import _GuiCoreContext
17+
18+
scenario_a = Scenario("scenario_a_config_id", None, {"a_prop": "a"})
19+
scenario_b = Scenario("scenario_b_config_id", None, {"a_prop": "b"})
20+
scenarios: t.List[t.Union[Cycle, Scenario]] = [scenario_a, scenario_b]
21+
22+
23+
class TestGuiCoreContext_on_init:
24+
def test_on_init(self):
25+
gui_core_context = _GuiCoreContext(Mock())
26+
mock_state = Mock()
27+
gui_core_context._auth_listener = Mock(gui_core_context._auth_listener)
28+
gui_core_context.on_user_init(mock_state)
29+
gui_core_context._auth_listener.assert_not_called()
30+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
12+
import typing as t
13+
from unittest.mock import Mock
14+
15+
from taipy.core import Cycle, Scenario
16+
from taipy.gui_core._context import _GuiCoreContext
17+
18+
scenario_a = Scenario("scenario_a_config_id", None, {"a_prop": "a"})
19+
scenario_b = Scenario("scenario_b_config_id", None, {"a_prop": "b"})
20+
scenarios: t.List[t.Union[Cycle, Scenario]] = [scenario_a, scenario_b]
21+
22+
23+
class TestGuiCoreContext_scenarios:
24+
def test_get_scenarios_no_scenarios(self):
25+
gui_core_context = _GuiCoreContext(Mock())
26+
gui_core_context.scenario_by_cycle = {}
27+
new_scenarios = gui_core_context.get_scenarios(None, None, None)
28+
assert isinstance(new_scenarios, list)
29+
assert len(new_scenarios) == 0
30+
31+
def test_get_scenarios_no_filter(self):
32+
gui_core_context = _GuiCoreContext(Mock())
33+
gui_core_context.scenario_by_cycle = {}
34+
new_scenarios = gui_core_context.get_scenarios(scenarios, None, None)
35+
assert len(new_scenarios) == len(scenarios)
36+
for new_scenario, scenario in zip(new_scenarios, scenarios):
37+
assert new_scenario is scenario
38+
39+
def test_select_scenario(self):
40+
gui_core_context = _GuiCoreContext(Mock())
41+
mock_state = Mock()
42+
mock_state.assign = Mock()
43+
gui_core_context.select_scenario(mock_state, "", {})
44+
mock_state.assign.assert_not_called()
45+
gui_core_context.select_scenario(mock_state, "scenario_a_config_id", {"args": ["var", "value"]}) # pyright: ignore[reportArgumentType]
46+
mock_state.assign.assert_called_once_with("var", "value")
47+
48+
def test_get_scenario_configs(self):
49+
gui_core_context = _GuiCoreContext(Mock())
50+
res = gui_core_context.get_scenario_configs()
51+
assert res is not None
52+
assert len(res) == 0

tools/packages/taipy-core/setup.requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
boto3>=1.29.4,<=1.38.27
22
networkx>=2.6,<=3.5
33
openpyxl>=3.1.2,<=3.1.5
4-
pandas>=1.3.5,<=2.2.3
4+
pandas>=2.2,<3; python_version >= '3.10'
5+
pandas>=1.3.5,<=2.2.3; python_version < '3.10'
56
pyarrow>=16.0.0,<=20.0.0
67
pymongo[srv]>=4.2.0,<=4.13.0
78
sqlalchemy>=2.0.16,<=2.0.41

0 commit comments

Comments
 (0)