metaobjectdump.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. # Copyright (C) 2022 The Qt Company Ltd.
  2. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
  3. from __future__ import annotations
  4. import ast
  5. import json
  6. import os
  7. import sys
  8. import tokenize
  9. from argparse import ArgumentParser, RawTextHelpFormatter
  10. from pathlib import Path
  11. from typing import Union
  12. DESCRIPTION = """Parses Python source code to create QObject metatype
  13. information in JSON format for qmltyperegistrar."""
  14. REVISION = 68
  15. CPP_TYPE_MAPPING = {"str": "QString"}
  16. QML_IMPORT_NAME = "QML_IMPORT_NAME"
  17. QML_IMPORT_MAJOR_VERSION = "QML_IMPORT_MAJOR_VERSION"
  18. QML_IMPORT_MINOR_VERSION = "QML_IMPORT_MINOR_VERSION"
  19. QT_MODULES = "QT_MODULES"
  20. ITEM_MODELS = ["QAbstractListModel", "QAbstractProxyModel",
  21. "QAbstractTableModel", "QConcatenateTablesProxyModel",
  22. "QFileSystemModel", "QIdentityProxyModel", "QPdfBookmarkModel",
  23. "QPdfSearchModel", "QSortFilterProxyModel", "QSqlQueryModel",
  24. "QStandardItemModel", "QStringListModel", "QTransposeProxyModel",
  25. "QWebEngineHistoryModel"]
  26. QOBJECT_DERIVED = ["QObject", "QQuickItem", "QQuickPaintedItem"] + ITEM_MODELS
  27. # Python 3.9 does not support this syntax, yet
  28. # AstDecorator = ast.Name | ast.Call
  29. # AstPySideTypeSpec = ast.Name | ast.Constant
  30. AstDecorator = Union[ast.Name, ast.Call]
  31. AstPySideTypeSpec = Union[ast.Name, ast.Constant]
  32. ClassList = list[dict]
  33. # PropertyEntry = dict[str, str | int | bool]
  34. PropertyEntry = dict[str, Union[str, int, bool]]
  35. Argument = dict[str, str]
  36. Arguments = list[Argument]
  37. # Signal = dict[str, str | Arguments]
  38. # Slot = dict[str, str | Arguments]
  39. Signal = dict[str, Union[str, Arguments]]
  40. Slot = dict[str, Union[str, Arguments]]
  41. def _decorator(name: str, value: str) -> dict[str, str]:
  42. """Create a QML decorator JSON entry"""
  43. return {"name": name, "value": value}
  44. def _attribute(node: ast.Attribute) -> tuple[str, str]:
  45. """Split an attribute."""
  46. return node.value.id, node.attr
  47. def _name(node: ast.Name | ast.Attribute | ast.Constant) -> str:
  48. """Return the name of something that is either an attribute or a name,
  49. such as base classes or call.func"""
  50. if isinstance(node, ast.Constant):
  51. return str(node.value)
  52. if isinstance(node, ast.Attribute):
  53. qualifier, name = _attribute(node)
  54. return f"{qualifier}.{node.attr}"
  55. return node.id
  56. def _func_name(node: ast.Call) -> str:
  57. return _name(node.func)
  58. def _python_to_cpp_type(type: str) -> str:
  59. """Python to C++ type"""
  60. c = CPP_TYPE_MAPPING.get(type)
  61. return c if c else type
  62. def _parse_property_kwargs(keywords: list[ast.keyword], prop: PropertyEntry):
  63. """Parse keyword arguments of @Property"""
  64. for k in keywords:
  65. if k.arg == "notify":
  66. prop["notify"] = _name(k.value)
  67. def _parse_assignment(node: ast.Assign) -> tuple[str | None, ast.AST | None]:
  68. """Parse an assignment and return a tuple of name, value."""
  69. if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
  70. var_name = node.targets[0].id
  71. return (var_name, node.value)
  72. return (None, None)
  73. def _parse_pyside_type(type_spec: AstPySideTypeSpec) -> str:
  74. """Parse type specification of a Slot/Property decorator. Usually a type,
  75. but can also be a string constant with a C++ type name."""
  76. if isinstance(type_spec, ast.Constant):
  77. return type_spec.value
  78. return _python_to_cpp_type(_name(type_spec))
  79. def _parse_call_args(call: ast.Call):
  80. """Parse arguments of a Signal call/Slot decorator (type list)."""
  81. result: Arguments = []
  82. for n, arg in enumerate(call.args):
  83. par_name = f"a{n + 1}"
  84. par_type = _parse_pyside_type(arg)
  85. result.append({"name": par_name, "type": par_type})
  86. return result
  87. def _parse_slot(func_name: str, call: ast.Call) -> Slot:
  88. """Parse a 'Slot' decorator."""
  89. return_type = "void"
  90. for kwarg in call.keywords:
  91. if kwarg.arg == "result":
  92. return_type = _python_to_cpp_type(_name(kwarg.value))
  93. break
  94. return {"access": "public", "name": func_name,
  95. "arguments": _parse_call_args(call),
  96. "returnType": return_type}
  97. class VisitorContext:
  98. """Stores a list of QObject-derived classes encountered in order to find
  99. out which classes inherit QObject."""
  100. def __init__(self):
  101. self.qobject_derived = QOBJECT_DERIVED
  102. class MetaObjectDumpVisitor(ast.NodeVisitor):
  103. """AST visitor for parsing sources and creating the data structure for
  104. JSON."""
  105. def __init__(self, context: VisitorContext):
  106. super().__init__()
  107. self._context = context
  108. self._json_class_list: ClassList = []
  109. # Property by name, which will be turned into the JSON List later
  110. self._properties: list[PropertyEntry] = []
  111. self._signals: list[Signal] = []
  112. self._within_class: bool = False
  113. self._qt_modules: set[str] = set()
  114. self._qml_import_name = ""
  115. self._qml_import_major_version = 0
  116. self._qml_import_minor_version = 0
  117. def json_class_list(self) -> ClassList:
  118. return self._json_class_list
  119. def qml_import_name(self) -> str:
  120. return self._qml_import_name
  121. def qml_import_version(self) -> tuple[int, int]:
  122. return (self._qml_import_major_version, self._qml_import_minor_version)
  123. def qt_modules(self):
  124. return sorted(self._qt_modules)
  125. @staticmethod
  126. def create_ast(filename: Path) -> ast.Module:
  127. """Create an Abstract Syntax Tree on which a visitor can be run"""
  128. node = None
  129. with tokenize.open(filename) as file:
  130. node = ast.parse(file.read(), mode="exec")
  131. return node
  132. def visit_Assign(self, node: ast.Assign):
  133. """Parse the global constants for QML-relevant values"""
  134. var_name, value_node = _parse_assignment(node)
  135. if not var_name or not isinstance(value_node, ast.Constant):
  136. return
  137. value = value_node.value
  138. if var_name == QML_IMPORT_NAME:
  139. self._qml_import_name = value
  140. elif var_name == QML_IMPORT_MAJOR_VERSION:
  141. self._qml_import_major_version = value
  142. elif var_name == QML_IMPORT_MINOR_VERSION:
  143. self._qml_import_minor_version = value
  144. def visit_ClassDef(self, node: ast.Module):
  145. """Visit a class definition"""
  146. self._properties = []
  147. self._signals = []
  148. self._slots = []
  149. self._within_class = True
  150. qualified_name = node.name
  151. last_dot = qualified_name.rfind('.')
  152. name = (qualified_name[last_dot + 1:] if last_dot != -1
  153. else qualified_name)
  154. data = {"className": name,
  155. "qualifiedClassName": qualified_name}
  156. q_object = False
  157. bases = []
  158. for b in node.bases:
  159. # PYSIDE-2202: catch weird constructs like "class C(type(Base)):"
  160. if isinstance(b, ast.Name):
  161. base_name = _name(b)
  162. if base_name in self._context.qobject_derived:
  163. q_object = True
  164. self._context.qobject_derived.append(name)
  165. base_dict = {"access": "public", "name": base_name}
  166. bases.append(base_dict)
  167. data["object"] = q_object
  168. if bases:
  169. data["superClasses"] = bases
  170. class_decorators: list[dict] = []
  171. for d in node.decorator_list:
  172. self._parse_class_decorator(d, class_decorators)
  173. if class_decorators:
  174. data["classInfos"] = class_decorators
  175. for b in node.body:
  176. if isinstance(b, ast.Assign):
  177. self._parse_class_variable(b)
  178. else:
  179. self.visit(b)
  180. if self._properties:
  181. data["properties"] = self._properties
  182. if self._signals:
  183. data["signals"] = self._signals
  184. if self._slots:
  185. data["slots"] = self._slots
  186. self._json_class_list.append(data)
  187. self._within_class = False
  188. def visit_FunctionDef(self, node):
  189. if self._within_class:
  190. for d in node.decorator_list:
  191. self._parse_function_decorator(node.name, d)
  192. def _parse_class_decorator(self, node: AstDecorator,
  193. class_decorators: list[dict]):
  194. """Parse ClassInfo decorators."""
  195. if isinstance(node, ast.Call):
  196. name = _func_name(node)
  197. if name == "QmlUncreatable":
  198. class_decorators.append(_decorator("QML.Creatable", "false"))
  199. if node.args:
  200. reason = node.args[0].value
  201. if isinstance(reason, str):
  202. d = _decorator("QML.UncreatableReason", reason)
  203. class_decorators.append(d)
  204. elif name == "QmlAttached" and len(node.args) == 1:
  205. d = _decorator("QML.Attached", node.args[0].id)
  206. class_decorators.append(d)
  207. elif name == "QmlExtended" and len(node.args) == 1:
  208. d = _decorator("QML.Extended", node.args[0].id)
  209. class_decorators.append(d)
  210. elif name == "ClassInfo" and node.keywords:
  211. kw = node.keywords[0]
  212. class_decorators.append(_decorator(kw.arg, kw.value.value))
  213. elif name == "QmlForeign" and len(node.args) == 1:
  214. d = _decorator("QML.Foreign", node.args[0].id)
  215. class_decorators.append(d)
  216. elif name == "QmlNamedElement" and node.args:
  217. name = node.args[0].value
  218. class_decorators.append(_decorator("QML.Element", name))
  219. elif name.startswith('Q'):
  220. print('Unknown decorator with parameters:', name,
  221. file=sys.stderr)
  222. return
  223. if isinstance(node, ast.Name):
  224. name = node.id
  225. if name == "QmlElement":
  226. class_decorators.append(_decorator("QML.Element", "auto"))
  227. elif name == "QmlSingleton":
  228. class_decorators.append(_decorator("QML.Singleton", "true"))
  229. elif name == "QmlAnonymous":
  230. class_decorators.append(_decorator("QML.Element", "anonymous"))
  231. elif name.startswith('Q'):
  232. print('Unknown decorator:', name, file=sys.stderr)
  233. return
  234. def _index_of_property(self, name: str) -> int:
  235. """Search a property by name"""
  236. for i in range(len(self._properties)):
  237. if self._properties[i]["name"] == name:
  238. return i
  239. return -1
  240. def _create_property_entry(self, name: str, type: str,
  241. getter: str | None = None) -> PropertyEntry:
  242. """Create a property JSON entry."""
  243. result: PropertyEntry = {"name": name, "type": type,
  244. "index": len(self._properties)}
  245. if getter:
  246. result["read"] = getter
  247. return result
  248. def _parse_function_decorator(self, func_name: str, node: AstDecorator):
  249. """Parse function decorators."""
  250. if isinstance(node, ast.Attribute):
  251. name = node.value.id
  252. value = node.attr
  253. if value == "setter": # Property setter
  254. idx = self._index_of_property(name)
  255. if idx != -1:
  256. self._properties[idx]["write"] = func_name
  257. return
  258. if isinstance(node, ast.Call):
  259. name = _name(node.func)
  260. if name == "Property": # Property getter
  261. if node.args: # 1st is type/type string
  262. type = _parse_pyside_type(node.args[0])
  263. prop = self._create_property_entry(func_name, type,
  264. func_name)
  265. _parse_property_kwargs(node.keywords, prop)
  266. self._properties.append(prop)
  267. elif name == "Slot":
  268. self._slots.append(_parse_slot(func_name, node))
  269. else:
  270. print('Unknown decorator with parameters:', name,
  271. file=sys.stderr)
  272. def _parse_class_variable(self, node: ast.Assign):
  273. """Parse a class variable assignment (Property, Signal, etc.)"""
  274. (var_name, call) = _parse_assignment(node)
  275. if not var_name or not isinstance(node.value, ast.Call):
  276. return
  277. func_name = _func_name(call)
  278. if func_name == "Signal" or func_name == "QtCore.Signal":
  279. signal: Signal = {"access": "public", "name": var_name,
  280. "arguments": _parse_call_args(call),
  281. "returnType": "void"}
  282. self._signals.append(signal)
  283. elif func_name == "Property" or func_name == "QtCore.Property":
  284. type = _python_to_cpp_type(call.args[0].id)
  285. prop = self._create_property_entry(var_name, type, call.args[1].id)
  286. if len(call.args) > 2:
  287. prop["write"] = call.args[2].id
  288. _parse_property_kwargs(call.keywords, prop)
  289. self._properties.append(prop)
  290. elif func_name == "ListProperty" or func_name == "QtCore.ListProperty":
  291. type = _python_to_cpp_type(call.args[0].id)
  292. type = f"QQmlListProperty<{type}>"
  293. prop = self._create_property_entry(var_name, type)
  294. self._properties.append(prop)
  295. def visit_Import(self, node):
  296. for n in node.names: # "import PySide6.QtWidgets"
  297. self._handle_import(n.name)
  298. def visit_ImportFrom(self, node):
  299. if "." in node.module: # "from PySide6.QtWidgets import QWidget"
  300. self._handle_import(node.module)
  301. elif node.module == "PySide6": # "from PySide6 import QtWidgets"
  302. for n in node.names:
  303. if n.name.startswith("Qt"):
  304. self._qt_modules.add(n.name)
  305. def _handle_import(self, mod: str):
  306. if mod.startswith("PySide6."):
  307. self._qt_modules.add(mod[8:])
  308. def create_arg_parser(desc: str) -> ArgumentParser:
  309. parser = ArgumentParser(description=desc,
  310. formatter_class=RawTextHelpFormatter)
  311. parser.add_argument('--compact', '-c', action='store_true',
  312. help='Use compact format')
  313. parser.add_argument('--suppress-file', '-s', action='store_true',
  314. help='Suppress inputFile entry (for testing)')
  315. parser.add_argument('--quiet', '-q', action='store_true',
  316. help='Suppress warnings')
  317. parser.add_argument('files', type=str, nargs="+",
  318. help='Python source file')
  319. parser.add_argument('--out-file', '-o', type=str,
  320. help='Write output to file rather than stdout')
  321. return parser
  322. def parse_file(file: Path, context: VisitorContext,
  323. suppress_file: bool = False) -> dict | None:
  324. """Parse a file and return its json data"""
  325. ast_tree = MetaObjectDumpVisitor.create_ast(file)
  326. visitor = MetaObjectDumpVisitor(context)
  327. visitor.visit(ast_tree)
  328. class_list = visitor.json_class_list()
  329. if not class_list:
  330. return None
  331. result = {"classes": class_list,
  332. "outputRevision": REVISION}
  333. # Non-standard QML-related values for pyside6-build usage
  334. if visitor.qml_import_name():
  335. result[QML_IMPORT_NAME] = visitor.qml_import_name()
  336. qml_import_version = visitor.qml_import_version()
  337. if qml_import_version[0]:
  338. result[QML_IMPORT_MAJOR_VERSION] = qml_import_version[0]
  339. result[QML_IMPORT_MINOR_VERSION] = qml_import_version[1]
  340. qt_modules = visitor.qt_modules()
  341. if qt_modules:
  342. result[QT_MODULES] = qt_modules
  343. if not suppress_file:
  344. result["inputFile"] = os.fspath(file).replace("\\", "/")
  345. return result
  346. if __name__ == '__main__':
  347. arg_parser = create_arg_parser(DESCRIPTION)
  348. args = arg_parser.parse_args()
  349. context = VisitorContext()
  350. json_list = []
  351. for file_name in args.files:
  352. file = Path(file_name).resolve()
  353. if not file.is_file():
  354. print(f'{file_name} does not exist or is not a file.',
  355. file=sys.stderr)
  356. sys.exit(-1)
  357. try:
  358. json_data = parse_file(file, context, args.suppress_file)
  359. if json_data:
  360. json_list.append(json_data)
  361. elif not args.quiet:
  362. print(f"No classes found in {file_name}", file=sys.stderr)
  363. except (AttributeError, SyntaxError) as e:
  364. reason = str(e)
  365. print(f"Error parsing {file_name}: {reason}", file=sys.stderr)
  366. raise
  367. indent = None if args.compact else 4
  368. if args.out_file:
  369. with open(args.out_file, 'w') as f:
  370. json.dump(json_list, f, indent=indent)
  371. else:
  372. json.dump(json_list, sys.stdout, indent=indent)