generate_pyi.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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 json
  12. import os
  13. import re
  14. import sys
  15. from typing import Any, Dict, List, Union, get_args, get_origin
  16. from markdownify import markdownify
  17. __RE_INDEXED_PROPERTY = re.compile(r"^([\w_]+)\[(<\w+>)?([\w]+)(</\w+>)?\]$")
  18. # Script should be located in <taipy_root>/tools
  19. script_dir = os.path.dirname(os.path.realpath(__file__))
  20. # Move to <taipy_root>
  21. os.chdir(os.path.dirname(os.path.dirname(script_dir)))
  22. # Make sure we can import the mandatory packages
  23. if not os.path.isdir(os.path.abspath(os.path.join(script_dir, "taipy"))):
  24. sys.path.append(os.path.abspath(os.path.join(script_dir, os.pardir, os.pardir)))
  25. # ##################################################################################################
  26. # Generate gui pyi file (gui/gui.pyi)
  27. # ##################################################################################################
  28. gui_py_file = "./taipy/gui/gui.py"
  29. gui_pyi_file = f"{gui_py_file}i"
  30. from taipy.gui.config import Config # noqa: E402
  31. # Generate Python interface definition files
  32. os.system(f"pipenv run stubgen {gui_py_file} --no-import --parse-only --export-less -o ./")
  33. gui_config = "".join(
  34. (
  35. f", {k}: {v.__name__} = ..."
  36. if "<class" in str(v)
  37. else f", {k}: {str(v).replace('typing', 't').replace('taipy.gui.config.', '')} = ..."
  38. )
  39. for k, v in Config.__annotations__.items()
  40. )
  41. replaced_content = ""
  42. with open(gui_pyi_file, "r", encoding="utf-8") as file:
  43. for line in file:
  44. if "def run(" in line:
  45. replace_str = line[line.index(", run_server") : (line.index("**kwargs") + len("**kwargs"))]
  46. # ", run_server: bool = ..., run_in_thread: bool = ..., async_mode: str = ..., **kwargs"
  47. line = line.replace(replace_str, gui_config)
  48. replaced_content += line
  49. with open(gui_pyi_file, "w", encoding="utf-8") as write_file:
  50. write_file.write(replaced_content)
  51. # ##################################################################################################
  52. # Generate Page Builder pyi file (gui/builder/__init__.pyi)
  53. # ##################################################################################################
  54. # Types that appear in viselements.json
  55. from taipy.gui import Icon # noqa: E402
  56. from taipy.core import Cycle, DataNode, Job, Scenario # noqa: E402
  57. from datetime import datetime
  58. # Read the version
  59. current_version = "latest"
  60. with open("./taipy/gui/version.json", "r", encoding="utf-8") as vfile:
  61. version = json.load(vfile)
  62. if "dev" in version.get("ext", ""):
  63. current_version = "develop"
  64. else:
  65. current_version = f'release-{version.get("major", 0)}.{version.get("minor", 0)}'
  66. taipy_doc_url = f"https://docs.taipy.io/en/{current_version}/manuals/userman/gui/viselements/generic/"
  67. builder_py_file = "./taipy/gui/builder/__init__.py"
  68. builder_pyi_file = f"{builder_py_file}i"
  69. controls: Dict[str, List] = {}
  70. blocks: Dict[str, List] = {}
  71. undocumented: Dict[str, List] = {}
  72. with open("./taipy/gui/viselements.json", "r", encoding="utf-8") as file:
  73. viselements: Dict[str, List] = json.load(file)
  74. controls[""] = viselements.get("controls", [])
  75. blocks[""] = viselements.get("blocks", [])
  76. undocumented[""] = viselements.get("undocumented", [])
  77. with open("./taipy/gui_core/viselements.json", "r", encoding="utf-8") as file:
  78. core_viselements: Dict[str, List] = json.load(file)
  79. controls['if find_spec("taipy.core"):'] = core_viselements.get("controls", [])
  80. blocks['if find_spec("taipy.core"):'] = core_viselements.get("blocks", [])
  81. undocumented['if find_spec("taipy.core"):'] = core_viselements.get("undocumented", [])
  82. os.system(f"pipenv run stubgen {builder_py_file} --no-import --parse-only --export-less -o ./")
  83. with open(builder_pyi_file, "a", encoding="utf-8") as file:
  84. file.write("from datetime import datetime\n")
  85. file.write("from importlib.util import find_spec\n")
  86. file.write("from typing import Any, Callable, Optional, Union\n")
  87. file.write("\n")
  88. file.write("from .. import Icon\n")
  89. file.write("from ._element import _Block, _Control\n")
  90. file.write('if find_spec("taipy.core"):\n')
  91. file.write("\tfrom taipy.core import Cycle, DataNode, Job, Scenario\n")
  92. def resolve_inherit(
  93. name: str, properties, inherits, blocks: List, controls: List, undocumented: List
  94. ) -> List[Dict[str, Any]]:
  95. if not inherits:
  96. return properties
  97. for inherit_name in inherits:
  98. inherited_desc = next((e for e in undocumented if e[0] == inherit_name), None)
  99. if inherited_desc is None:
  100. inherited_desc = next((e for e in blocks if e[0] == inherit_name), None)
  101. if inherited_desc is None:
  102. inherited_desc = next((e for e in controls if e[0] == inherit_name), None)
  103. if inherited_desc is None:
  104. raise RuntimeError(f"Element type '{name}' inherits from unknown element type '{inherit_name}'")
  105. inherited_desc = inherited_desc[1]
  106. for inherit_prop in inherited_desc["properties"]:
  107. prop_desc = next((p for p in properties if p["name"] == inherit_prop["name"]), None)
  108. if prop_desc: # Property exists
  109. def override(current, inherits, p: str):
  110. if p not in current and (inherited := inherits.get(p, None)):
  111. current[p] = inherited
  112. override(prop_desc, inherit_prop, "type")
  113. override(prop_desc, inherit_prop, "default_value")
  114. override(prop_desc, inherit_prop, "doc")
  115. override(prop_desc, inherit_prop, "signature")
  116. else:
  117. properties.append(inherit_prop)
  118. properties = resolve_inherit(
  119. inherit_name, properties, inherited_desc.get("inherits", None), blocks, controls, undocumented
  120. )
  121. return properties
  122. def format_as_parameter(property: Dict[str, str], element_name: str):
  123. name = property["name"]
  124. if match := __RE_INDEXED_PROPERTY.match(name):
  125. name = f"{match.group(1)}__{match.group(3)}"
  126. type = property["type"]
  127. if m := re.match(r"indexed\((.*)\)", type):
  128. type = m[1]
  129. property["indexed"] = " (indexed)"
  130. else:
  131. property["indexed"] = ""
  132. if m := re.match(r"dynamic\((.*)\)", type):
  133. type = m[1]
  134. property["dynamic"] = " (dynamic)"
  135. else:
  136. property["dynamic"] = ""
  137. default_value = property.get("default_value", None)
  138. type, _ = re.subn(r"\bCallable|Callback|Function\b", "callable", type)
  139. type = re.sub(r"((plotly|taipy)\.[\w\.]*)", r'"\1"', type)
  140. try:
  141. type_desc = eval(type)
  142. if get_origin(type_desc) is Union:
  143. types = get_args(type_desc)
  144. if not any(t.__name__ in ["str", "Any"] for t in types):
  145. type = type.rpartition("]")
  146. type = type[0] + ", str]"
  147. elif hasattr(type_desc, "__name__") and type_desc.__name__ not in ["str", "Any"]:
  148. type = f"Union[{type}, str]"
  149. except NameError:
  150. print(f"WARNING - Couldn't parse type '{type}' in {element_name}.{name}")
  151. if default_value is None or default_value == "None":
  152. default_value = " = None"
  153. if type.startswith("Union["):
  154. type = type.rpartition("]")
  155. type = ": " + type[0] + ", None]"
  156. else:
  157. type = f": Optional[{type}]"
  158. else:
  159. try:
  160. eval(default_value)
  161. default_value = f" = {default_value}"
  162. if type:
  163. type = f": {type}"
  164. except Exception:
  165. default_value = " = None"
  166. if type.startswith("Union["):
  167. type = type.rpartition("]")
  168. type = ": " + type[0] + ", None]"
  169. else:
  170. type = f": Optional[{type}]"
  171. return f"{name}{type}{default_value}"
  172. def build_doc(name: str, desc: Dict[str, Any]):
  173. if "doc" not in desc:
  174. return ""
  175. doc = str(desc["doc"])
  176. # Hack to replace the actual element name in the class_name property doc
  177. if desc["name"] == "class_name":
  178. doc = doc.replace("[element_type]", name)
  179. # This won't work for Scenario Management and Block elements
  180. doc = re.sub(r"(href=\")\.\.((?:.*?)\")", r"\1" + taipy_doc_url + name + r"/../..\2", doc)
  181. doc = re.sub(r"<tt>([\w_]+)</tt>", r"`\1`", doc) # <tt> not processed properly by markdownify()
  182. doc = "\n ".join(markdownify(doc).split("\n"))
  183. # <, >, `, [, -, _ and * prefixed with a \
  184. doc = doc.replace(" \n", " \\n").replace("\\<", "<").replace("\\>", ">").replace("\\`", "`")
  185. doc = doc.replace("\\[", "[").replace("\\-", "-").replace("\\_", "_").replace("\\*", "*")
  186. # Final dots are prefixed with a \
  187. doc = re.sub(r"\\.$", ".", doc)
  188. # Link anchors # signs are prefixed with a \
  189. doc = re.sub(r"\\(#[a-z_]+\))", r"\1", doc)
  190. doc = re.sub(r"(?:\s+\\n)?\s+See below(?:[^\.]*)?\.", "", doc).replace("\n", "\\n")
  191. return f"{desc['name']}{desc['dynamic']}{desc['indexed']}\\n {doc}\\n\\n"
  192. def element_template(name: str, base_class: str, n: str, properties_decl: str, properties_doc: str, ind: str):
  193. return f"""
  194. {ind}class {name}(_{base_class}):
  195. {ind} _ELEMENT_NAME: str
  196. {ind} def __init__(self, {properties_decl}) -> None:
  197. {ind} \"\"\"Creates a{n} {name} element.\\n\\nParameters\\n----------\\n\\n{properties_doc}\"\"\" # noqa: E501
  198. {ind} ...
  199. """
  200. def generate_elements(elements_by_prefix: Dict[str, List], base_class: str):
  201. for prefix, elements in elements_by_prefix.items():
  202. if not elements:
  203. continue
  204. indent = ""
  205. if prefix:
  206. indent = " "
  207. with open(builder_pyi_file, "a", encoding="utf-8") as file:
  208. file.write(prefix + "\n")
  209. for element in elements:
  210. name = element[0]
  211. desc = element[1]
  212. properties_doc = ""
  213. property_list: List[Dict[str, Any]] = []
  214. property_names: List[str] = []
  215. properties = resolve_inherit(
  216. name,
  217. desc["properties"],
  218. desc.get("inherits", None),
  219. blocks.get(prefix, []),
  220. controls.get(prefix, []),
  221. undocumented.get(prefix, []),
  222. )
  223. # Remove hidden properties
  224. properties = [p for p in properties if not p.get("hide", False)]
  225. # Generate function parameters
  226. properties_decl = [format_as_parameter(p, name) for p in properties]
  227. # Manually add the 'inline' property for the text control
  228. if name == "text":
  229. properties_decl.append("inline: bool = False")
  230. # Generate properties doc
  231. for property in properties:
  232. if "default_property" in property and property["default_property"] is True:
  233. property_list.insert(0, property)
  234. property_names.insert(0, property["name"])
  235. continue
  236. property_list.append(property)
  237. property_names.append(property["name"])
  238. # Append properties doc to element doc (once ordered)
  239. for property in property_list:
  240. property_doc = build_doc(name, property)
  241. properties_doc += property_doc
  242. if name == "text":
  243. properties_doc += ("inline\n If True, the text is created next to "
  244. + "the previous element and not on a new line.\n\n")
  245. if len(properties_decl) > 1:
  246. properties_decl.insert(1, "*")
  247. # Append element to __init__.pyi
  248. with open(builder_pyi_file, "a", encoding="utf-8") as file:
  249. file.write(
  250. element_template(
  251. name,
  252. base_class,
  253. "n" if name[0] in ["a", "e", "i", "o"] else "",
  254. ", ".join(properties_decl),
  255. properties_doc,
  256. indent,
  257. )
  258. )
  259. generate_elements(controls, "Control")
  260. generate_elements(blocks, "Block")
  261. os.system(f"pipenv run isort {gui_pyi_file}")
  262. os.system(f"pipenv run black {gui_pyi_file}")
  263. os.system(f"pipenv run isort {builder_pyi_file}")
  264. os.system(f"pipenv run black {builder_pyi_file}")