library.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. # Copyright 2021-2025 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 re
  12. import sys
  13. import typing as t
  14. import xml.etree.ElementTree as etree
  15. from abc import ABC, abstractmethod
  16. from inspect import isclass
  17. from pathlib import Path
  18. from urllib.parse import urlencode, urlparse
  19. from .._renderers.builder import _Builder
  20. from .._warnings import _warn
  21. from ..types import PropertyType
  22. from ..utils import _get_broadcast_var_name, _TaipyBase, _to_camel_case
  23. if t.TYPE_CHECKING:
  24. from ..gui import Gui
  25. from ..state import State
  26. class ElementProperty:
  27. """
  28. The declaration of a property of a visual element.
  29. Each visual element property is described by an instance of `ElementProperty`.
  30. This class holds the information on the name, type and default value for the
  31. element property.
  32. """
  33. def __init__(
  34. self,
  35. property_type: t.Union[PropertyType, t.Type[_TaipyBase]],
  36. default_value: t.Optional[t.Any] = None,
  37. js_name: t.Optional[str] = None,
  38. with_update: t.Optional[bool] = None,
  39. *,
  40. doc_string: t.Optional[str] = None,
  41. ) -> None:
  42. """Initializes a new custom property declaration for an `Element^`.
  43. Arguments:
  44. property_type (PropertyType): The type of this property.
  45. default_value (Optional[Any]): The default value for this property. Default is None.
  46. js_name (Optional[str]): The name of this property, in the front-end JavaScript code.<br/>
  47. If unspecified, a camel case version of `name` is generated: for example, if `name` is
  48. "my_property_name", then this property is referred to as "myPropertyName" in the
  49. JavaScript code.
  50. doc_string: An optional string that holds documentation for that property.<br/>
  51. This is used when generating the stub classes for extension libraries.
  52. """
  53. self.default_value = default_value
  54. self.property_type: t.Union[PropertyType, t.Type[_TaipyBase]]
  55. if property_type == PropertyType.broadcast:
  56. if isinstance(default_value, str):
  57. self.default_value = _get_broadcast_var_name(default_value)
  58. else:
  59. _warn("Element property with type 'broadcast' must define a string default value.")
  60. self.property_type = PropertyType.react
  61. else:
  62. self.property_type = property_type
  63. self._js_name = js_name
  64. self.with_update = with_update
  65. self.doc_string = doc_string
  66. super().__init__()
  67. def check(self, element_name: str, prop_name: str):
  68. if not isinstance(prop_name, str) or not prop_name or not prop_name.isidentifier():
  69. _warn(f"Property name '{prop_name}' is invalid for element '{element_name}'.")
  70. if not isinstance(self.property_type, PropertyType) and not (
  71. isclass(self.property_type) and issubclass(self.property_type, _TaipyBase)
  72. ):
  73. _warn(f"Property type '{self.property_type}' is invalid for element property '{element_name}.{prop_name}'.")
  74. def _get_tuple(self, name: str) -> tuple:
  75. return (
  76. (name, self.property_type, self.default_value)
  77. if self.with_update is None
  78. else (name, self.property_type, self.default_value, self.with_update)
  79. )
  80. def get_js_name(self, name: str) -> str:
  81. return self._js_name or _to_camel_case(name)
  82. class Element:
  83. """
  84. The definition of a custom visual element.
  85. An element is defined by its properties (name, type and default value) and
  86. what the default property name is.
  87. """
  88. __RE_PROP_VAR = re.compile(r"<tp:prop:(\w+)>")
  89. __RE_UNIQUE_VAR = re.compile(r"<tp:uniq:(\w+)>")
  90. def __init__(
  91. self,
  92. default_property: str,
  93. properties: t.Dict[str, ElementProperty],
  94. react_component: t.Optional[str] = None,
  95. *,
  96. render_xhtml: t.Optional[t.Callable[[t.Dict[str, t.Any]], str]] = None,
  97. doc_string: t.Optional[str] = None,
  98. ) -> None:
  99. """Initializes a new custom element declaration.
  100. If *render_xhtml* is specified, then this is a static element, and
  101. *react_component* is ignored.
  102. Arguments:
  103. default_property: The name of the default property for this element.
  104. properties: The dictionary containing the properties of this element, where
  105. the keys are the property names and the values are instances of ElementProperty.
  106. react_component: The name of the component to be created on the front-end.<br/>
  107. If not specified, it is set to a camel case version of the element's name
  108. ("one_name" is transformed to "OneName").
  109. render_xhtml: A function that receives a dictionary containing the element's properties and their values
  110. and that must return a valid XHTML string.<br/>
  111. This is used to implement static elements.
  112. doc_string: The documentation text for this element or None if there is none, which is
  113. the default.<br/>
  114. This string is used when generating stub functions so elements of extension libraries
  115. can be used with the Page Builder API.
  116. """ # noqa: E501
  117. self.default_attribute = default_property
  118. self.attributes = properties
  119. self.js_name = react_component
  120. self.doc_string = doc_string
  121. if callable(render_xhtml):
  122. self._render_xhtml = render_xhtml
  123. super().__init__()
  124. def _get_js_name(self, name: str) -> str:
  125. return self.js_name or _to_camel_case(name, True)
  126. def check(self, name: str):
  127. if not isinstance(name, str) or not name or not name.isidentifier():
  128. _warn(f"Invalid element name: '{name}'.")
  129. default_found = False
  130. if self.attributes:
  131. for prop_name, property in self.attributes.items():
  132. if isinstance(property, ElementProperty):
  133. property.check(name, prop_name)
  134. if not default_found:
  135. default_found = self.default_attribute == prop_name
  136. else:
  137. _warn(f"Property must inherit from 'ElementProperty' '{name}.{prop_name}'.")
  138. if not default_found:
  139. _warn(f"Element {name} has no default property.")
  140. def _is_server_only(self):
  141. return hasattr(self, "_render_xhtml") and callable(self._render_xhtml)
  142. def _process_inner_properties(self, _gui: "Gui", _attributes: t.Dict[str, t.Any], _counter: int):
  143. pass
  144. def _call_builder(
  145. self,
  146. name,
  147. gui: "Gui",
  148. properties: t.Optional[t.Dict[str, t.Any]],
  149. lib: "ElementLibrary",
  150. is_html: t.Optional[bool] = False,
  151. counter: int = 0,
  152. ) -> t.Union[t.Any, t.Tuple[str, str]]:
  153. attributes = properties if isinstance(properties, dict) else {}
  154. self._process_inner_properties(gui, attributes, counter)
  155. # this modifies attributes
  156. hash_names = _Builder._get_variable_hash_names(gui, attributes) # variable replacement
  157. # call user render if any
  158. if self._is_server_only():
  159. xhtml = self._render_xhtml(attributes)
  160. try:
  161. xml_root = etree.fromstring(xhtml)
  162. return (xhtml, name) if is_html else xml_root
  163. except Exception as e:
  164. _warn(f"{name}.render_xhtml() did not return a valid XHTML string", e)
  165. return f"{name}.render_xhtml() did not return a valid XHTML string. {e}"
  166. else:
  167. default_attr: t.Optional[ElementProperty] = None
  168. default_value = None
  169. default_name = None
  170. attrs = []
  171. if self.attributes:
  172. for prop_name, property in self.attributes.items():
  173. if isinstance(property, ElementProperty):
  174. if self.default_attribute == prop_name:
  175. default_name = prop_name
  176. default_attr = property
  177. default_value = property.default_value
  178. else:
  179. attrs.append(property._get_tuple(prop_name))
  180. elt_built = _Builder(
  181. gui=gui,
  182. control_type=name,
  183. element_name=f"{lib.get_js_module_name()}_{self._get_js_name(name)}",
  184. prop_values=properties,
  185. hash_names=hash_names,
  186. lib_name=lib.get_name(),
  187. default_value=default_value,
  188. )
  189. if default_attr is not None:
  190. elt_built.set_value_and_default(
  191. var_name=default_name,
  192. var_type=t.cast(PropertyType, default_attr.property_type),
  193. default_val=default_attr.default_value,
  194. with_default=default_attr.property_type != PropertyType.data,
  195. )
  196. elt_built.set_attributes(attrs)
  197. return elt_built._build_to_string() if is_html else elt_built.el
  198. class ElementLibrary(ABC):
  199. """
  200. A library of user-defined visual elements.
  201. An element library can declare any number of custom visual elements.
  202. In order to use those elements you must register the element library
  203. using the function `Gui.add_library()^`.
  204. An element library can mix *static* and *dynamic* elements.
  205. """
  206. @abstractmethod
  207. def get_elements(self) -> t.Dict[str, Element]:
  208. """
  209. Return the dictionary holding all visual element declarations.
  210. The key for each of this dictionary's entry is the name of the element,
  211. and the value is an instance of `Element^`.
  212. The default implementation returns an empty dictionary, indicating that this library
  213. contains no custom visual elements.
  214. """
  215. pass
  216. @abstractmethod
  217. def get_name(self) -> str:
  218. """
  219. Return the library name.
  220. This string is used for different purposes:
  221. - It allows for finding the definition of visual elements when parsing the
  222. page content.<br/>
  223. Custom elements are defined with the fragment `<|<library_name>.<element_name>|>` in
  224. Markdown pages, and with the tag `<<library_name>:<element_name>>` in HTML pages.
  225. - In element libraries that hold elements with dynamic properties (where JavaScript)
  226. is involved, the name of the JavaScript module that has the front-end code is
  227. derived from this name, as described in `(ElementLibrary.)get_js_module_name()^`.
  228. Returns:
  229. The name of this element library. This must be a valid Python identifier.
  230. !!! note "Element libraries with the same name"
  231. You can add different libraries that have the same name.<br/>
  232. This is useful in large projects where you want to split a potentially large number
  233. of custom visual elements into different groups but still access them from your pages
  234. using the same library name prefix.<br/>
  235. In this situation, you will have to implement `(ElementLibrary.)get_js_module_name()^`
  236. because each JavaScript module will have to have a unique name.
  237. """
  238. pass
  239. def get_js_module_name(self) -> str:
  240. """
  241. Return the name of the JavaScript module.
  242. The JavaScript module is the JavaScript file that contains all the front-end
  243. code for this element library. Typically, the name of JavaScript modules uses camel case.<br/>
  244. This module name must be unique on the browser window scope: if your application uses
  245. several custom element libraries, they must define a unique name for their JavaScript module.
  246. The default implementation transforms the return value of `(ElementLibrary.)get_name()^` in
  247. the following manner:
  248. - The JavaScript module name is a camel case version of the element library name
  249. (see `(ElementLibrary.)get_name()^`):
  250. - If the library name is "library", the JavaScript module name defaults to "Library".
  251. - If the library name is "myLibrary", the JavaScript module name defaults to "Mylibrary".
  252. - If the element library name has underscore characters, each underscore-separated fragment is
  253. considered as a distinct word:
  254. - If the library name is "my_library", the JavaScript module name defaults to "MyLibrary".
  255. Returns:
  256. The name of the JavaScript module for this element library.<br/>
  257. The default implementation returns a camel case version of `self.get_name()`,
  258. as described above.
  259. """
  260. return _to_camel_case(self.get_name(), True)
  261. def __get_class_folder(self):
  262. if not hasattr(self, "_class_folder"):
  263. module_obj = sys.modules.get(self.__class__.__module__)
  264. base = (Path(".") if module_obj is None else Path(module_obj.__file__).parent).resolve() # type: ignore
  265. self._class_folder = base if base.exists() else Path(".").resolve()
  266. return self._class_folder
  267. def _do_get_relative_paths(self, paths: t.List[str]) -> t.List[str]:
  268. ret = set()
  269. for path in paths or []:
  270. if bool(urlparse(path).netloc):
  271. ret.add(path)
  272. elif file_paths := self.__get_class_folder().glob(path):
  273. ret.update([file_path.relative_to(self.__get_class_folder()).as_posix() for file_path in file_paths])
  274. elif path:
  275. ret.add(path)
  276. return list(ret)
  277. def get_scripts(self) -> t.List[str]:
  278. """
  279. Return the list of the mandatory script file path names.
  280. If a script file pathname is an absolute URL it will be used as is.<br/>
  281. If it's not it will be passed to `(ElementLibrary.)get_resource()^` to retrieve a local
  282. path to the resource.
  283. The default implementation returns an empty list, indicating that this library contains
  284. no custom visual elements with dynamic properties.
  285. Returns:
  286. A list of paths (relative to the element library Python implementation file or
  287. absolute) to all JavaScript module files to be loaded on the front-end.<br/>
  288. The default implementation returns an empty list.
  289. """
  290. return []
  291. def get_styles(self) -> t.List[str]:
  292. """
  293. TODO
  294. Returns the list of resources names for the css stylesheets.
  295. Defaults to []
  296. """
  297. return []
  298. def get_resource(self, name: str) -> Path:
  299. """
  300. TODO
  301. Defaults to return None?
  302. Returns a path for a resource name.
  303. Resource URL should be formed as /taipy-extension/<library_name>/<resource virtual path> with(see get_resource_url)
  304. - <resource virtual path> being the `name` parameter
  305. - <library_name> the value returned by `get_name()`
  306. Arguments:
  307. name (str): The name of the resource for which a local Path should be returned.
  308. """ # noqa: E501
  309. base = self.__get_class_folder()
  310. file = (base / name).resolve()
  311. if str(file).startswith(str(base)) and file.exists():
  312. return file
  313. else:
  314. raise FileNotFoundError(f"Cannot access resource {file}.")
  315. def get_resource_url(self, resource: str) -> str:
  316. """TODO"""
  317. from ..gui import Gui
  318. return f"/{Gui._EXTENSION_ROOT}/{self.get_name()}/{resource}{self.get_query(resource)}"
  319. def get_data(self, library_name: str, payload: t.Dict, var_name: str, value: t.Any) -> t.Optional[t.Dict]:
  320. """
  321. TODO
  322. Called if implemented (i.e returns a dict).
  323. Arguments:
  324. library_name (str): The name of this library.
  325. payload (dict): The payload send by the `createRequestDataUpdateAction()` front-end function.
  326. var_name (str): The name of the variable holding the data.
  327. value (any): The current value of the variable identified by *var_name*.
  328. """
  329. return None
  330. def on_init(self, gui: "Gui") -> t.Optional[t.Tuple[str, t.Any]]:
  331. """
  332. Initialize this element library.
  333. This method is invoked by `Gui.run()^`.
  334. It allows to define variables that are accessible from elements
  335. defined in this element library.
  336. Arguments
  337. gui: The `Gui^` instance.
  338. Returns:
  339. An optional tuple composed of a variable name (that *must* be a valid Python
  340. identifier), associated with its value.<br/>
  341. This name can be used as the name of a variable accessible by the elements defined
  342. in this library.<br/>
  343. This name must be unique across the entire application, which is a problem since
  344. different element libraries might use the same symbol. A good development practice
  345. is to make this variable name unique by prefixing it with the name of the element
  346. library itself.
  347. """
  348. return None
  349. def on_user_init(self, state: "State"): # noqa: B027
  350. """
  351. Initialize user state on first access.
  352. Arguments
  353. state: The `State^` instance.
  354. """
  355. pass
  356. def get_query(self, name: str) -> str:
  357. """
  358. Return an URL query depending on the resource name.<br/>
  359. Default implementation returns the version if defined.
  360. Arguments:
  361. name (str): The name of the resource for which a query should be returned.
  362. Returns:
  363. A string that holds the query part of an URL (starting with ?).
  364. """
  365. if version := self.get_version():
  366. return f"?{urlencode({'v': version})}"
  367. return ""
  368. def get_version(self) -> t.Optional[str]:
  369. """
  370. The optional library version
  371. Returns:
  372. An optional string representing the library version.<br/>
  373. This version will be appended to the resource URL as a query arg (?v=<version>)
  374. """
  375. return None
  376. class _ElementWithInnerProps(Element):
  377. def __init__(
  378. self,
  379. default_property: str,
  380. properties: t.Dict[str, ElementProperty],
  381. react_component: t.Optional[str] = None,
  382. render_xhtml: t.Optional[t.Callable[[t.Dict[str, t.Any]], str]] = None,
  383. doc_string: t.Optional[str] = None,
  384. *,
  385. inner_properties: t.Optional[t.Dict[str, ElementProperty]] = None,
  386. ) -> None:
  387. """NOT DOCUMENTED
  388. Arguments:
  389. inner_properties (Optional[List[ElementProperty]]): The optional list of inner properties
  390. for this element.<br/>
  391. Default values are set/bound automatically.
  392. """
  393. super().__init__(
  394. default_property=default_property,
  395. properties=properties,
  396. react_component=react_component,
  397. render_xhtml=render_xhtml,
  398. doc_string=doc_string,
  399. )
  400. self.inner_properties = inner_properties
  401. def _process_inner_properties(self, gui: "Gui", attributes: t.Dict[str, t.Any], counter: int):
  402. if self.inner_properties:
  403. uniques: t.Dict[str, int] = {}
  404. self.attributes.update(
  405. {
  406. prop: ElementProperty(attr.property_type, None, attr._js_name, attr.with_update)
  407. for prop, attr in self.inner_properties.items()
  408. }
  409. )
  410. for prop, attr in self.inner_properties.items():
  411. val = attr.default_value
  412. if val:
  413. # handling property replacement in inner properties <tp:prop:...>
  414. while m := Element.__RE_PROP_VAR.search(val):
  415. var = attributes.get(m.group(1))
  416. hash_value = None if var is None else gui._evaluate_expr(var)
  417. if hash_value:
  418. names = gui._get_real_var_name(hash_value)
  419. hash_value = names[0] if isinstance(names, tuple) else names
  420. else:
  421. hash_value = "None"
  422. val = val[: m.start()] + hash_value + val[m.end() :]
  423. # handling unique id replacement in inner properties <tp:uniq:...>
  424. has_uniq = False
  425. while m := Element.__RE_UNIQUE_VAR.search(val):
  426. has_uniq = True
  427. id = uniques.get(m.group(1))
  428. if id is None:
  429. id = len(uniques) + 1
  430. uniques[m.group(1)] = id
  431. val = f"{val[: m.start()]}{counter}{id}{val[m.end() :]}"
  432. if has_uniq and gui._is_expression(val):
  433. gui._evaluate_expr(val, True)
  434. attributes[prop] = val