generate_pyi.py 11 KB

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