generate_pyi.py 14 KB

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