builder.py 43 KB

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