builder.py 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096
  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_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.Optional[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.Optional[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 = f"__lambda_{id(v)}"
  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. if not isinstance(lof, list) and hasattr(lof, "tolist"):
  170. try:
  171. return lof.tolist() # type: ignore[union-attr]
  172. except Exception as e:
  173. _warn("Error accessing List of values", e)
  174. return lof
  175. def get_name_indexed_property(self, name: str) -> t.Dict[str, t.Any]:
  176. """
  177. TODO-undocumented
  178. Returns all properties defined as <property name>[<named index>] as a dict.
  179. Arguments:
  180. name (str): The property name.
  181. """
  182. return _get_name_indexed_property(self.__attributes, name)
  183. def __get_boolean_attribute(self, name: str, default_value=False):
  184. boolattr = self.__attributes.get(name, default_value)
  185. return _is_true(boolattr) if isinstance(boolattr, str) else bool(boolattr)
  186. def set_boolean_attribute(self, name: str, value: bool):
  187. """
  188. TODO-undocumented
  189. Defines a React Boolean attribute (attr={true|false}).
  190. Arguments:
  191. name (str): The property name.
  192. value (bool): the boolean value.
  193. """
  194. return self.__set_react_attribute(_to_camel_case(name), value)
  195. def set_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None):
  196. """
  197. TODO-undocumented
  198. Defines a React attribute as a stringified json dict.
  199. The original property can be a dict or a string formed as <key 1>:<value 1>;<key 2>:<value 2>.
  200. Arguments:
  201. name (str): The property name.
  202. default value (dict): used if no value is specified.
  203. """
  204. dict_attr = self.__attributes.get(name)
  205. if dict_attr is None:
  206. dict_attr = default_value
  207. if dict_attr is not None:
  208. if isinstance(dict_attr, str):
  209. vals = [x.strip().split(":") for x in dict_attr.split(";")]
  210. dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1}
  211. if isinstance(dict_attr, (dict, _MapDict)):
  212. self.__set_json_attribute(_to_camel_case(name), dict_attr)
  213. else:
  214. _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.")
  215. return self
  216. def set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None):
  217. """
  218. TODO-undocumented
  219. Defines a React attribute as a stringified json dict.
  220. The original property can be a dict or a string formed as <key 1>:<value 1>;<key 2>:<value 2>.
  221. Arguments:
  222. name (str): The property name.
  223. default value (dict): used if no value is specified.
  224. """
  225. dict_attr = self.__attributes.get(name)
  226. if dict_attr is None:
  227. dict_attr = default_value
  228. if dict_attr is not None:
  229. if isinstance(dict_attr, str):
  230. vals = [x.strip().split(":") for x in dict_attr.split(";")]
  231. dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1}
  232. if isinstance(dict_attr, (dict, _MapDict)):
  233. self.__set_json_attribute(_to_camel_case("default_" + name), dict_attr)
  234. else:
  235. _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.")
  236. if dict_hash := self.__hashes.get(name):
  237. dict_hash = self.__get_typed_hash_name(dict_hash, PropertyType.dynamic_dict)
  238. prop_name = _to_camel_case(name)
  239. self.__update_vars.append(f"{prop_name}={dict_hash}")
  240. self.__set_react_attribute(prop_name, dict_hash)
  241. return self
  242. def __set_json_attribute(self, name, value):
  243. return self.set_attribute(name, json.dumps(value, cls=_TaipyJsonEncoder))
  244. def set_number_attribute(self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True):
  245. """
  246. TODO-undocumented
  247. Defines a React number attribute (attr={<number>}).
  248. Arguments:
  249. name (str): The property name.
  250. default_value (optional(str)): the default value as a string.
  251. optional (bool): Default to True, the property is required if False.
  252. """
  253. value = self.__attributes.get(name, default_value)
  254. if value is None:
  255. if not optional:
  256. _warn(f"Property {name} is required for control {self.__control_type}.")
  257. return self
  258. if isinstance(value, str):
  259. try:
  260. val = float(value)
  261. except ValueError:
  262. raise ValueError(f"Property {name} expects a number for control {self.__control_type}") from None
  263. elif isinstance(value, numbers.Number):
  264. val = value # type: ignore
  265. else:
  266. raise ValueError(
  267. f"Property {name} expects a number for control {self.__control_type}, received {type(value)}"
  268. )
  269. return self.__set_react_attribute(_to_camel_case(name), val)
  270. def __set_string_attribute(
  271. self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
  272. ):
  273. strattr = self.__attributes.get(name, default_value)
  274. if strattr is None:
  275. if not optional:
  276. _warn(f"Property {name} is required for control {self.__control_type}.")
  277. return self
  278. return self.set_attribute(_to_camel_case(name), str(strattr))
  279. def __set_dynamic_date_attribute(self, var_name: str, default_value: t.Optional[str] = None):
  280. date_attr = self.__attributes.get(var_name, default_value)
  281. if date_attr is None:
  282. date_attr = default_value
  283. if isinstance(date_attr, (datetime, date, time)):
  284. value = _date_to_string(date_attr)
  285. self.set_attribute(_to_camel_case(var_name), value)
  286. return self
  287. def __set_dynamic_string_attribute(
  288. self,
  289. name: str,
  290. default_value: t.Optional[str] = None,
  291. with_update: t.Optional[bool] = False,
  292. dynamic_property_name: t.Optional[str] = None,
  293. ):
  294. str_val = self.__attributes.get(name, default_value)
  295. if str_val is not None:
  296. self.set_attribute(
  297. _to_camel_case(f"default_{name}" if dynamic_property_name is None else name), str(str_val)
  298. )
  299. if hash_name := self.__hashes.get(name):
  300. prop_name = _to_camel_case(name if dynamic_property_name is None else dynamic_property_name)
  301. if with_update:
  302. self.__update_vars.append(f"{prop_name}={hash_name}")
  303. self.__set_react_attribute(prop_name, hash_name)
  304. return self
  305. def __set_function_attribute(
  306. self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
  307. ):
  308. strattr = self.__attributes.get(name, default_value)
  309. if strattr is None:
  310. if not optional:
  311. _warn(f"Property {name} is required for control {self.__control_type}.")
  312. return self
  313. elif callable(strattr):
  314. strattr = self.__hashes.get(name)
  315. if strattr is None:
  316. return self
  317. elif _is_boolean(strattr) and not _is_true(strattr):
  318. return self.__set_react_attribute(_to_camel_case(name), False)
  319. elif strattr:
  320. strattr = str(strattr)
  321. func = self.__gui._get_user_function(strattr)
  322. if func == strattr:
  323. _warn(f"{self.__control_type}.{name}: {strattr} is not a function.")
  324. return self.set_attribute(_to_camel_case(name), strattr) if strattr else self
  325. def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[t.Any] = None):
  326. attr = self.__attributes.get(name, default_value)
  327. if attr is None:
  328. return self
  329. if isinstance(attr, numbers.Number):
  330. return self.__set_react_attribute(_to_camel_case(name), attr)
  331. else:
  332. return self.set_attribute(_to_camel_case(name), attr)
  333. def __set_react_attribute(self, name: str, value: t.Any):
  334. return self.set_attribute(name, "{!" + (str(value).lower() if isinstance(value, bool) else str(value)) + "!}")
  335. def _get_lov_adapter( # noqa: C901
  336. self, var_name: str, property_name: t.Optional[str] = None, multi_selection=True, with_default=True
  337. ):
  338. property_name = var_name if property_name is None else property_name
  339. lov_name = self.__hashes.get(var_name)
  340. lov = self.__get_list_of_(var_name)
  341. default_lov: t.Optional[t.List[t.Any]] = [] if with_default or not lov_name else None
  342. adapter = self.__attributes.get("adapter")
  343. if adapter and isinstance(adapter, str):
  344. adapter = self.__gui._get_user_function(adapter)
  345. if adapter and not callable(adapter):
  346. _warn(f"{self.__element_name}: adapter property value is invalid.")
  347. adapter = None
  348. var_type = self.__attributes.get("type")
  349. if isclass(var_type):
  350. var_type = var_type.__name__ # type: ignore
  351. if isinstance(lov, list):
  352. if not isinstance(var_type, str):
  353. elt = None
  354. if lov:
  355. elt = lov[0]
  356. else:
  357. value = self.__attributes.get("value")
  358. if isinstance(value, list):
  359. if len(value) > 0:
  360. elt = value[0]
  361. else:
  362. elt = value
  363. var_type = self.__gui._get_unique_type_adapter(type(elt).__name__)
  364. if adapter is None:
  365. adapter = self.__gui._get_adapter_for_type(var_type)
  366. elif var_type == str.__name__ and callable(adapter):
  367. var_type += (
  368. f"__lambda_{id(adapter)}"
  369. if adapter.__name__ == "<lambda>"
  370. else _get_expr_var_name(adapter.__name__)
  371. )
  372. if lov_name:
  373. if adapter is None:
  374. adapter = self.__gui._get_adapter_for_type(lov_name)
  375. else:
  376. self.__gui._add_type_for_var(lov_name, var_type)
  377. if value_name := self.__hashes.get("value"):
  378. if adapter is None:
  379. adapter = self.__gui._get_adapter_for_type(value_name)
  380. else:
  381. self.__gui._add_type_for_var(value_name, var_type)
  382. if adapter is not None:
  383. self.__gui._add_adapter_for_type(var_type, adapter) # type: ignore
  384. if default_lov is not None and lov:
  385. for elt in lov:
  386. ret = self.__gui._run_adapter(
  387. t.cast(t.Callable, adapter), elt, adapter.__name__ if callable(adapter) else "adapter"
  388. ) # type: ignore
  389. if ret is not None:
  390. default_lov.append(ret)
  391. ret_list = []
  392. value = self.__attributes.get("value")
  393. val_list = value if isinstance(value, list) else [value]
  394. for val in val_list:
  395. ret = self.__gui._run_adapter(
  396. t.cast(t.Callable, adapter), val, adapter.__name__ if callable(adapter) else "adapter", id_only=True
  397. ) # type: ignore
  398. if ret is not None:
  399. ret_list.append(ret)
  400. if multi_selection:
  401. self.__set_default_value("value", ret_list)
  402. else:
  403. ret_val = ret_list[0] if len(ret_list) else ""
  404. if ret_val == "-1" and self.__attributes.get("unselected_value") is not None:
  405. ret_val = str(self.__attributes.get("unselected_value", ""))
  406. self.__set_default_value("value", ret_val)
  407. # LoV default value
  408. if default_lov is not None:
  409. self.__set_json_attribute(_to_camel_case(f"default_{property_name}"), default_lov)
  410. # LoV expression binding
  411. if lov_name:
  412. typed_lov_hash = (
  413. self.__gui._evaluate_expr(
  414. "{"
  415. + f"{self.__gui._get_call_method_name('_get_adapted_lov')}"
  416. + f"({self.__gui._get_real_var_name(lov_name)[0]},'{var_type}')"
  417. + "}"
  418. )
  419. if var_type
  420. else lov_name
  421. )
  422. hash_name = self.__get_typed_hash_name(typed_lov_hash, PropertyType.lov)
  423. camel_prop = _to_camel_case(property_name)
  424. self.__update_vars.append(f"{camel_prop}={hash_name}")
  425. self.__set_react_attribute(camel_prop, hash_name)
  426. return self
  427. def __filter_attribute_names(self, names: t.Iterable[str]):
  428. return [k for k in self.__attributes if k in names or any(k.startswith(n + "[") for n in names)]
  429. def __get_held_name(self, key: str):
  430. name = self.__hashes.get(key)
  431. if name:
  432. v = self.__attributes.get(key)
  433. if isinstance(v, _TaipyBase):
  434. return name[: len(v.get_hash()) + 1]
  435. return name
  436. def __filter_attributes_hashes(self, keys: t.List[str]):
  437. hash_names = [k for k in self.__hashes if k in keys]
  438. attr_names = [k for k in keys if k not in hash_names]
  439. return (
  440. {k: v for k, v in self.__attributes.items() if k in attr_names},
  441. {k: self.__get_held_name(k) for k in self.__hashes if k in hash_names},
  442. )
  443. def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]):
  444. rebuild = self.__attributes.get("rebuild", False)
  445. rebuild_hash = self.__hashes.get("rebuild")
  446. if rebuild_hash or _is_true(rebuild):
  447. attributes, hashes = self.__filter_attributes_hashes(self.__filter_attribute_names(attribute_names))
  448. rebuild_name = f"bool({self.__gui._get_real_var_name(rebuild_hash)[0]})" if rebuild_hash else "None"
  449. try:
  450. self.__gui._set_building(True)
  451. return self.__gui._evaluate_expr(
  452. "{"
  453. + 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
  454. + "}"
  455. )
  456. finally:
  457. self.__gui._set_building(False)
  458. return None
  459. def _get_dataframe_attributes(self) -> "_Builder":
  460. date_format = _add_to_dict_and_get(self.__attributes, "date_format", "MM/dd/yyyy")
  461. data = self.__attributes.get("data")
  462. data_hash = self.__hashes.get("data", "")
  463. cmp_hash = ""
  464. if data_hash:
  465. cmp_idx = 1
  466. cmp_datas = []
  467. cmp_datas_hash = []
  468. while cmp_data := self.__hashes.get(f"data[{cmp_idx}]"):
  469. cmp_idx += 1
  470. cmp_datas.append(self.__gui._get_real_var_name(cmp_data)[0])
  471. cmp_datas_hash.append(cmp_data)
  472. if cmp_datas:
  473. cmp_hash = self.__gui._evaluate_expr(
  474. "{"
  475. + f"{self.__gui._get_call_method_name('_compare_data')}"
  476. + f'({self.__gui._get_real_var_name(data_hash)[0]},{",".join(cmp_datas)})'
  477. + "}"
  478. )
  479. self.__update_vars.append(f"comparedatas={','.join(cmp_datas_hash)}")
  480. col_types = self.__gui._get_accessor().get_col_types(data_hash, _TaipyData(data, data_hash))
  481. col_dict = _get_columns_dict(
  482. data, self.__attributes.get("columns", {}), col_types, date_format, self.__attributes.get("number_format")
  483. )
  484. rebuild_fn_hash = self.__build_rebuild_fn(
  485. self.__gui._get_call_method_name("_tbl_cols"), _Builder.__TABLE_COLUMNS_DEPS
  486. )
  487. if rebuild_fn_hash:
  488. self.__set_react_attribute("columns", rebuild_fn_hash)
  489. if col_dict is not None:
  490. _enhance_columns(self.__attributes, self.__hashes, col_dict, self.__element_name)
  491. self.__set_json_attribute("defaultColumns", col_dict)
  492. if cmp_hash:
  493. hash_name = self.__get_typed_hash_name(cmp_hash, PropertyType.data)
  494. self.__set_react_attribute(
  495. _to_camel_case("data"),
  496. _get_client_var_name(hash_name),
  497. )
  498. self.__set_update_var_name(hash_name)
  499. self.set_boolean_attribute("compare", True)
  500. self.__set_string_attribute("on_compare")
  501. if line_style := self.__attributes.get("style"):
  502. if callable(line_style):
  503. value = self.__hashes.get("style")
  504. elif isinstance(line_style, str):
  505. value = line_style.strip()
  506. else:
  507. value = None
  508. if value in col_types.keys():
  509. _warn(f"{self.__element_name}: style={value} must not be a column name.")
  510. elif value:
  511. self.set_attribute("lineStyle", value)
  512. if tooltip := self.__attributes.get("tooltip"):
  513. if callable(tooltip):
  514. value = self.__hashes.get("tooltip")
  515. elif isinstance(tooltip, str):
  516. value = tooltip.strip()
  517. else:
  518. value = None
  519. if value in col_types.keys():
  520. _warn(f"{self.__element_name}: tooltip={value} must not be a column name.")
  521. elif value:
  522. self.set_attribute("tooltip", value)
  523. return self
  524. def _get_chart_config(self, default_type: str, default_mode: str):
  525. self.__attributes["_default_type"] = default_type
  526. self.__attributes["_default_mode"] = default_mode
  527. rebuild_fn_hash = self.__build_rebuild_fn(
  528. self.__gui._get_call_method_name("_chart_conf"),
  529. _CHART_NAMES + ("_default_type", "_default_mode", "data"),
  530. )
  531. if rebuild_fn_hash:
  532. self.__set_react_attribute("config", rebuild_fn_hash)
  533. # read column definitions
  534. data = self.__attributes.get("data")
  535. data_hash = self.__hashes.get("data", "")
  536. col_types = self.__gui._get_accessor().get_col_types(data_hash, _TaipyData(data, data_hash))
  537. config = _build_chart_config(self.__gui, self.__attributes, col_types)
  538. self.__set_json_attribute("defaultConfig", config)
  539. self._set_chart_selected(max=len(config.get("traces", "")))
  540. self.__set_refresh_on_update()
  541. return self
  542. def _set_string_with_check(self, var_name: str, values: t.List[str], default_value: t.Optional[str] = None):
  543. value = self.__attributes.get(var_name, default_value)
  544. if value is not None:
  545. value = str(value).lower()
  546. self.__attributes[var_name] = value
  547. if value not in values:
  548. _warn(f"{self.__element_name}: {var_name}={value} should be in {values}.")
  549. else:
  550. self.__set_string_attribute(var_name, default_value)
  551. return self
  552. def __set_list_attribute(
  553. self,
  554. name: str,
  555. hash_name: t.Optional[str],
  556. val: t.Any,
  557. elt_type: t.Type,
  558. dynamic=True,
  559. default_val: t.Optional[t.Any] = None,
  560. ) -> t.List[str]:
  561. val = default_val if val is None else val
  562. if not hash_name and isinstance(val, str):
  563. val = [elt_type(t.strip()) for t in val.split(";")]
  564. if isinstance(val, list):
  565. if hash_name and dynamic:
  566. self.__set_react_attribute(name, hash_name)
  567. return [f"{name}={hash_name}"]
  568. else:
  569. self.__set_json_attribute(name, val)
  570. elif val is not None:
  571. _warn(f"{self.__element_name}: {name} should be a list of {elt_type}.")
  572. return []
  573. def _set_chart_selected(self, max=0):
  574. name = "selected"
  575. default_sel = self.__attributes.get(name)
  576. if not isinstance(default_sel, list) and name in self.__attributes:
  577. default_sel = []
  578. idx = 1
  579. name_idx = f"{name}[{idx}]"
  580. sel = self.__attributes.get(name_idx)
  581. if not isinstance(sel, list) and name_idx in self.__attributes:
  582. sel = []
  583. while idx <= max or name_idx in self.__attributes:
  584. if sel is not None or default_sel is not None:
  585. self.__update_vars.extend(
  586. self.__set_list_attribute(
  587. f"{name}{idx - 1}",
  588. self.__hashes.get(name_idx if sel is not None else name),
  589. sel if sel is not None else default_sel,
  590. int,
  591. )
  592. )
  593. idx += 1
  594. name_idx = f"{name}[{idx}]"
  595. sel = self.__attributes.get(name_idx)
  596. if not isinstance(sel, list) and name_idx in self.__attributes:
  597. sel = []
  598. def _get_list_attribute(self, name: str, list_type: PropertyType):
  599. hash_name = self.__hashes.get(name)
  600. if hash_name is None:
  601. list_val = self.__attributes.get(name)
  602. if isinstance(list_val, str):
  603. list_val = list(list_val.split(";"))
  604. if isinstance(list_val, list):
  605. # TODO catch the cast exception
  606. if list_type.value == PropertyType.number.value:
  607. list_val = [int(v) for v in list_val]
  608. else:
  609. list_val = [int(v) for v in list_val]
  610. else:
  611. if list_val is not None:
  612. _warn(f"{self.__element_name}: {name} should be a list.")
  613. list_val = []
  614. self.__set_react_attribute(_to_camel_case(name), list_val)
  615. else:
  616. self.__set_react_attribute(_to_camel_case(name), hash_name)
  617. self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}")
  618. return self
  619. def __set_class_names(self):
  620. self.set_attribute("libClassName", self.__lib_name + "-" + self.__control_type.replace("_", "-"))
  621. return self.__set_dynamic_string_attribute("class_name", dynamic_property_name="dynamic_class_name")
  622. def _set_dataType(self):
  623. value = self.__attributes.get("value")
  624. return self.set_attribute("dataType", _get_data_type(value))
  625. def _set_file_content(self, var_name: str = "content"):
  626. if hash_name := self.__hashes.get(var_name):
  627. self.__set_update_var_name(hash_name)
  628. else:
  629. _warn(f"file_selector: {var_name} should be bound.")
  630. return self
  631. def _set_content(self, var_name: str = "content", image=True):
  632. content = self.__attributes.get(var_name)
  633. hash_name = self.__hashes.get(var_name)
  634. if content is None and hash_name is None:
  635. return self
  636. value = self.__gui._get_content(hash_name or var_name, content, image)
  637. if hash_name:
  638. hash_name = self.__get_typed_hash_name(hash_name, PropertyType.image if image else PropertyType.content)
  639. if hash_name:
  640. self.__set_react_attribute(
  641. var_name,
  642. _get_client_var_name(hash_name),
  643. )
  644. return self.set_attribute(_to_camel_case(f"default_{var_name}"), value)
  645. def __set_dynamic_string_list(self, var_name: str, default_value: t.Any):
  646. hash_name = self.__hashes.get(var_name)
  647. loi = self.__attributes.get(var_name)
  648. if loi is None:
  649. loi = default_value
  650. if isinstance(loi, str):
  651. loi = [s.strip() for s in loi.split(";") if s.strip()]
  652. if isinstance(loi, list):
  653. self.__set_json_attribute(_to_camel_case(f"default_{var_name}"), loi)
  654. if hash_name:
  655. self.__update_vars.append(f"{var_name}={hash_name}")
  656. self.__set_react_attribute(var_name, hash_name)
  657. return self
  658. def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any):
  659. hash_name = self.__hashes.get(var_name)
  660. numVal = self.__attributes.get(var_name)
  661. if numVal is None:
  662. numVal = default_value
  663. if isinstance(numVal, str):
  664. try:
  665. numVal = float(numVal)
  666. except Exception as e:
  667. _warn(f"{self.__element_name}: {var_name} cannot be transformed into a number", e)
  668. numVal = 0
  669. if isinstance(numVal, numbers.Number):
  670. self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), numVal)
  671. elif numVal is not None:
  672. _warn(f"{self.__element_name}: {var_name} value is not valid ({numVal}).")
  673. if hash_name:
  674. hash_name = self.__get_typed_hash_name(hash_name, PropertyType.number)
  675. self.__update_vars.append(f"{var_name}={hash_name}")
  676. self.__set_react_attribute(var_name, hash_name)
  677. return self
  678. def __set_default_value(
  679. self,
  680. var_name: str,
  681. value: t.Optional[t.Any] = None,
  682. native_type: bool = False,
  683. var_type: t.Optional[PropertyType] = None,
  684. ):
  685. if value is None:
  686. value = self.__attributes.get(var_name)
  687. default_var_name = _to_camel_case(f"default_{var_name}")
  688. if isinstance(value, (datetime, date, time)):
  689. return self.set_attribute(default_var_name, _date_to_string(value))
  690. elif isinstance(value, str):
  691. return self.set_attribute(default_var_name, value)
  692. elif native_type and isinstance(value, numbers.Number):
  693. return self.__set_react_attribute(default_var_name, value)
  694. elif value is None:
  695. return self.__set_react_attribute(default_var_name, "null")
  696. elif var_type == PropertyType.lov_value:
  697. # Done by _get_adapter
  698. return self
  699. elif isclass(var_type) and issubclass(var_type, _TaipyBase): # type: ignore
  700. return self.__set_default_value(var_name, t.cast(t.Callable, var_type)(value, "").get())
  701. else:
  702. return self.__set_json_attribute(default_var_name, value)
  703. def __set_update_var_name(self, hash_name: str):
  704. return self.set_attribute("updateVarName", hash_name)
  705. def set_value_and_default(
  706. self,
  707. var_name: t.Optional[str] = None,
  708. with_update=True,
  709. with_default=True,
  710. native_type=False,
  711. var_type: t.Optional[PropertyType] = None,
  712. default_val: t.Any = None,
  713. ):
  714. """
  715. TODO-undocumented
  716. Sets the value associated with the default property.
  717. Arguments:
  718. var_name (str): The property name (default to default property name).
  719. with_update (optional(bool)): Should the attribute be dynamic (default True).
  720. with_default (optional(bool)): Should a default attribute be set (default True).
  721. native_type (optional(bool)): If var_type == dynamic_number, parse the value to number.
  722. var_type (optional(PropertyType)): the property type (default to string).
  723. default_val (optional(Any)): the default value.
  724. """
  725. var_name = self.__default_property_name if var_name is None else var_name
  726. if var_type == PropertyType.slider_value or var_type == PropertyType.toggle_value:
  727. if self.__attributes.get("lov"):
  728. var_type = PropertyType.lov_value
  729. native_type = False
  730. elif var_type == PropertyType.toggle_value:
  731. self.__set_react_attribute(_to_camel_case("is_switch"), True)
  732. var_type = PropertyType.dynamic_boolean
  733. native_type = True
  734. else:
  735. var_type = (
  736. PropertyType.dynamic_lo_numbers
  737. if isinstance(self.__attributes.get("value"), list)
  738. else PropertyType.dynamic_number
  739. )
  740. native_type = True
  741. if var_type == PropertyType.dynamic_boolean:
  742. return self.set_attributes([(var_name, var_type, bool(default_val), with_update)])
  743. if hash_name := self.__hashes.get(var_name):
  744. hash_name = self.__get_typed_hash_name(hash_name, var_type)
  745. self.__set_react_attribute(
  746. _to_camel_case(var_name),
  747. _get_client_var_name(hash_name),
  748. )
  749. if with_update:
  750. self.__set_update_var_name(hash_name)
  751. if with_default:
  752. if native_type:
  753. val = self.__attributes.get(var_name)
  754. if native_type and isinstance(val, str):
  755. with contextlib.suppress(Exception):
  756. val = float(val)
  757. self.__set_default_value(var_name, val, native_type=native_type)
  758. else:
  759. self.__set_default_value(var_name, var_type=var_type)
  760. else:
  761. if var_type == PropertyType.data and (self.__control_type != "chart" or "figure" not in self.__attributes):
  762. _warn(f"{self.__control_type}.{var_name} property should be bound.")
  763. value = self.__attributes.get(var_name)
  764. if value is not None:
  765. if native_type:
  766. if isinstance(value, str):
  767. with contextlib.suppress(Exception):
  768. value = float(value)
  769. if isinstance(value, (int, float)):
  770. return self.__set_react_attribute(_to_camel_case(var_name), value)
  771. if isinstance(value, (datetime, date, time)):
  772. value = _date_to_string(value)
  773. self.set_attribute(_to_camel_case(var_name), value)
  774. return self
  775. def _set_labels(self, var_name: str = "labels"):
  776. if value := self.__attributes.get(var_name):
  777. if _is_true(value):
  778. return self.__set_react_attribute(_to_camel_case(var_name), True)
  779. elif isinstance(value, (dict, _MapDict)):
  780. return self.set_dict_attribute(var_name)
  781. return self
  782. def _set_partial(self):
  783. if self.__control_type not in _Builder.__BLOCK_CONTROLS:
  784. return self
  785. if partial := self.__attributes.get("partial"):
  786. if self.__attributes.get("page"):
  787. _warn(f"{self.__element_name} control: page and partial should not be both defined.")
  788. if isinstance(partial, Partial):
  789. self.__attributes["page"] = partial._route
  790. self.__set_react_attribute("partial", partial._route)
  791. self.__set_react_attribute("defaultPartial", True)
  792. return self
  793. def _set_propagate(self):
  794. val = self.__get_boolean_attribute("propagate", self.__gui._config.config.get("propagate"))
  795. return self if val else self.set_boolean_attribute("propagate", False)
  796. def __set_refresh_on_update(self):
  797. if self.__update_vars:
  798. self.set_attribute("updateVars", ";".join(self.__update_vars))
  799. return self
  800. def _set_table_pagesize_options(self, default_size=None):
  801. if default_size is None:
  802. default_size = [50, 100, 500]
  803. page_size_options = self.__attributes.get("page_size_options", default_size)
  804. if isinstance(page_size_options, str):
  805. try:
  806. page_size_options = [int(s.strip()) for s in page_size_options.split(";")]
  807. except Exception as e:
  808. _warn(f"{self.__element_name}: page_size_options value is invalid ({page_size_options})", e)
  809. if isinstance(page_size_options, list):
  810. self.__set_json_attribute("pageSizeOptions", page_size_options)
  811. else:
  812. _warn(f"{self.__element_name}: page_size_options should be a list.")
  813. return self
  814. def _set_input_type(self, type_name: str, allow_password=False):
  815. if allow_password and self.__get_boolean_attribute("password", False):
  816. return self.set_attribute("type", "password")
  817. return self.set_attribute("type", self.__attributes.get("type", type_name))
  818. def _set_kind(self):
  819. if self.__attributes.get("theme", False):
  820. self.set_attribute("mode", "theme")
  821. return self
  822. def __get_typed_hash_name(self, hash_name: str, var_type: t.Optional[PropertyType]) -> str:
  823. if taipy_type := _get_taipy_type(var_type):
  824. expr = self.__gui._get_expr_from_hash(hash_name)
  825. hash_name = self.__gui._evaluate_bind_holder(taipy_type, expr)
  826. return hash_name
  827. def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True):
  828. hash_name = self.__hashes.get(name)
  829. val = self.__get_boolean_attribute(name, def_val)
  830. default_name = f"default_{name}" if hash_name is not None else name
  831. if val != def_val:
  832. self.set_boolean_attribute(default_name, val)
  833. if hash_name is not None:
  834. hash_name = self.__get_typed_hash_name(hash_name, PropertyType.dynamic_boolean)
  835. self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name))
  836. if with_update:
  837. if update_main:
  838. self.__set_update_var_name(hash_name)
  839. else:
  840. self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}")
  841. def __set_dynamic_property_without_default(
  842. self, name: str, property_type: PropertyType, optional: t.Optional[bool] = False
  843. ):
  844. hash_name = self.__hashes.get(name)
  845. if hash_name is None:
  846. if not optional:
  847. _warn(f"{self.__element_name}.{name} should be bound.")
  848. else:
  849. hash_name = self.__get_typed_hash_name(hash_name, property_type)
  850. self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}")
  851. self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name))
  852. return self
  853. def __set_html_content(self, name: str, property_name: str, property_type: PropertyType):
  854. hash_name = self.__hashes.get(name)
  855. if not hash_name:
  856. return self
  857. front_var = self.__get_typed_hash_name(hash_name, property_type)
  858. self.set_attribute(
  859. _to_camel_case(f"default_{property_name}"),
  860. self.__gui._get_user_content_url(
  861. None,
  862. {
  863. "variable_name": front_var,
  864. self.__gui._HTML_CONTENT_KEY: str(_time.time()),
  865. },
  866. ),
  867. )
  868. return self.__set_react_attribute(_to_camel_case(property_name), _get_client_var_name(front_var))
  869. def set_attributes(self, attributes: t.List[tuple]): # noqa: C901
  870. """
  871. TODO-undocumented
  872. Sets the attributes from the property with type and default value.
  873. Arguments:
  874. attributes (list(tuple)): The list of attributes as (property name, property type, default value).
  875. """
  876. attributes.append(("id",)) # Every element should have an id attribute
  877. for attr in attributes:
  878. if not isinstance(attr, tuple):
  879. attr = (attr,)
  880. var_type = _get_tuple_val(attr, 1, PropertyType.string)
  881. if var_type == PropertyType.to_json:
  882. var_type = _TaipyToJson
  883. if var_type == PropertyType.boolean:
  884. def_val = _get_tuple_val(attr, 2, False)
  885. if isinstance(def_val, bool) or self.__attributes.get(attr[0], None) is not None:
  886. val = self.__get_boolean_attribute(attr[0], def_val)
  887. if val != def_val:
  888. self.set_boolean_attribute(attr[0], val)
  889. elif var_type == PropertyType.dynamic_boolean:
  890. self.__set_dynamic_bool_attribute(
  891. attr[0],
  892. _get_tuple_val(attr, 2, False),
  893. _get_tuple_val(attr, 3, False),
  894. _get_tuple_val(attr, 4, True),
  895. )
  896. elif var_type == PropertyType.number:
  897. self.set_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
  898. elif var_type == PropertyType.dynamic_number:
  899. self.__set_dynamic_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
  900. elif var_type == PropertyType.string:
  901. self.__set_string_attribute(attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, True))
  902. elif var_type == PropertyType.dynamic_string:
  903. self.__set_dynamic_string_attribute(
  904. attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, False)
  905. )
  906. elif var_type == PropertyType.string_list:
  907. self.__update_vars.extend(
  908. self.__set_list_attribute(
  909. attr[0],
  910. self.__hashes.get(attr[0]),
  911. self.__attributes.get(attr[0]),
  912. str,
  913. False,
  914. _get_tuple_val(attr, 2, None),
  915. )
  916. )
  917. elif var_type == PropertyType.function:
  918. self.__set_function_attribute(attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, True))
  919. elif var_type == PropertyType.react:
  920. prop_name = _to_camel_case(attr[0])
  921. if hash_name := self.__hashes.get(attr[0]):
  922. self.__update_vars.append(f"{prop_name}={hash_name}")
  923. self.__set_react_attribute(prop_name, hash_name)
  924. else:
  925. self.__set_react_attribute(prop_name, self.__attributes.get(attr[0], _get_tuple_val(attr, 2, None)))
  926. elif var_type == PropertyType.broadcast:
  927. self.__set_react_attribute(
  928. _to_camel_case(attr[0]), _get_broadcast_var_name(_get_tuple_val(attr, 2, None))
  929. )
  930. elif var_type == PropertyType.string_or_number:
  931. self.__set_string_or_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
  932. elif var_type == PropertyType.dict:
  933. self.set_dict_attribute(attr[0], _get_tuple_val(attr, 2, None))
  934. elif var_type == PropertyType.dynamic_dict:
  935. self.set_dynamic_dict_attribute(attr[0], _get_tuple_val(attr, 2, None))
  936. elif var_type == PropertyType.dynamic_list:
  937. self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
  938. elif var_type == PropertyType.boolean_or_list:
  939. if _is_boolean(self.__attributes.get(attr[0])):
  940. self.__set_dynamic_bool_attribute(attr[0], _get_tuple_val(attr, 2, False), True, update_main=False)
  941. else:
  942. self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
  943. elif var_type == PropertyType.dynamic_date:
  944. self.__set_dynamic_date_attribute(attr[0], _get_tuple_val(attr, 2, None))
  945. elif var_type == PropertyType.data:
  946. self.__set_dynamic_property_without_default(attr[0], var_type)
  947. elif (
  948. var_type == PropertyType.lov
  949. or var_type == PropertyType.single_lov
  950. or var_type == PropertyType.lov_no_default
  951. ):
  952. self._get_lov_adapter(
  953. attr[0],
  954. multi_selection=var_type != PropertyType.single_lov,
  955. with_default=var_type != PropertyType.lov_no_default,
  956. )
  957. elif var_type == PropertyType.lov_value:
  958. self.__set_dynamic_property_without_default(
  959. attr[0], var_type, _get_tuple_val(attr, 2, None) == "optional"
  960. )
  961. elif var_type == PropertyType.toHtmlContent:
  962. self.__set_html_content(attr[0], "page", var_type)
  963. elif isclass(var_type) and issubclass(var_type, _TaipyBase):
  964. prop_name = _to_camel_case(attr[0])
  965. if hash_name := self.__hashes.get(attr[0]):
  966. expr = self.__gui._get_expr_from_hash(hash_name)
  967. hash_name = self.__gui._evaluate_bind_holder(var_type, expr)
  968. self.__update_vars.append(f"{prop_name}={hash_name}")
  969. self.__set_react_attribute(prop_name, hash_name)
  970. else:
  971. val = self.__attributes.get(attr[0])
  972. self.set_attribute(
  973. prop_name, var_type(_get_tuple_val(attr, 2, None) if val is None else val, "").get()
  974. )
  975. self.__set_refresh_on_update()
  976. return self
  977. def set_attribute(self, name: str, value: t.Any):
  978. """
  979. TODO-undocumented
  980. Sets an attribute.
  981. Arguments:
  982. name (str): The name of the attribute.
  983. value (Any): The value of the attribute (must be json serializable).
  984. """
  985. if value is not None:
  986. self.el.set(name, value)
  987. return self
  988. def get_element(self):
  989. """
  990. TODO-undocumented
  991. Returns the xml.etree.ElementTree.Element
  992. """
  993. return self.el
  994. def _build_to_string(self):
  995. el_str = str(etree.tostring(self.el, encoding="utf8").decode("utf8"))
  996. el_str = el_str.replace("<?xml version='1.0' encoding='utf8'?>\n", "")
  997. el_str = el_str.replace("/>", ">")
  998. return el_str, self.__element_name