builder.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009
  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 contextlib
  12. import json
  13. import numbers
  14. import time as _time
  15. import typing as t
  16. import xml.etree.ElementTree as etree
  17. from datetime import date, datetime, time
  18. from inspect import isclass
  19. from urllib.parse import quote
  20. from .._warnings import _warn
  21. from ..partial import Partial
  22. from ..types import PropertyType, _get_taipy_type
  23. from ..utils import (
  24. _date_to_string,
  25. _get_broadcast_var_name,
  26. _get_client_var_name,
  27. _get_data_type,
  28. _get_expr_var_name,
  29. _getscopeattr,
  30. _getscopeattr_drill,
  31. _is_boolean,
  32. _is_boolean_true,
  33. _MapDict,
  34. _to_camel_case,
  35. )
  36. from ..utils.chart_config_builder import _CHART_NAMES, _build_chart_config
  37. from ..utils.table_col_builder import _enhance_columns, _get_name_indexed_property
  38. from ..utils.types import _TaipyBase, _TaipyData, _TaipyToJson
  39. from .json import _TaipyJsonEncoder
  40. from .utils import _add_to_dict_and_get, _get_columns_dict, _get_tuple_val
  41. if t.TYPE_CHECKING:
  42. from ..gui import Gui
  43. class _Builder:
  44. """
  45. Constructs an XML node that can be rendered as a React node.
  46. This class can only be instantiated internally by Taipy.
  47. """
  48. __keys: t.Dict[str, int] = {}
  49. __BLOCK_CONTROLS = ["dialog", "expandable", "pane", "part"]
  50. __TABLE_COLUMNS_DEPS = [
  51. "data",
  52. "columns",
  53. "date_format",
  54. "number_format",
  55. "nan_value",
  56. "width",
  57. "filter",
  58. "editable",
  59. "group_by",
  60. "apply",
  61. "style",
  62. "tooltip",
  63. "lov",
  64. ]
  65. def __init__(
  66. self,
  67. gui: "Gui",
  68. control_type: str,
  69. element_name: str,
  70. attributes: t.Optional[t.Dict[str, t.Any]],
  71. hash_names: t.Dict[str, str] = None,
  72. default_value="<Empty>",
  73. lib_name: str = "taipy",
  74. ):
  75. from ..gui import Gui
  76. from .factory import _Factory
  77. if hash_names is None:
  78. hash_names = {}
  79. self.el = etree.Element(element_name)
  80. self.__control_type = control_type
  81. self.__element_name = element_name
  82. self.__lib_name = lib_name
  83. self.__attributes = attributes or {}
  84. self.__hashes = hash_names.copy()
  85. self.__update_vars: t.List[str] = []
  86. self.__gui: Gui = gui
  87. self.__default_property_name = _Factory.get_default_property_name(control_type) or ""
  88. default_property_value = self.__attributes.get(self.__default_property_name, None)
  89. if default_property_value is None and default_value is not None:
  90. self.__attributes[self.__default_property_name] = default_value
  91. # Bind properties dictionary to attributes if condition is matched (will
  92. # leave the binding for function at the builder )
  93. if "properties" in self.__attributes:
  94. (prop_dict, prop_hash) = _Builder.__parse_attribute_value(gui, self.__attributes["properties"])
  95. if prop_hash is None:
  96. prop_hash = prop_dict
  97. prop_hash = self.__gui._bind_var(prop_hash)
  98. if hasattr(self.__gui._bindings(), prop_hash):
  99. prop_dict = _getscopeattr(self.__gui, prop_hash)
  100. if isinstance(prop_dict, (dict, _MapDict)):
  101. # Iterate through prop_dict and append to self.attributes
  102. var_name, _ = gui._get_real_var_name(prop_hash)
  103. for k, v in prop_dict.items():
  104. (val, key_hash) = _Builder.__parse_attribute_value(gui, v)
  105. self.__attributes[k] = (
  106. f"{{None if ({var_name}) is None else ({var_name}).get('{k}')}}" if key_hash is None else v
  107. )
  108. else:
  109. _warn(f"{self.__control_type}.properties ({prop_hash}) must be a dict.")
  110. # Bind potential function and expressions in self.attributes
  111. self.__hashes.update(_Builder._get_variable_hash_names(gui, self.__attributes, hash_names))
  112. # set classname
  113. self.__set_class_names()
  114. # define a unique key
  115. self.set_attribute("key", _Builder._get_key(self.__element_name))
  116. @staticmethod
  117. def __parse_attribute_value(gui: "Gui", value) -> t.Tuple:
  118. if isinstance(value, str) and gui._is_expression(value):
  119. hash_value = gui._evaluate_expr(value)
  120. try:
  121. func = gui._get_user_function(hash_value)
  122. if callable(func):
  123. return (func, hash_value)
  124. return (_getscopeattr_drill(gui, hash_value), hash_value)
  125. except AttributeError:
  126. _warn(f"Expression '{value}' cannot be evaluated.")
  127. return (value, None)
  128. @staticmethod
  129. def _get_variable_hash_names(
  130. gui: "Gui", attributes: t.Dict[str, t.Any], hash_names: t.Dict[str, str] = None
  131. ) -> t.Dict[str, str]:
  132. if hash_names is None:
  133. hash_names = {}
  134. hashes = {}
  135. # Bind potential function and expressions in self.attributes
  136. for k, v in attributes.items():
  137. val = v
  138. hashname = hash_names.get(k)
  139. if hashname is None:
  140. if callable(v):
  141. if v.__name__ == "<lambda>":
  142. hashname = _get_expr_var_name(v.__code__)
  143. gui._bind_var_val(hashname, v)
  144. else:
  145. hashname = _get_expr_var_name(v.__name__)
  146. elif isinstance(v, str):
  147. # need to unescape the double quotes that were escaped during preprocessing
  148. (val, hashname) = _Builder.__parse_attribute_value(gui, v.replace('\\"', '"'))
  149. if val is not None or hashname:
  150. attributes[k] = val
  151. if hashname:
  152. hashes[k] = hashname
  153. return hashes
  154. @staticmethod
  155. def __to_string(x: t.Any) -> str:
  156. return str(x)
  157. @staticmethod
  158. def _get_key(name: str) -> str:
  159. key_index = _Builder.__keys.get(name, 0)
  160. _Builder.__keys[name] = key_index + 1
  161. return f"{name}.{key_index}"
  162. @staticmethod
  163. def _reset_key() -> None:
  164. _Builder.__keys = {}
  165. def __get_list_of_(self, name: str):
  166. lof = self.__attributes.get(name)
  167. if isinstance(lof, str):
  168. lof = list(lof.split(";"))
  169. return lof
  170. def get_name_indexed_property(self, name: str) -> t.Dict[str, t.Any]:
  171. """
  172. TODO-undocumented
  173. Returns all properties defined as <property name>[<named index>] as a dict.
  174. Arguments:
  175. name (str): The property name.
  176. """
  177. return _get_name_indexed_property(self.__attributes, name)
  178. def __get_boolean_attribute(self, name: str, default_value=False):
  179. boolattr = self.__attributes.get(name, default_value)
  180. return _is_boolean_true(boolattr) if isinstance(boolattr, str) else bool(boolattr)
  181. def set_boolean_attribute(self, name: str, value: bool):
  182. """
  183. TODO-undocumented
  184. Defines a React Boolean attribute (attr={true|false}).
  185. Arguments:
  186. name (str): The property name.
  187. value (bool): the boolean value.
  188. """
  189. return self.__set_react_attribute(_to_camel_case(name), value)
  190. def set_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None):
  191. """
  192. TODO-undocumented
  193. Defines a React attribute as a stringified json dict.
  194. The original property can be a dict or a string formed as <key 1>:<value 1>;<key 2>:<value 2>.
  195. Arguments:
  196. name (str): The property name.
  197. default value (dict): used if no value is specified.
  198. """
  199. dict_attr = self.__attributes.get(name)
  200. if dict_attr is None:
  201. dict_attr = default_value
  202. if dict_attr is not None:
  203. if isinstance(dict_attr, str):
  204. vals = [x.strip().split(":") for x in dict_attr.split(";")]
  205. dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1}
  206. if isinstance(dict_attr, (dict, _MapDict)):
  207. self.__set_json_attribute(_to_camel_case(name), dict_attr)
  208. else:
  209. _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.")
  210. return self
  211. def set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None):
  212. """
  213. TODO-undocumented
  214. Defines a React attribute as a stringified json dict.
  215. The original property can be a dict or a string formed as <key 1>:<value 1>;<key 2>:<value 2>.
  216. Arguments:
  217. name (str): The property name.
  218. default value (dict): used if no value is specified.
  219. """
  220. dict_attr = self.__attributes.get(name)
  221. if dict_attr is None:
  222. dict_attr = default_value
  223. if dict_attr is not None:
  224. if isinstance(dict_attr, str):
  225. vals = [x.strip().split(":") for x in dict_attr.split(";")]
  226. dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1}
  227. if isinstance(dict_attr, (dict, _MapDict)):
  228. self.__set_json_attribute(_to_camel_case("default_" + name), dict_attr)
  229. else:
  230. _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.")
  231. if dict_hash := self.__hashes.get(name):
  232. dict_hash = self.__get_typed_hash_name(dict_hash, PropertyType.dynamic_dict)
  233. prop_name = _to_camel_case(name)
  234. self.__update_vars.append(f"{prop_name}={dict_hash}")
  235. self.__set_react_attribute(prop_name, dict_hash)
  236. return self
  237. def __set_json_attribute(self, name, value):
  238. return self.set_attribute(name, json.dumps(value, cls=_TaipyJsonEncoder))
  239. def __set_list_of_(self, name: str):
  240. lof = self.__get_list_of_(name)
  241. if not isinstance(lof, (list, tuple)):
  242. if lof is not None:
  243. _warn(f"{self.__element_name}: {name} should be a list.")
  244. return self
  245. return self.__set_json_attribute(_to_camel_case(name), lof)
  246. def set_number_attribute(self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True):
  247. """
  248. TODO-undocumented
  249. Defines a React number attribute (attr={<number>}).
  250. Arguments:
  251. name (str): The property name.
  252. default_value (optional(str)): the default value as a string.
  253. optional (bool): Default to True, the property is required if False.
  254. """
  255. value = self.__attributes.get(name, default_value)
  256. if value is None:
  257. if not optional:
  258. _warn(f"Property {name} is required for control {self.__control_type}.")
  259. return self
  260. if isinstance(value, str):
  261. try:
  262. val = float(value)
  263. except ValueError:
  264. raise ValueError(f"Property {name} expects a number for control {self.__control_type}") from None
  265. elif isinstance(value, numbers.Number):
  266. val = value # type: ignore
  267. else:
  268. raise ValueError(
  269. f"Property {name} expects a number for control {self.__control_type}, received {type(value)}"
  270. )
  271. return self.__set_react_attribute(_to_camel_case(name), val)
  272. def __set_string_attribute(
  273. self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
  274. ):
  275. strattr = self.__attributes.get(name, default_value)
  276. if strattr is None:
  277. if not optional:
  278. _warn(f"Property {name} is required for control {self.__control_type}.")
  279. return self
  280. return self.set_attribute(_to_camel_case(name), str(strattr))
  281. def __set_dynamic_string_attribute(
  282. self,
  283. name: str,
  284. default_value: t.Optional[str] = None,
  285. with_update: t.Optional[bool] = False,
  286. dynamic_property_name: t.Optional[str] = None,
  287. ):
  288. str_val = self.__attributes.get(name, default_value)
  289. if str_val is not None:
  290. self.set_attribute(
  291. _to_camel_case(f"default_{name}" if dynamic_property_name is None else name), str(str_val)
  292. )
  293. if hash_name := self.__hashes.get(name):
  294. prop_name = _to_camel_case(name if dynamic_property_name is None else dynamic_property_name)
  295. if with_update:
  296. self.__update_vars.append(f"{prop_name}={hash_name}")
  297. self.__set_react_attribute(prop_name, hash_name)
  298. return self
  299. def __set_function_attribute(
  300. self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
  301. ):
  302. strattr = self.__attributes.get(name, default_value)
  303. if strattr is None:
  304. if not optional:
  305. _warn(f"Property {name} is required for control {self.__control_type}.")
  306. return self
  307. elif callable(strattr):
  308. strattr = self.__hashes.get(name)
  309. if strattr is None:
  310. return self
  311. elif strattr:
  312. strattr = str(strattr)
  313. func = self.__gui._get_user_function(strattr)
  314. if func == strattr:
  315. _warn(f"{self.__control_type}.{name}: {strattr} is not a function.")
  316. return self.set_attribute(_to_camel_case(name), strattr) if strattr else self
  317. def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[t.Any] = None):
  318. attr = self.__attributes.get(name, default_value)
  319. if attr is None:
  320. return self
  321. if isinstance(attr, numbers.Number):
  322. return self.__set_react_attribute(_to_camel_case(name), attr)
  323. else:
  324. return self.set_attribute(_to_camel_case(name), attr)
  325. def __set_react_attribute(self, name: str, value: t.Any):
  326. return self.set_attribute(name, "{!" + (str(value).lower() if isinstance(value, bool) else str(value)) + "!}")
  327. def _get_adapter(self, var_name: str, property_name: t.Optional[str] = None, multi_selection=True): # noqa: C901
  328. property_name = var_name if property_name is None else property_name
  329. lov = self.__get_list_of_(var_name)
  330. if isinstance(lov, list):
  331. adapter = self.__attributes.get("adapter")
  332. if adapter and isinstance(adapter, str):
  333. adapter = self.__gui._get_user_function(adapter)
  334. if adapter and not callable(adapter):
  335. _warn("'adapter' property value is invalid.")
  336. adapter = None
  337. var_type = self.__attributes.get("type")
  338. if isclass(var_type):
  339. var_type = var_type.__name__ # type: ignore
  340. if not isinstance(var_type, str):
  341. elt = None
  342. if len(lov) == 0:
  343. value = self.__attributes.get("value")
  344. if isinstance(value, list):
  345. if len(value) > 0:
  346. elt = value[0]
  347. else:
  348. elt = value
  349. else:
  350. elt = lov[0]
  351. var_type = self.__gui._get_unique_type_adapter(type(elt).__name__)
  352. if adapter is None:
  353. adapter = self.__gui._get_adapter_for_type(var_type)
  354. elif var_type == str.__name__ and callable(adapter):
  355. var_type += (
  356. _get_expr_var_name(str(adapter.__code__))
  357. if adapter.__name__ == "<lambda>"
  358. else _get_expr_var_name(adapter.__name__)
  359. )
  360. if lov_name := self.__hashes.get(var_name):
  361. if adapter is None:
  362. adapter = self.__gui._get_adapter_for_type(lov_name)
  363. else:
  364. self.__gui._add_type_for_var(lov_name, var_type)
  365. if value_name := self.__hashes.get("value"):
  366. if adapter is None:
  367. adapter = self.__gui._get_adapter_for_type(value_name)
  368. else:
  369. self.__gui._add_type_for_var(value_name, var_type)
  370. if adapter is not None:
  371. self.__gui._add_adapter_for_type(var_type, adapter) # type: ignore
  372. ret_list = []
  373. if len(lov) > 0:
  374. for elt in lov:
  375. ret = self.__gui._run_adapter(
  376. t.cast(t.Callable, adapter), elt, adapter.__name__ if callable(adapter) else "adapter"
  377. ) # type: ignore
  378. if ret is not None:
  379. ret_list.append(ret)
  380. self.__attributes[f"default_{property_name}"] = ret_list
  381. ret_list = []
  382. value = self.__attributes.get("value")
  383. val_list = value if isinstance(value, list) else [value]
  384. for val in val_list:
  385. ret = self.__gui._run_adapter(
  386. t.cast(t.Callable, adapter), val, adapter.__name__ if callable(adapter) else "adapter", id_only=True
  387. ) # type: ignore
  388. if ret is not None:
  389. ret_list.append(ret)
  390. if multi_selection:
  391. self.__set_default_value("value", ret_list)
  392. else:
  393. ret_val = ret_list[0] if len(ret_list) else ""
  394. if ret_val == "-1" and self.__attributes.get("unselected_value") is not None:
  395. ret_val = str(self.__attributes.get("unselected_value", ""))
  396. self.__set_default_value("value", ret_val)
  397. return self
  398. def __filter_attribute_names(self, names: t.Iterable[str]):
  399. return [k for k in self.__attributes if k in names or any(k.startswith(n + "[") for n in names)]
  400. def __get_holded_name(self, key: str):
  401. name = self.__hashes.get(key)
  402. if name:
  403. v = self.__attributes.get(key)
  404. if isinstance(v, _TaipyBase):
  405. return name[: len(v.get_hash()) + 1]
  406. return name
  407. def __filter_attributes_hashes(self, keys: t.List[str]):
  408. hash_names = [k for k in self.__hashes if k in keys]
  409. attr_names = [k for k in keys if k not in hash_names]
  410. return (
  411. {k: v for k, v in self.__attributes.items() if k in attr_names},
  412. {k: self.__get_holded_name(k) for k in self.__hashes if k in hash_names},
  413. )
  414. def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]):
  415. rebuild = self.__attributes.get("rebuild", False)
  416. rebuild_hash = self.__hashes.get("rebuild")
  417. if rebuild_hash or rebuild:
  418. attributes, hashes = self.__filter_attributes_hashes(self.__filter_attribute_names(attribute_names))
  419. rebuild_name = f"bool({self.__gui._get_real_var_name(rebuild_hash)[0]})" if rebuild_hash else "None"
  420. try:
  421. self.__gui._set_building(True)
  422. return self.__gui._evaluate_expr(
  423. "{"
  424. + f'{fn_name}({rebuild}, {rebuild_name}, "{quote(json.dumps(attributes))}", "{quote(json.dumps(hashes))}", {", ".join([f"{k}={v2}" for k, v2 in {v: self.__gui._get_real_var_name(v)[0] for v in hashes.values()}.items()])})' # noqa: E501
  425. + "}"
  426. )
  427. finally:
  428. self.__gui._set_building(False)
  429. return None
  430. def _get_dataframe_attributes(self) -> "_Builder":
  431. date_format = _add_to_dict_and_get(self.__attributes, "date_format", "MM/dd/yyyy")
  432. data = self.__attributes.get("data")
  433. data_hash = self.__hashes.get("data", "")
  434. col_types = self.__gui._accessors._get_col_types(data_hash, _TaipyData(data, data_hash))
  435. col_dict = _get_columns_dict(
  436. data, self.__attributes.get("columns", {}), col_types, date_format, self.__attributes.get("number_format")
  437. )
  438. rebuild_fn_hash = self.__build_rebuild_fn(
  439. self.__gui._get_rebuild_fn_name("_tbl_cols"), _Builder.__TABLE_COLUMNS_DEPS
  440. )
  441. if rebuild_fn_hash:
  442. self.__set_react_attribute("columns", rebuild_fn_hash)
  443. if col_dict is not None:
  444. _enhance_columns(self.__attributes, self.__hashes, col_dict, self.__element_name)
  445. self.__set_json_attribute("defaultColumns", col_dict)
  446. if line_style := self.__attributes.get("style"):
  447. if callable(line_style):
  448. value = self.__hashes.get("style")
  449. elif isinstance(line_style, str):
  450. value = line_style.strip()
  451. else:
  452. value = None
  453. if value in col_types.keys():
  454. _warn(f"{self.__element_name}: style={value} must not be a column name.")
  455. elif value:
  456. self.set_attribute("lineStyle", value)
  457. if tooltip := self.__attributes.get("tooltip"):
  458. if callable(tooltip):
  459. value = self.__hashes.get("tooltip")
  460. elif isinstance(tooltip, str):
  461. value = tooltip.strip()
  462. else:
  463. value = None
  464. if value in col_types.keys():
  465. _warn(f"{self.__element_name}: tooltip={value} must not be a column name.")
  466. elif value:
  467. self.set_attribute("tooltip", value)
  468. return self
  469. def _get_chart_config(self, default_type: str, default_mode: str):
  470. self.__attributes["_default_type"] = default_type
  471. self.__attributes["_default_mode"] = default_mode
  472. rebuild_fn_hash = self.__build_rebuild_fn(
  473. self.__gui._get_rebuild_fn_name("_chart_conf"), _CHART_NAMES + ("_default_type", "_default_mode", "data")
  474. )
  475. if rebuild_fn_hash:
  476. self.__set_react_attribute("config", rebuild_fn_hash)
  477. # read column definitions
  478. data = self.__attributes.get("data")
  479. data_hash = self.__hashes.get("data", "")
  480. col_types = self.__gui._accessors._get_col_types(data_hash, _TaipyData(data, data_hash))
  481. config = _build_chart_config(self.__gui, self.__attributes, col_types)
  482. self.__set_json_attribute("defaultConfig", config)
  483. self._set_chart_selected(max=len(config.get("traces", "")))
  484. self.__set_refresh_on_update()
  485. return self
  486. def _set_string_with_check(self, var_name: str, values: t.List[str], default_value: t.Optional[str] = None):
  487. value = self.__attributes.get(var_name, default_value)
  488. if value is not None:
  489. value = str(value).lower()
  490. self.__attributes[var_name] = value
  491. if value not in values:
  492. _warn(f"{self.__element_name}: {var_name}={value} should be in {values}.")
  493. else:
  494. self.__set_string_attribute(var_name, default_value)
  495. return self
  496. def __set_list_attribute(
  497. self, name: str, hash_name: t.Optional[str], val: t.Any, elt_type: t.Type, dynamic=True
  498. ) -> t.List[str]:
  499. if not hash_name and isinstance(val, str):
  500. val = [elt_type(t.strip()) for t in val.split(";")]
  501. if isinstance(val, list):
  502. if hash_name and dynamic:
  503. self.__set_react_attribute(name, hash_name)
  504. return [f"{name}={hash_name}"]
  505. else:
  506. self.__set_json_attribute(name, val)
  507. elif val is not None:
  508. _warn(f"{self.__element_name}: {name} should be a list of {elt_type}.")
  509. return []
  510. def _set_chart_selected(self, max=0):
  511. name = "selected"
  512. default_sel = self.__attributes.get(name)
  513. if not isinstance(default_sel, list) and name in self.__attributes:
  514. default_sel = []
  515. idx = 1
  516. name_idx = f"{name}[{idx}]"
  517. sel = self.__attributes.get(name_idx)
  518. if not isinstance(sel, list) and name_idx in self.__attributes:
  519. sel = []
  520. while idx <= max or name_idx in self.__attributes:
  521. if sel is not None or default_sel is not None:
  522. self.__update_vars.extend(
  523. self.__set_list_attribute(
  524. f"{name}{idx - 1}",
  525. self.__hashes.get(name_idx if sel is not None else name),
  526. sel if sel is not None else default_sel,
  527. int,
  528. )
  529. )
  530. idx += 1
  531. name_idx = f"{name}[{idx}]"
  532. sel = self.__attributes.get(name_idx)
  533. if not isinstance(sel, list) and name_idx in self.__attributes:
  534. sel = []
  535. def _get_list_attribute(self, name: str, list_type: PropertyType):
  536. varname = self.__hashes.get(name)
  537. if varname is None:
  538. list_val = self.__attributes.get(name)
  539. if isinstance(list_val, str):
  540. list_val = list(list_val.split(";"))
  541. if isinstance(list_val, list):
  542. # TODO catch the cast exception
  543. if list_type.value == PropertyType.number.value:
  544. list_val = [int(v) for v in list_val]
  545. else:
  546. list_val = [int(v) for v in list_val]
  547. else:
  548. if list_val is not None:
  549. _warn(f"{self.__element_name}: {name} should be a list.")
  550. list_val = []
  551. self.__set_react_attribute(_to_camel_case(name), list_val)
  552. else:
  553. self.__set_react_attribute(_to_camel_case(name), varname)
  554. return self
  555. def __set_class_names(self):
  556. self.set_attribute("libClassName", self.__lib_name + "-" + self.__control_type.replace("_", "-"))
  557. return self.__set_dynamic_string_attribute("class_name", dynamic_property_name="dynamic_class_name")
  558. def _set_dataType(self):
  559. value = self.__attributes.get("value")
  560. return self.set_attribute("dataType", _get_data_type(value))
  561. def _set_file_content(self, var_name: str = "content"):
  562. if hash_name := self.__hashes.get(var_name):
  563. self.__set_update_var_name(hash_name)
  564. else:
  565. _warn(f"file_selector: {var_name} should be bound.")
  566. return self
  567. def _set_content(self, var_name: str = "content", image=True):
  568. content = self.__attributes.get(var_name)
  569. hash_name = self.__hashes.get(var_name)
  570. if content is None and hash_name is None:
  571. return self
  572. value = self.__gui._get_content(hash_name or var_name, content, image)
  573. if hash_name:
  574. hash_name = self.__get_typed_hash_name(hash_name, PropertyType.image if image else PropertyType.content)
  575. if hash_name:
  576. self.__set_react_attribute(
  577. var_name,
  578. _get_client_var_name(hash_name),
  579. )
  580. return self.set_attribute(_to_camel_case(f"default_{var_name}"), value)
  581. def _set_lov(self, var_name="lov", property_name: t.Optional[str] = None):
  582. property_name = var_name if property_name is None else property_name
  583. self.__set_list_of_(f"default_{property_name}")
  584. if hash_name := self.__hashes.get(var_name):
  585. hash_name = self.__get_typed_hash_name(hash_name, PropertyType.lov)
  586. self.__update_vars.append(f"{property_name}={hash_name}")
  587. self.__set_react_attribute(property_name, hash_name)
  588. return self
  589. def __set_dynamic_string_list(self, var_name: str, default_value: t.Any):
  590. hash_name = self.__hashes.get(var_name)
  591. loi = self.__attributes.get(var_name)
  592. if loi is None:
  593. loi = default_value
  594. if isinstance(loi, str):
  595. loi = [s.strip() for s in loi.split(";") if s.strip()]
  596. if isinstance(loi, list):
  597. self.__set_json_attribute(_to_camel_case(f"default_{var_name}"), loi)
  598. if hash_name:
  599. self.__update_vars.append(f"{var_name}={hash_name}")
  600. self.__set_react_attribute(var_name, hash_name)
  601. return self
  602. def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any):
  603. hash_name = self.__hashes.get(var_name)
  604. numVal = self.__attributes.get(var_name)
  605. if numVal is None:
  606. numVal = default_value
  607. if isinstance(numVal, str):
  608. try:
  609. numVal = float(numVal)
  610. except Exception as e:
  611. _warn(f"{self.__element_name}: {var_name} cannot be transformed into a number", e)
  612. numVal = 0
  613. if isinstance(numVal, numbers.Number):
  614. self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), numVal)
  615. elif numVal is not None:
  616. _warn(f"{self.__element_name}: {var_name} value is not valid ({numVal}).")
  617. if hash_name:
  618. hash_name = self.__get_typed_hash_name(hash_name, PropertyType.number)
  619. self.__update_vars.append(f"{var_name}={hash_name}")
  620. self.__set_react_attribute(var_name, hash_name)
  621. return self
  622. def __set_default_value(
  623. self,
  624. var_name: str,
  625. value: t.Optional[t.Any] = None,
  626. native_type: bool = False,
  627. var_type: t.Optional[PropertyType] = None,
  628. ):
  629. if value is None:
  630. value = self.__attributes.get(var_name)
  631. default_var_name = _to_camel_case(f"default_{var_name}")
  632. if isinstance(value, (datetime, date, time)):
  633. return self.set_attribute(default_var_name, _date_to_string(value))
  634. elif isinstance(value, str):
  635. return self.set_attribute(default_var_name, value)
  636. elif native_type and isinstance(value, numbers.Number):
  637. return self.__set_react_attribute(default_var_name, value)
  638. elif value is None:
  639. return self.__set_react_attribute(default_var_name, "null")
  640. elif var_type == PropertyType.lov_value:
  641. # Done by _get_adapter
  642. return self
  643. elif isclass(var_type) and issubclass(var_type, _TaipyBase): # type: ignore
  644. return self.__set_default_value(var_name, t.cast(t.Callable, var_type)(value, "").get())
  645. else:
  646. return self.__set_json_attribute(default_var_name, value)
  647. def __set_update_var_name(self, hash_name: str):
  648. return self.set_attribute("updateVarName", hash_name)
  649. def set_value_and_default(
  650. self,
  651. var_name: t.Optional[str] = None,
  652. with_update=True,
  653. with_default=True,
  654. native_type=False,
  655. var_type: t.Optional[PropertyType] = None,
  656. default_val: t.Any = None,
  657. ):
  658. """
  659. TODO-undocumented
  660. Sets the value associated with the default property.
  661. Arguments:
  662. var_name (str): The property name (default to default property name).
  663. with_update (optional(bool)): Should the attribute be dynamic (default True).
  664. with_default (optional(bool)): Should a default attribute be set (default True).
  665. native_type (optional(bool)): If var_type == dynamic_number, parse the value to number.
  666. var_type (optional(PropertyType)): the property type (default to string).
  667. default_val (optional(Any)): the default value.
  668. """
  669. var_name = self.__default_property_name if var_name is None else var_name
  670. if var_type == PropertyType.slider_value or var_type == PropertyType.toggle_value:
  671. if self.__attributes.get("lov"):
  672. var_type = PropertyType.lov_value
  673. native_type = False
  674. elif var_type == PropertyType.toggle_value:
  675. self.__set_react_attribute(_to_camel_case("is_switch"), True)
  676. var_type = PropertyType.dynamic_boolean
  677. native_type = True
  678. else:
  679. var_type = (
  680. PropertyType.dynamic_lo_numbers
  681. if isinstance(self.__attributes.get("value"), list)
  682. else PropertyType.dynamic_number
  683. )
  684. native_type = True
  685. if var_type == PropertyType.dynamic_boolean:
  686. return self.set_attributes([(var_name, var_type, bool(default_val), with_update)])
  687. if hash_name := self.__hashes.get(var_name):
  688. hash_name = self.__get_typed_hash_name(hash_name, var_type)
  689. self.__set_react_attribute(
  690. _to_camel_case(var_name),
  691. _get_client_var_name(hash_name),
  692. )
  693. if with_update:
  694. self.__set_update_var_name(hash_name)
  695. if with_default:
  696. if native_type:
  697. val = self.__attributes.get(var_name)
  698. if native_type and isinstance(val, str):
  699. with contextlib.suppress(Exception):
  700. val = float(val)
  701. self.__set_default_value(var_name, val, native_type=native_type)
  702. else:
  703. self.__set_default_value(var_name, var_type=var_type)
  704. else:
  705. if var_type == PropertyType.data:
  706. _warn(f"{self.__control_type}.data property should be bound.")
  707. value = self.__attributes.get(var_name)
  708. if value is not None:
  709. if native_type:
  710. if isinstance(value, str):
  711. with contextlib.suppress(Exception):
  712. value = float(value)
  713. if isinstance(value, (int, float)):
  714. return self.__set_react_attribute(_to_camel_case(var_name), value)
  715. self.set_attribute(_to_camel_case(var_name), value)
  716. return self
  717. def _set_labels(self, var_name: str = "labels"):
  718. if value := self.__attributes.get(var_name):
  719. if _is_boolean_true(value):
  720. return self.__set_react_attribute(_to_camel_case(var_name), True)
  721. elif isinstance(value, (dict, _MapDict)):
  722. return self.set_dict_attribute(var_name)
  723. return self
  724. def _set_partial(self):
  725. if self.__control_type not in _Builder.__BLOCK_CONTROLS:
  726. return self
  727. if partial := self.__attributes.get("partial"):
  728. if self.__attributes.get("page"):
  729. _warn(f"{self.__element_name} control: page and partial should not be both defined.")
  730. if isinstance(partial, Partial):
  731. self.__attributes["page"] = partial._route
  732. self.__set_react_attribute("partial", partial._route)
  733. self.__set_react_attribute("defaultPartial", True)
  734. return self
  735. def _set_propagate(self):
  736. val = self.__get_boolean_attribute("propagate", self.__gui._config.config.get("propagate"))
  737. return self if val else self.set_boolean_attribute("propagate", False)
  738. def __set_refresh_on_update(self):
  739. if self.__update_vars:
  740. self.set_attribute("updateVars", ";".join(self.__update_vars))
  741. return self
  742. def _set_table_pagesize_options(self, default_size=None):
  743. if default_size is None:
  744. default_size = [50, 100, 500]
  745. page_size_options = self.__attributes.get("page_size_options", default_size)
  746. if isinstance(page_size_options, str):
  747. try:
  748. page_size_options = [int(s.strip()) for s in page_size_options.split(";")]
  749. except Exception as e:
  750. _warn(f"{self.__element_name}: page_size_options value is invalid ({page_size_options})", e)
  751. if isinstance(page_size_options, list):
  752. self.__set_json_attribute("pageSizeOptions", page_size_options)
  753. else:
  754. _warn(f"{self.__element_name}: page_size_options should be a list.")
  755. return self
  756. def _set_input_type(self, type_name: str, allow_password=False):
  757. if allow_password and self.__get_boolean_attribute("password", False):
  758. return self.set_attribute("type", "password")
  759. return self.set_attribute("type", type_name)
  760. def _set_kind(self):
  761. if self.__attributes.get("theme", False):
  762. self.set_attribute("mode", "theme")
  763. return self
  764. def __get_typed_hash_name(self, hash_name: str, var_type: t.Optional[PropertyType]) -> str:
  765. if taipy_type := _get_taipy_type(var_type):
  766. expr = self.__gui._get_expr_from_hash(hash_name)
  767. hash_name = self.__gui._evaluate_bind_holder(taipy_type, expr)
  768. return hash_name
  769. def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True):
  770. hash_name = self.__hashes.get(name)
  771. val = self.__get_boolean_attribute(name, def_val)
  772. default_name = f"default_{name}" if hash_name is not None else name
  773. if val != def_val:
  774. self.set_boolean_attribute(default_name, val)
  775. if hash_name is not None:
  776. hash_name = self.__get_typed_hash_name(hash_name, PropertyType.dynamic_boolean)
  777. self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name))
  778. if with_update:
  779. if update_main:
  780. self.__set_update_var_name(hash_name)
  781. else:
  782. self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}")
  783. def __set_dynamic_property_without_default(
  784. self, name: str, property_type: PropertyType, optional: t.Optional[bool] = False
  785. ):
  786. hash_name = self.__hashes.get(name)
  787. if hash_name is None:
  788. if not optional:
  789. _warn(f"{self.__element_name}.{name} should be bound.")
  790. else:
  791. hash_name = self.__get_typed_hash_name(hash_name, property_type)
  792. self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}")
  793. self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name))
  794. return self
  795. def __set_html_content(self, name: str, property_name: str, property_type: PropertyType):
  796. hash_name = self.__hashes.get(name)
  797. if not hash_name:
  798. return self
  799. front_var = self.__get_typed_hash_name(hash_name, property_type)
  800. self.set_attribute(
  801. _to_camel_case(f"default_{property_name}"),
  802. self.__gui._get_user_content_url(
  803. None,
  804. {
  805. "variable_name": front_var,
  806. self.__gui._HTML_CONTENT_KEY: str(_time.time()),
  807. },
  808. ),
  809. )
  810. return self.__set_react_attribute(_to_camel_case(property_name), _get_client_var_name(front_var))
  811. def set_attributes(self, attributes: t.List[tuple]): # noqa: C901
  812. """
  813. TODO-undocumented
  814. Sets the attributes from the property with type and default value.
  815. Arguments:
  816. attributes (list(tuple)): The list of attributes as (property name, property type, default value).
  817. """
  818. for attr in attributes:
  819. if not isinstance(attr, tuple):
  820. attr = (attr,)
  821. var_type = _get_tuple_val(attr, 1, PropertyType.string)
  822. if var_type == PropertyType.to_json:
  823. var_type = _TaipyToJson
  824. if var_type == PropertyType.boolean:
  825. def_val = _get_tuple_val(attr, 2, False)
  826. val = self.__get_boolean_attribute(attr[0], def_val)
  827. if val != def_val:
  828. self.set_boolean_attribute(attr[0], val)
  829. elif var_type == PropertyType.dynamic_boolean:
  830. self.__set_dynamic_bool_attribute(
  831. attr[0],
  832. _get_tuple_val(attr, 2, False),
  833. _get_tuple_val(attr, 3, False),
  834. _get_tuple_val(attr, 4, True),
  835. )
  836. elif var_type == PropertyType.number:
  837. self.set_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
  838. elif var_type == PropertyType.dynamic_number:
  839. self.__set_dynamic_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
  840. elif var_type == PropertyType.string:
  841. self.__set_string_attribute(attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, True))
  842. elif var_type == PropertyType.dynamic_string:
  843. self.__set_dynamic_string_attribute(
  844. attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, False)
  845. )
  846. elif var_type == PropertyType.string_list:
  847. self.__set_list_attribute(
  848. attr[0], self.__hashes.get(attr[0]), self.__attributes.get(attr[0]), str, False
  849. )
  850. elif var_type == PropertyType.function:
  851. self.__set_function_attribute(attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, True))
  852. elif var_type == PropertyType.react:
  853. prop_name = _to_camel_case(attr[0])
  854. if hash_name := self.__hashes.get(attr[0]):
  855. self.__update_vars.append(f"{prop_name}={hash_name}")
  856. self.__set_react_attribute(prop_name, hash_name)
  857. else:
  858. self.__set_react_attribute(prop_name, self.__attributes.get(attr[0], _get_tuple_val(attr, 2, None)))
  859. elif var_type == PropertyType.broadcast:
  860. self.__set_react_attribute(
  861. _to_camel_case(attr[0]), _get_broadcast_var_name(_get_tuple_val(attr, 2, None))
  862. )
  863. elif var_type == PropertyType.string_or_number:
  864. self.__set_string_or_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
  865. elif var_type == PropertyType.dict:
  866. self.set_dict_attribute(attr[0], _get_tuple_val(attr, 2, None))
  867. elif var_type == PropertyType.dynamic_dict:
  868. self.set_dynamic_dict_attribute(attr[0], _get_tuple_val(attr, 2, None))
  869. elif var_type == PropertyType.dynamic_list:
  870. self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
  871. elif var_type == PropertyType.boolean_or_list:
  872. if _is_boolean(self.__attributes.get(attr[0])):
  873. self.__set_dynamic_bool_attribute(attr[0], _get_tuple_val(attr, 2, False), True, update_main=False)
  874. else:
  875. self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
  876. elif var_type == PropertyType.data:
  877. self.__set_dynamic_property_without_default(attr[0], var_type)
  878. elif var_type == PropertyType.lov:
  879. self._get_adapter(attr[0]) # need to be called before set_lov
  880. self._set_lov(attr[0])
  881. elif var_type == PropertyType.lov_value:
  882. self.__set_dynamic_property_without_default(
  883. attr[0], var_type, _get_tuple_val(attr, 2, None) == "optional"
  884. )
  885. elif var_type == PropertyType.toHtmlContent:
  886. self.__set_html_content(attr[0], "page", var_type)
  887. elif isclass(var_type) and issubclass(var_type, _TaipyBase):
  888. if hash_name := self.__hashes.get(attr[0]):
  889. prop_name = _to_camel_case(attr[0])
  890. expr = self.__gui._get_expr_from_hash(hash_name)
  891. hash_name = self.__gui._evaluate_bind_holder(var_type, expr)
  892. self.__update_vars.append(f"{prop_name}={hash_name}")
  893. self.__set_react_attribute(prop_name, hash_name)
  894. self.__set_refresh_on_update()
  895. return self
  896. def set_attribute(self, name: str, value: t.Any):
  897. """
  898. TODO-undocumented
  899. Sets an attribute.
  900. Arguments:
  901. name (str): The name of the attribute.
  902. value (Any): The value of the attribute (must be json serializable).
  903. """
  904. self.el.set(name, value)
  905. return self
  906. def get_element(self):
  907. """
  908. TODO-undocumented
  909. Returns the xml.etree.ElementTree.Element
  910. """
  911. return self.el
  912. def _build_to_string(self):
  913. el_str = str(etree.tostring(self.el, encoding="utf8").decode("utf8"))
  914. el_str = el_str.replace("<?xml version='1.0' encoding='utf8'?>\n", "")
  915. el_str = el_str.replace("/>", ">")
  916. return el_str, self.__element_name