| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461 |
- # 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
- from __future__ import annotations
- import ast
- import json
- import os
- import sys
- import tokenize
- from argparse import ArgumentParser, RawTextHelpFormatter
- from pathlib import Path
- from typing import 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
- # Python 3.9 does not support this syntax, yet
- # AstDecorator = ast.Name | ast.Call
- # AstPySideTypeSpec = ast.Name | ast.Constant
- AstDecorator = Union[ast.Name, ast.Call]
- AstPySideTypeSpec = Union[ast.Name, ast.Constant]
- ClassList = list[dict]
- # PropertyEntry = dict[str, str | int | bool]
- PropertyEntry = dict[str, Union[str, int, bool]]
- Argument = dict[str, str]
- Arguments = list[Argument]
- # Signal = dict[str, str | Arguments]
- # Slot = dict[str, str | Arguments]
- 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: ast.Name | ast.Attribute | ast.Constant) -> 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.Constant):
- return str(node.value)
- 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[str | None, ast.AST | None]:
- """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: str | None = 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) -> dict | None:
- """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)
|