builder.py 45 KB

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