############################################################################# ## ## 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 == "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)) 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)