builder.py 48 KB

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