library.py 19 KB

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