diff options
Diffstat (limited to 'sources/pyside-tools/metaobjectdump.py')
-rw-r--r-- | sources/pyside-tools/metaobjectdump.py | 452 |
1 files changed, 452 insertions, 0 deletions
diff --git a/sources/pyside-tools/metaobjectdump.py b/sources/pyside-tools/metaobjectdump.py new file mode 100644 index 000000000..0970f9974 --- /dev/null +++ b/sources/pyside-tools/metaobjectdump.py @@ -0,0 +1,452 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import ast +import json +import os +import sys +import tokenize +from argparse import ArgumentParser, RawTextHelpFormatter +from pathlib import Path +from typing import Dict, List, Optional, Set, Tuple, Union + + +DESCRIPTION = """Parses Python source code to create QObject metatype +information in JSON format for qmltyperegistrar.""" + + +REVISION = 68 + + +CPP_TYPE_MAPPING = {"str": "QString"} + + +QML_IMPORT_NAME = "QML_IMPORT_NAME" +QML_IMPORT_MAJOR_VERSION = "QML_IMPORT_MAJOR_VERSION" +QML_IMPORT_MINOR_VERSION = "QML_IMPORT_MINOR_VERSION" +QT_MODULES = "QT_MODULES" + + +ITEM_MODELS = ["QAbstractListModel", "QAbstractProxyModel", + "QAbstractTableModel", "QConcatenateTablesProxyModel", + "QFileSystemModel", "QIdentityProxyModel", "QPdfBookmarkModel", + "QPdfSearchModel", "QSortFilterProxyModel", "QSqlQueryModel", + "QStandardItemModel", "QStringListModel", "QTransposeProxyModel", + "QWebEngineHistoryModel"] + + +QOBJECT_DERIVED = ["QObject", "QQuickItem", "QQuickPaintedItem"] + ITEM_MODELS + + +AstDecorator = Union[ast.Name, ast.Call] +AstPySideTypeSpec = Union[ast.Name, ast.Constant] + + +ClassList = List[dict] + + +PropertyEntry = Dict[str, Union[str, int, bool]] + +Argument = Dict[str, str] +Arguments = List[Argument] +Signal = Dict[str, Union[str, Arguments]] +Slot = Dict[str, Union[str, Arguments]] + + +def _decorator(name: str, value: str) -> Dict[str, str]: + """Create a QML decorator JSON entry""" + return {"name": name, "value": value} + + +def _attribute(node: ast.Attribute) -> Tuple[str, str]: + """Split an attribute.""" + return node.value.id, node.attr + + +def _name(node: Union[ast.Name, ast.Attribute]) -> str: + """Return the name of something that is either an attribute or a name, + such as base classes or call.func""" + if isinstance(node, ast.Attribute): + qualifier, name = _attribute(node) + return f"{qualifier}.{node.attr}" + return node.id + + +def _func_name(node: ast.Call) -> str: + return _name(node.func) + + +def _python_to_cpp_type(type: str) -> str: + """Python to C++ type""" + c = CPP_TYPE_MAPPING.get(type) + return c if c else type + + +def _parse_property_kwargs(keywords: List[ast.keyword], prop: PropertyEntry): + """Parse keyword arguments of @Property""" + for k in keywords: + if k.arg == "notify": + prop["notify"] = _name(k.value) + + +def _parse_assignment(node: ast.Assign) -> Tuple[Optional[str], Optional[ast.AST]]: + """Parse an assignment and return a tuple of name, value.""" + if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): + var_name = node.targets[0].id + return (var_name, node.value) + return (None, None) + + +def _parse_pyside_type(type_spec: AstPySideTypeSpec) -> str: + """Parse type specification of a Slot/Property decorator. Usually a type, + but can also be a string constant with a C++ type name.""" + if isinstance(type_spec, ast.Constant): + return type_spec.value + return _python_to_cpp_type(_name(type_spec)) + + +def _parse_call_args(call: ast.Call): + """Parse arguments of a Signal call/Slot decorator (type list).""" + result: Arguments = [] + for n, arg in enumerate(call.args): + par_name = f"a{n+1}" + par_type = _parse_pyside_type(arg) + result.append({"name": par_name, "type": par_type}) + return result + + +def _parse_slot(func_name: str, call: ast.Call) -> Slot: + """Parse a 'Slot' decorator.""" + return_type = "void" + for kwarg in call.keywords: + if kwarg.arg == "result": + return_type = _python_to_cpp_type(_name(kwarg.value)) + break + return {"access": "public", "name": func_name, + "arguments": _parse_call_args(call), + "returnType": return_type} + + +class VisitorContext: + """Stores a list of QObject-derived classes encountered in order to find + out which classes inherit QObject.""" + + def __init__(self): + self.qobject_derived = QOBJECT_DERIVED + + +class MetaObjectDumpVisitor(ast.NodeVisitor): + """AST visitor for parsing sources and creating the data structure for + JSON.""" + + def __init__(self, context: VisitorContext): + super().__init__() + self._context = context + self._json_class_list: ClassList = [] + # Property by name, which will be turned into the JSON List later + self._properties: List[PropertyEntry] = [] + self._signals: List[Signal] = [] + self._within_class: bool = False + self._qt_modules: Set[str] = set() + self._qml_import_name = "" + self._qml_import_major_version = 0 + self._qml_import_minor_version = 0 + + def json_class_list(self) -> ClassList: + return self._json_class_list + + def qml_import_name(self) -> str: + return self._qml_import_name + + def qml_import_version(self) -> Tuple[int, int]: + return (self._qml_import_major_version, self._qml_import_minor_version) + + def qt_modules(self): + return sorted(self._qt_modules) + + @staticmethod + def create_ast(filename: Path) -> ast.Module: + """Create an Abstract Syntax Tree on which a visitor can be run""" + node = None + with tokenize.open(filename) as file: + node = ast.parse(file.read(), mode="exec") + return node + + def visit_Assign(self, node: ast.Assign): + """Parse the global constants for QML-relevant values""" + var_name, value_node = _parse_assignment(node) + if not var_name or not isinstance(value_node, ast.Constant): + return + value = value_node.value + if var_name == QML_IMPORT_NAME: + self._qml_import_name = value + elif var_name == QML_IMPORT_MAJOR_VERSION: + self._qml_import_major_version = value + elif var_name == QML_IMPORT_MINOR_VERSION: + self._qml_import_minor_version = value + + def visit_ClassDef(self, node: ast.Module): + """Visit a class definition""" + self._properties = [] + self._signals = [] + self._slots = [] + self._within_class = True + qualified_name = node.name + last_dot = qualified_name.rfind('.') + name = (qualified_name[last_dot + 1:] if last_dot != -1 + else qualified_name) + + data = {"className": name, + "qualifiedClassName": qualified_name} + + q_object = False + bases = [] + for b in node.bases: + # PYSIDE-2202: catch weird constructs like "class C(type(Base)):" + if isinstance(b, ast.Name): + base_name = _name(b) + if base_name in self._context.qobject_derived: + q_object = True + self._context.qobject_derived.append(name) + base_dict = {"access": "public", "name": base_name} + bases.append(base_dict) + + data["object"] = q_object + if bases: + data["superClasses"] = bases + + class_decorators: List[dict] = [] + for d in node.decorator_list: + self._parse_class_decorator(d, class_decorators) + + if class_decorators: + data["classInfos"] = class_decorators + + for b in node.body: + if isinstance(b, ast.Assign): + self._parse_class_variable(b) + else: + self.visit(b) + + if self._properties: + data["properties"] = self._properties + + if self._signals: + data["signals"] = self._signals + + if self._slots: + data["slots"] = self._slots + + self._json_class_list.append(data) + + self._within_class = False + + def visit_FunctionDef(self, node): + if self._within_class: + for d in node.decorator_list: + self._parse_function_decorator(node.name, d) + + def _parse_class_decorator(self, node: AstDecorator, + class_decorators: List[dict]): + """Parse ClassInfo decorators.""" + if isinstance(node, ast.Call): + name = _func_name(node) + if name == "QmlUncreatable": + class_decorators.append(_decorator("QML.Creatable", "false")) + if node.args: + reason = node.args[0].value + if isinstance(reason, str): + d = _decorator("QML.UncreatableReason", reason) + class_decorators.append(d) + elif name == "QmlAttached" and len(node.args) == 1: + d = _decorator("QML.Attached", node.args[0].id) + class_decorators.append(d) + elif name == "QmlExtended" and len(node.args) == 1: + d = _decorator("QML.Extended", node.args[0].id) + class_decorators.append(d) + elif name == "ClassInfo" and node.keywords: + kw = node.keywords[0] + class_decorators.append(_decorator(kw.arg, kw.value.value)) + elif name == "QmlForeign" and len(node.args) == 1: + d = _decorator("QML.Foreign", node.args[0].id) + class_decorators.append(d) + elif name == "QmlNamedElement" and node.args: + name = node.args[0].value + class_decorators.append(_decorator("QML.Element", name)) + elif name.startswith('Q'): + print('Unknown decorator with parameters:', name, + file=sys.stderr) + return + + if isinstance(node, ast.Name): + name = node.id + if name == "QmlElement": + class_decorators.append(_decorator("QML.Element", "auto")) + elif name == "QmlSingleton": + class_decorators.append(_decorator("QML.Singleton", "true")) + elif name == "QmlAnonymous": + class_decorators.append(_decorator("QML.Element", "anonymous")) + elif name.startswith('Q'): + print('Unknown decorator:', name, file=sys.stderr) + return + + def _index_of_property(self, name: str) -> int: + """Search a property by name""" + for i in range(len(self._properties)): + if self._properties[i]["name"] == name: + return i + return -1 + + def _create_property_entry(self, name: str, type: str, + getter: Optional[str] = None) -> PropertyEntry: + """Create a property JSON entry.""" + result: PropertyEntry = {"name": name, "type": type, + "index": len(self._properties)} + if getter: + result["read"] = getter + return result + + def _parse_function_decorator(self, func_name: str, node: AstDecorator): + """Parse function decorators.""" + if isinstance(node, ast.Attribute): + name = node.value.id + value = node.attr + if value == "setter": # Property setter + idx = self._index_of_property(name) + if idx != -1: + self._properties[idx]["write"] = func_name + return + + if isinstance(node, ast.Call): + name = _name(node.func) + if name == "Property": # Property getter + if node.args: # 1st is type/type string + type = _parse_pyside_type(node.args[0]) + prop = self._create_property_entry(func_name, type, + func_name) + _parse_property_kwargs(node.keywords, prop) + self._properties.append(prop) + elif name == "Slot": + self._slots.append(_parse_slot(func_name, node)) + else: + print('Unknown decorator with parameters:', name, + file=sys.stderr) + + def _parse_class_variable(self, node: ast.Assign): + """Parse a class variable assignment (Property, Signal, etc.)""" + (var_name, call) = _parse_assignment(node) + if not var_name or not isinstance(node.value, ast.Call): + return + func_name = _func_name(call) + if func_name == "Signal" or func_name == "QtCore.Signal": + signal: Signal = {"access": "public", "name": var_name, + "arguments": _parse_call_args(call), + "returnType": "void"} + self._signals.append(signal) + elif func_name == "Property" or func_name == "QtCore.Property": + type = _python_to_cpp_type(call.args[0].id) + prop = self._create_property_entry(var_name, type, call.args[1].id) + if len(call.args) > 2: + prop["write"] = call.args[2].id + _parse_property_kwargs(call.keywords, prop) + self._properties.append(prop) + elif func_name == "ListProperty" or func_name == "QtCore.ListProperty": + type = _python_to_cpp_type(call.args[0].id) + type = f"QQmlListProperty<{type}>" + prop = self._create_property_entry(var_name, type) + self._properties.append(prop) + + def visit_Import(self, node): + for n in node.names: # "import PySide6.QtWidgets" + self._handle_import(n.name) + + def visit_ImportFrom(self, node): + if "." in node.module: # "from PySide6.QtWidgets import QWidget" + self._handle_import(node.module) + elif node.module == "PySide6": # "from PySide6 import QtWidgets" + for n in node.names: + if n.name.startswith("Qt"): + self._qt_modules.add(n.name) + + def _handle_import(self, mod: str): + if mod.startswith("PySide6."): + self._qt_modules.add(mod[8:]) + + +def create_arg_parser(desc: str) -> ArgumentParser: + parser = ArgumentParser(description=desc, + formatter_class=RawTextHelpFormatter) + parser.add_argument('--compact', '-c', action='store_true', + help='Use compact format') + parser.add_argument('--suppress-file', '-s', action='store_true', + help='Suppress inputFile entry (for testing)') + parser.add_argument('--quiet', '-q', action='store_true', + help='Suppress warnings') + parser.add_argument('files', type=str, nargs="+", + help='Python source file') + parser.add_argument('--out-file', '-o', type=str, + help='Write output to file rather than stdout') + return parser + + +def parse_file(file: Path, context: VisitorContext, + suppress_file: bool = False) -> Optional[Dict]: + """Parse a file and return its json data""" + ast_tree = MetaObjectDumpVisitor.create_ast(file) + visitor = MetaObjectDumpVisitor(context) + visitor.visit(ast_tree) + + class_list = visitor.json_class_list() + if not class_list: + return None + result = {"classes": class_list, + "outputRevision": REVISION} + + # Non-standard QML-related values for pyside6-build usage + if visitor.qml_import_name(): + result[QML_IMPORT_NAME] = visitor.qml_import_name() + qml_import_version = visitor.qml_import_version() + if qml_import_version[0]: + result[QML_IMPORT_MAJOR_VERSION] = qml_import_version[0] + result[QML_IMPORT_MINOR_VERSION] = qml_import_version[1] + + qt_modules = visitor.qt_modules() + if qt_modules: + result[QT_MODULES] = qt_modules + + if not suppress_file: + result["inputFile"] = os.fspath(file).replace("\\", "/") + return result + + +if __name__ == '__main__': + arg_parser = create_arg_parser(DESCRIPTION) + args = arg_parser.parse_args() + + context = VisitorContext() + json_list = [] + + for file_name in args.files: + file = Path(file_name).resolve() + if not file.is_file(): + print(f'{file_name} does not exist or is not a file.', + file=sys.stderr) + sys.exit(-1) + + try: + json_data = parse_file(file, context, args.suppress_file) + if json_data: + json_list.append(json_data) + elif not args.quiet: + print(f"No classes found in {file_name}", file=sys.stderr) + except (AttributeError, SyntaxError) as e: + reason = str(e) + print(f"Error parsing {file_name}: {reason}", file=sys.stderr) + raise + + indent = None if args.compact else 4 + if args.out_file: + with open(args.out_file, 'w') as f: + json.dump(json_list, f, indent=indent) + else: + json.dump(json_list, sys.stdout, indent=indent) |