diff options
Diffstat (limited to 'sources/pyside-tools/metaobjectdump.py')
-rw-r--r-- | sources/pyside-tools/metaobjectdump.py | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/sources/pyside-tools/metaobjectdump.py b/sources/pyside-tools/metaobjectdump.py new file mode 100644 index 000000000..fd26c0a9d --- /dev/null +++ b/sources/pyside-tools/metaobjectdump.py @@ -0,0 +1,428 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python project. +## +## $QT_BEGIN_LICENSE:LGPL$ +## Commercial License Usage +## Licensees holding valid commercial Qt licenses may use this file in +## accordance with the commercial license agreement provided with the +## Software or, alternatively, in accordance with the terms contained in +## a written agreement between you and The Qt Company. For licensing terms +## and conditions see https://www.qt.io/terms-conditions. For further +## information use the contact form at https://www.qt.io/contact-us. +## +## GNU Lesser General Public License Usage +## Alternatively, this file may be used under the terms of the GNU Lesser +## General Public License version 3 as published by the Free Software +## Foundation and appearing in the file LICENSE.LGPL3 included in the +## packaging of this file. Please review the following information to +## ensure the GNU Lesser General Public License version 3 requirements +## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +## +## GNU General Public License Usage +## Alternatively, this file may be used under the terms of the GNU +## General Public License version 2.0 or (at your option) the GNU General +## Public license version 3 or any later version approved by the KDE Free +## Qt Foundation. The licenses are as published by the Free Software +## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +## included in the packaging of this file. Please review the following +## information to ensure the GNU General Public License requirements will +## be met: https://www.gnu.org/licenses/gpl-2.0.html and +## https://www.gnu.org/licenses/gpl-3.0.html. +## +## $QT_END_LICENSE$ +## +############################################################################# + +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, 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" + + +AstDecorator = Union[ast.Name, ast.Call] + + +ClassList = List[dict] + + +PropertyEntry = Dict[str, Union[str, int, bool]] + +SignalArgument = Dict[str, str] +SignalArguments = List[SignalArgument] +Signal = Dict[str, Union[str, SignalArguments]] + + +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) + + +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", "QQuickItem", "QQuickPaintedItem"] + + +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: List[str] = [] + 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 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._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: + 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 + + 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 == "ClassInfo" and node.keywords: + kw = node.keywords[0] + class_decorators.append(_decorator(kw.arg, kw.value.value)) + else: + 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")) + else: + 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 = node.func.id + if name == "Property": # Property getter + if node.args: # 1st is type + type = _python_to_cpp_type(_name(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": + pass + 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": + arguments: SignalArguments = [] + for n, arg in enumerate(call.args): + par_name = f"a{n+1}" + par_type = _python_to_cpp_type(_name(arg)) + arguments.append({"name": par_name, "type": par_type}) + signal: Signal = {"access": "public", "name": var_name, + "arguments": arguments, + "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): + if node.names: + self._handle_import(node.names[0].name) + + def visit_ImportFrom(self, node): + self._handle_import(node.module) + + def _handle_import(self, mod: str): + if mod.startswith('PySide'): + dot = mod.index(".") + self._qt_modules.append(mod[dot + 1:]) + + +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) |