Skip to content

Commit 033c4de

Browse files
FredLL-AvaigaFred Lefévère-LaoideCopilot
authored
block non front-end variables (#2724)
* block non front-end variables closes #2723 * Update taipy/gui/_renderers/builder.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * tests * import Gui * let it go when in custom page context (ie designer & co.) * no need * Fab's suggestion --------- Co-authored-by: Fred Lefévère-Laoide <Fred.Lefevere-Laoide@Taipy.io> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 73cbb79 commit 033c4de

File tree

8 files changed

+110
-76
lines changed

8 files changed

+110
-76
lines changed

taipy/gui/_renderers/builder.py

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,9 @@ def __set_dynamic_any_attribute(self, name: str, default_value: t.Optional[str]
222222
if hash := self.__hashes.get(name):
223223
if isinstance(value, (dict, _MapDict)):
224224
hash = self.__get_typed_hash_name(hash, PropertyType.dynamic_dict)
225-
react_name = _to_camel_case(name)
226-
self.__update_vars.append(f"{react_name}={hash}")
227-
self.__set_react_attribute(react_name, hash)
228-
else:
229-
self.__update_vars.append(f"{name}={hash}")
230-
self.__set_react_attribute(name, hash)
225+
react_name = _to_camel_case(name)
226+
self.__update_vars.append(f"{react_name}={hash}")
227+
self.__set_react_attribute(react_name, hash)
231228
return self
232229

233230
def __get_boolean_attribute(self, name: str, default_value=False):
@@ -243,7 +240,7 @@ def __set_boolean_attribute(self, name: str, value: bool):
243240
name (str): The property name.
244241
value (bool): the boolean value.
245242
"""
246-
return self.__set_react_attribute(_to_camel_case(name), value)
243+
return self.__set_react_attribute(_to_camel_case(name), value, is_var=False)
247244

248245
def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True):
249246
value = self.__get_boolean_attribute(name, def_val)
@@ -253,12 +250,13 @@ def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: b
253250
self.__set_boolean_attribute(default_name, value)
254251
if hash is not None:
255252
hash = self.__get_typed_hash_name(hash, PropertyType.dynamic_boolean)
256-
self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash))
253+
react_name = _to_camel_case(name)
254+
self.__set_react_attribute(react_name, hash, client_var_name=True)
257255
if with_update:
258256
if update_main:
259257
self.__set_update_var_name(hash)
260258
else:
261-
self.__update_vars.append(f"{_to_camel_case(name)}={hash}")
259+
self.__update_vars.append(f"{react_name}={hash}")
262260

263261
def __set_number_attribute(
264262
self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
@@ -289,7 +287,7 @@ def __set_number_attribute(
289287
raise ValueError(
290288
f"Property {name} expects a number for control {self.__control_type}, received {type(value)}"
291289
)
292-
return self.__set_react_attribute(_to_camel_case(name), val)
290+
return self.__set_react_attribute(_to_camel_case(name), val, is_var=False)
293291

294292
def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any):
295293
value = self.__prop_values.get(var_name)
@@ -302,7 +300,7 @@ def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any):
302300
_warn(f"{self.__element_name}: {var_name} cannot be transformed into a number", e)
303301
value = 0
304302
if isinstance(value, numbers.Number):
305-
self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), value)
303+
self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), value, is_var=False)
306304
elif value is not None:
307305
_warn(f"{self.__element_name}: {var_name} value is not valid ({value}).")
308306
if hash := self.__hashes.get(var_name):
@@ -343,7 +341,7 @@ def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[
343341
if value is None:
344342
return self
345343
if isinstance(value, numbers.Number):
346-
return self.__set_react_attribute(_to_camel_case(name), value)
344+
return self.__set_react_attribute(_to_camel_case(name), value, is_var=False)
347345
else:
348346
return self.set_attribute(_to_camel_case(name), value)
349347

@@ -456,15 +454,21 @@ def __set_function_attribute(
456454
if value is None:
457455
return self
458456
elif _is_boolean(value) and not _is_true(t.cast(str, value)):
459-
return self.__set_react_attribute(_to_camel_case(name), False)
457+
return self.__set_react_attribute(_to_camel_case(name), False, is_var=False)
460458
elif value:
461459
value = str(value)
462460
func = self.__gui._get_user_function(value) # type: ignore[attr-defined]
463461
if func == value:
464462
_warn(f"{self.__control_type}.{name}: {value} is not a function.")
465463
return self.set_attribute(_to_camel_case(name), value) if value else self
466464

467-
def __set_react_attribute(self, name: str, value: t.Any):
465+
def __set_react_attribute(
466+
self, name: str, value: t.Any, is_var: t.Optional[bool] = True, client_var_name: t.Optional[bool] = False
467+
):
468+
if is_var and isinstance(value, str):
469+
self.__gui._add_front_end_variable(value)
470+
if client_var_name:
471+
value = _get_client_var_name(value)
468472
return self.set_attribute(name, "{!" + (str(value).lower() if isinstance(value, bool) else str(value)) + "!}")
469473

470474
@staticmethod
@@ -665,10 +669,7 @@ def _get_dataframe_attributes(self) -> "_Builder":
665669
self.__set_json_attribute("defaultColumns", col_dict)
666670
if cmp_hash:
667671
hash_name = self.__get_typed_hash_name(cmp_hash, PropertyType.data)
668-
self.__set_react_attribute(
669-
_to_camel_case("data"),
670-
_get_client_var_name(hash_name),
671-
)
672+
self.__set_react_attribute(_to_camel_case("data"), hash_name, client_var_name=True)
672673
self.__set_update_var_name(hash_name)
673674
self.__set_boolean_attribute("compare", True)
674675
self.__set_string_attribute("on_compare")
@@ -721,7 +722,7 @@ def _get_chart_config(self, default_type: str, default_mode: str):
721722
while add_data_hash := self.__hashes.get(name_idx):
722723
typed_hash = self.__get_typed_hash_name(add_data_hash, _TaipyData)
723724
data_updates.append(typed_hash)
724-
self.__set_react_attribute(f"data{data_idx}", _get_client_var_name(typed_hash))
725+
self.__set_react_attribute(f"data{data_idx}", typed_hash, client_var_name=True)
725726
add_data = self.__prop_values.get(name_idx)
726727
data_idx += 1
727728
name_idx = f"data[{data_idx}]"
@@ -797,12 +798,13 @@ def _get_list_attribute(self, name: str, list_type: PropertyType):
797798
else:
798799
list_val = [str(v) for v in list_val]
799800
if list_val:
800-
self.__set_react_attribute(_to_camel_case(name), list_val)
801+
self.__set_react_attribute(_to_camel_case(name), list_val, is_var=False)
801802
elif list_val is not None:
802-
_warn(f"{self.__element_name}: {name} should be a list.")
803+
_warn(f"{self.__element_name}: {name} should be a list.")
803804
else:
804-
self.__set_react_attribute(_to_camel_case(name), hash_name)
805-
self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}")
805+
react_name = _to_camel_case(name)
806+
self.__set_react_attribute(react_name, hash_name)
807+
self.__update_vars.append(f"{react_name}={hash_name}")
806808
return self
807809

808810
def __set_class_names(self):
@@ -838,10 +840,7 @@ def _set_content(self, var_name: str = "content", image=True):
838840
if hash_name:
839841
hash_name = self.__get_typed_hash_name(hash_name, PropertyType.image if image else PropertyType.content)
840842
if hash_name:
841-
self.__set_react_attribute(
842-
var_name,
843-
_get_client_var_name(hash_name),
844-
)
843+
self.__set_react_attribute(var_name, hash_name, client_var_name=True)
845844
return self.set_attribute(_to_camel_case(f"default_{var_name}"), value)
846845

847846
def __set_default_value(
@@ -859,9 +858,9 @@ def __set_default_value(
859858
elif isinstance(value, str):
860859
return self.set_attribute(default_var_name, value)
861860
elif native_type and isinstance(value, numbers.Number):
862-
return self.__set_react_attribute(default_var_name, value)
861+
return self.__set_react_attribute(default_var_name, value, is_var=False)
863862
elif value is None:
864-
return self.__set_react_attribute(default_var_name, "null")
863+
return self.__set_react_attribute(default_var_name, "null", is_var=False)
865864
elif var_type == PropertyType.lov_value:
866865
# Done by _get_adapter
867866
return self
@@ -901,7 +900,7 @@ def set_value_and_default(
901900
var_type = PropertyType.lov_value
902901
native_type = False
903902
elif var_type == PropertyType.toggle_value:
904-
self.__set_react_attribute(_to_camel_case("is_switch"), True)
903+
self.__set_react_attribute(_to_camel_case("is_switch"), True, is_var=False)
905904
var_type = PropertyType.dynamic_boolean
906905
native_type = True
907906
else:
@@ -915,10 +914,7 @@ def set_value_and_default(
915914
return self.set_attributes([(var_name, var_type, bool(default_val), with_update)])
916915
if hash_name := self.__hashes.get(var_name):
917916
hash_name = self.__get_typed_hash_name(hash_name, var_type)
918-
self.__set_react_attribute(
919-
_to_camel_case(var_name),
920-
_get_client_var_name(hash_name),
921-
)
917+
self.__set_react_attribute(_to_camel_case(var_name), hash_name, client_var_name=True)
922918
if with_update:
923919
self.__set_update_var_name(hash_name)
924920
if with_default:
@@ -940,7 +936,7 @@ def set_value_and_default(
940936
with contextlib.suppress(Exception):
941937
value = float(value)
942938
if isinstance(value, (int, float)):
943-
return self.__set_react_attribute(_to_camel_case(var_name), value)
939+
return self.__set_react_attribute(_to_camel_case(var_name), value, is_var=False)
944940
if isinstance(value, (datetime, date, time)):
945941
value = _date_to_string(value)
946942
self.set_attribute(_to_camel_case(var_name), value)
@@ -949,7 +945,7 @@ def set_value_and_default(
949945
def _set_labels(self, var_name: str = "labels"):
950946
if value := self.__prop_values.get(var_name):
951947
if _is_true(value):
952-
return self.__set_react_attribute(_to_camel_case(var_name), True)
948+
return self.__set_react_attribute(_to_camel_case(var_name), True, is_var=False)
953949
elif isinstance(value, (dict, _MapDict)):
954950
return self.__set_dict_attribute(var_name)
955951
return self
@@ -962,8 +958,8 @@ def _set_partial(self):
962958
_warn(f"{self.__element_name} control: page and partial should not be both defined.")
963959
if isinstance(partial, Partial):
964960
self.__prop_values["page"] = partial._route
965-
self.__set_react_attribute("partial", partial._route)
966-
self.__set_react_attribute("defaultPartial", True)
961+
self.__set_react_attribute("partial", partial._route, is_var=False)
962+
self.__set_react_attribute("defaultPartial", True, is_var=False)
967963
return self
968964

969965
def _set_propagate(self):
@@ -1016,7 +1012,7 @@ def __set_dynamic_property_without_default(
10161012
else:
10171013
hash_name = self.__get_typed_hash_name(hash_name, property_type)
10181014
self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}")
1019-
self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name))
1015+
self.__set_react_attribute(_to_camel_case(name), hash_name, client_var_name=True)
10201016
return self
10211017

10221018
def __set_html_content(self, name: str, property_name: str, property_type: PropertyType):
@@ -1034,7 +1030,7 @@ def __set_html_content(self, name: str, property_name: str, property_type: Prope
10341030
},
10351031
),
10361032
)
1037-
return self.__set_react_attribute(_to_camel_case(property_name), _get_client_var_name(front_var))
1033+
return self.__set_react_attribute(_to_camel_case(property_name), front_var, client_var_name=True)
10381034

10391035
def _set_indexed_icons(self, name="use_icon"):
10401036
global_icon = self.__prop_values.get(name)
@@ -1134,7 +1130,7 @@ def set_attributes(self, attributes: t.List[tuple]): # noqa: C901
11341130
self.__set_react_attribute(prop_name, hash_name)
11351131
else:
11361132
self.__set_react_attribute(
1137-
prop_name, self.__prop_values.get(attr[0], _get_tuple_val(attr, 2, None))
1133+
prop_name, self.__prop_values.get(attr[0], _get_tuple_val(attr, 2, None)), is_var=False
11381134
)
11391135
elif var_type == PropertyType.broadcast:
11401136
self.__set_react_attribute(

taipy/gui/gui.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@
8585
_hasscopeattr,
8686
_is_function,
8787
_is_in_notebook,
88-
_is_plotly_figure,
8988
_is_unnamed_function,
9089
_LocalsContext,
9190
_MapDict,
@@ -446,8 +445,11 @@ def __init__(
446445
]
447446
)
448447

448+
# Init Event Manager
449449
self.__event_manager = _EventManager()
450450

451+
self.__front_end_variables: t.Set[str] = set()
452+
451453
# Init Gui Hooks
452454
_Hooks()._init(self)
453455

@@ -1182,9 +1184,13 @@ def __send_var_list_update( # noqa C901
11821184
modified_vars.remove(v.get_name())
11831185
elif isinstance(v, _DoNotUpdate):
11841186
modified_vars.remove(k)
1187+
custom_page_filtered_types = _Hooks()._get_resource_handler_data_layer_supported_types()
1188+
in_custom_page_context = _Hooks()._is_in_custom_page_context()
11851189
for _var in modified_vars:
1190+
if not self.__is_front_end_variable(_var) and not in_custom_page_context:
1191+
_TaipyLogger._get_logger().debug(f"Skipping variable '{_var}' not in front-end.")
1192+
continue
11861193
newvalue = values.get(_var)
1187-
custom_page_filtered_types = _Hooks()._get_resource_handler_data_layer_supported_types()
11881194
if isinstance(newvalue, (_TaipyData)) or (
11891195
custom_page_filtered_types and isinstance(newvalue, custom_page_filtered_types)
11901196
): # type: ignore
@@ -1211,9 +1217,7 @@ def __send_var_list_update( # noqa C901
12111217
is_json = isinstance(newvalue, _TaipyToJson)
12121218
newvalue = newvalue.get()
12131219
# Skip in taipy-gui, available in custom frontend
1214-
if isinstance(newvalue, (dict, _MapDict)) and not _Hooks()._is_in_custom_page_context():
1215-
continue
1216-
if _is_plotly_figure(newvalue):
1220+
if isinstance(newvalue, (dict, _MapDict)) and not in_custom_page_context:
12171221
continue
12181222
if isinstance(newvalue, float) and math.isnan(newvalue):
12191223
# do not let NaN go through json, it is not handle well (dies silently through websocket)
@@ -3107,3 +3111,9 @@ def __do_fire_event(
31073111
finally:
31083112
if this_sid:
31093113
get_server_request_accessor(self).set_sid(this_sid)
3114+
3115+
def _add_front_end_variable(self, var_name: str):
3116+
self.__front_end_variables.add(var_name)
3117+
3118+
def __is_front_end_variable(self, var_name: str) -> bool:
3119+
return var_name in self.__front_end_variables
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
13+
from taipy.gui import Gui, Markdown
14+
15+
16+
def test_variable_binding(helpers, gui_server):
17+
x = "a string" # noqa: F841
18+
gui = Gui(server=gui_server)
19+
gui.add_page("test", Markdown("<|{not x}|>"))
20+
gui.run(run_server=False, single_client=True)
21+
client = gui._server.test_client()
22+
client.get(f"/{Gui._JSX_URL}/test")
23+
fv_set = gui._Gui__front_end_variables # type: ignore[reportAttributeAccessIssue]
24+
assert len(fv_set) == 1
25+
assert "x" not in fv_set
26+
helpers.test_cleanup()

tests/gui/gui_specific/test_multiple_instances.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_multiple_instance(gui_server, helpers):
2525

2626

2727
def assert_multiple_instance(gui, client, helpers, expected_value):
28-
response = client.get("/taipy-jsx/TaiPy_root_page")
28+
response = client.get(f"/{Gui._JSX_URL}/TaiPy_root_page")
2929
response_data = helpers.get_response_data(response, gui)
3030
assert response.status_code == 200
3131
assert isinstance(response_data, dict)

tests/gui/gui_specific/test_run_thread.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ def test_run_thread(gui: Gui, helpers):
2323
gui.run(run_in_thread=True, run_browser=False)
2424
while not helpers.port_check():
2525
time.sleep(0.1)
26-
assert ">first page</h1>" in urlopen("http://127.0.0.1:5000/taipy-jsx/page1").read().decode("utf-8")
26+
assert ">first page</h1>" in urlopen(f"http://127.0.0.1:5000/{Gui._JSX_URL}/page1").read().decode("utf-8")
2727
gui.stop()
2828
while helpers.port_check():
2929
time.sleep(0.1)
3030
gui.run(run_in_thread=True, run_browser=False)
3131
while not helpers.port_check():
3232
time.sleep(0.1)
33-
assert ">first page</h1>" in urlopen("http://127.0.0.1:5000/taipy-jsx/page1").read().decode("utf-8")
33+
assert ">first page</h1>" in urlopen(f"http://127.0.0.1:5000/{Gui._JSX_URL}/page1").read().decode("utf-8")

tests/gui/gui_specific/test_variable_binding.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ def another_function(gui):
2828
gui.add_page("test", Markdown("<|{x}|> | <|{y}|> | <|{z}|button|on_action=another_function|>"))
2929
gui.run(run_server=False, single_client=True)
3030
client = gui._server.test_client()
31-
response = client.get("/taipy-jsx/test")
31+
response = client.get(f"/{Gui._JSX_URL}/test")
3232
jsx = helpers.get_response_data(response, gui)["jsx"]
3333
for expected in ["<Button", 'defaultLabel="button label"', "label={tpec_TpExPr_z_TPMDL_0}"]:
3434
assert expected in jsx
35-
assert gui._bindings().x == x
36-
assert gui._bindings().y == y
37-
assert gui._bindings().z == z
35+
assert gui._bindings().x == x # type: ignore[reportAttributeAccessIssue]
36+
assert gui._bindings().y == y # type: ignore[reportAttributeAccessIssue]
37+
assert gui._bindings().z == z # type: ignore[reportAttributeAccessIssue]
3838
with gui.get_app_context():
3939
assert callable(gui._get_user_function("another_function"))
4040
helpers.test_cleanup()
@@ -47,7 +47,7 @@ def test_properties_binding(helpers, gui_server):
4747
gui.add_page("test", Markdown("<|button|properties=button_properties|>"))
4848
gui.run(run_server=False)
4949
client = gui._server.test_client()
50-
response = client.get("/taipy-jsx/test")
50+
response = client.get(f"/{Gui._JSX_URL}/test")
5151
jsx = helpers.get_response_data(response, gui)["jsx"]
5252
for expected in ["<Button", 'defaultLabel="A nice button"']:
5353
assert expected in jsx
@@ -62,7 +62,7 @@ def test_dict_binding(helpers, gui_server):
6262
gui = Gui("<|{d.k}|>", server=gui_server)
6363
gui.run(run_server=False)
6464
client = gui._server.test_client()
65-
response = client.get("/taipy-jsx/TaiPy_root_page")
65+
response = client.get(f"/{Gui._JSX_URL}/TaiPy_root_page")
6666
jsx = helpers.get_response_data(response, gui)["jsx"]
6767
for expected in ["<Field", 'defaultValue="test"']:
6868
assert expected in jsx

0 commit comments

Comments
 (0)