aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFriedemann Kleint <[email protected]>2021-12-17 14:31:34 +0100
committerFriedemann Kleint <[email protected]>2022-01-27 22:43:22 +0100
commitf9447722afe6911ef705994abc31970d7a004414 (patch)
tree252a31e2cd1c68642786f4cba630f007e6262d98
parentfa799cbe62587091ef5d90900b37f4ae9a34554b (diff)
Long live pyside6-metaobjectdump!
Add a tool to print out the metatype information in JSON to be used as input for qmltyperegistrar. Task-number: PYSIDE-1709 Change-Id: Ie57feeeecc09b1a01aadcc08f7e529a16609b3a4 Reviewed-by: Christian Tismer <[email protected]>
-rw-r--r--build_scripts/config.py2
-rw-r--r--build_scripts/platforms/unix.py8
-rw-r--r--build_scripts/platforms/windows_desktop.py8
-rw-r--r--sources/pyside-tools/CMakeLists.txt3
-rw-r--r--sources/pyside-tools/metaobjectdump.py428
-rw-r--r--sources/pyside-tools/pyside_tool.py11
-rw-r--r--sources/pyside6/tests/CMakeLists.txt1
-rw-r--r--sources/pyside6/tests/tools/metaobjectdump/CMakeLists.txt1
-rw-r--r--sources/pyside6/tests/tools/metaobjectdump/baseline_coercion_birthdayparty.json1
-rw-r--r--sources/pyside6/tests/tools/metaobjectdump/baseline_coercion_person.json1
-rw-r--r--sources/pyside6/tests/tools/metaobjectdump/baseline_default_birthdayparty.json1
-rw-r--r--sources/pyside6/tests/tools/metaobjectdump/baseline_default_person.json1
-rw-r--r--sources/pyside6/tests/tools/metaobjectdump/test_metaobjectdump.py93
13 files changed, 549 insertions, 10 deletions
diff --git a/build_scripts/config.py b/build_scripts/config.py
index 0a48e84b6..ad3dd1542 100644
--- a/build_scripts/config.py
+++ b/build_scripts/config.py
@@ -223,7 +223,7 @@ class Config(object):
f'{PYSIDE}-lupdate = {package_name}.scripts.pyside_tool:lupdate',
f'{PYSIDE}-lrelease = {package_name}.scripts.pyside_tool:lrelease',
f'{PYSIDE}-genpyi = {package_name}.scripts.pyside_tool:genpyi',
- f'{PYSIDE}-moc = {package_name}.scripts.pyside_tool:moc',
+ f'{PYSIDE}-metaobjectdump = {package_name}.scripts.pyside_tool:metaobjectdump',
f'{PYSIDE}-qmltyperegistrar = {package_name}.scripts.pyside_tool:qmltyperegistrar',
f'{PYSIDE}-qmllint = {package_name}.scripts.pyside_tool:qmllint'
]
diff --git a/build_scripts/platforms/unix.py b/build_scripts/platforms/unix.py
index 57163e58f..da418503b 100644
--- a/build_scripts/platforms/unix.py
+++ b/build_scripts/platforms/unix.py
@@ -148,10 +148,10 @@ def prepare_packages_posix(self, vars):
vars=vars)
# For setting up setuptools entry points
- copyfile(
- "{install_dir}/bin/pyside_tool.py",
- "{st_build_dir}/{st_package_name}/scripts/pyside_tool.py",
- force=False, vars=vars)
+ for script in ("pyside_tool.py", "metaobjectdump.py"):
+ src = f"{{install_dir}}/bin/{script}"
+ target = f"{{st_build_dir}}/{{st_package_name}}/scripts/{script}"
+ copyfile(src, target, force=False, vars=vars)
# <install>/bin/* -> {st_package_name}/
executables.extend(copydir(
diff --git a/build_scripts/platforms/windows_desktop.py b/build_scripts/platforms/windows_desktop.py
index b57b82828..d14ab6044 100644
--- a/build_scripts/platforms/windows_desktop.py
+++ b/build_scripts/platforms/windows_desktop.py
@@ -152,10 +152,10 @@ def prepare_packages_win32(self, vars):
vars=vars)
# For setting up setuptools entry points
- copyfile(
- "{install_dir}/bin/pyside_tool.py",
- "{st_build_dir}/{st_package_name}/scripts/pyside_tool.py",
- force=False, vars=vars)
+ for script in ("pyside_tool.py", "metaobjectdump.py"):
+ src = f"{{install_dir}}/bin/{script}"
+ target = f"{{st_build_dir}}/{{st_package_name}}/scripts/{script}"
+ copyfile(src, target, force=False, vars=vars)
# <install>/bin/*.exe,*.dll -> {st_package_name}/
filters = ["pyside*.exe", "pyside*.dll"]
diff --git a/sources/pyside-tools/CMakeLists.txt b/sources/pyside-tools/CMakeLists.txt
index f4667c479..384985125 100644
--- a/sources/pyside-tools/CMakeLists.txt
+++ b/sources/pyside-tools/CMakeLists.txt
@@ -8,7 +8,8 @@ endif()
find_package(Qt6 COMPONENTS Core HostInfo)
-set(files ${CMAKE_CURRENT_SOURCE_DIR}/pyside_tool.py)
+set(files ${CMAKE_CURRENT_SOURCE_DIR}/pyside_tool.py
+ ${CMAKE_CURRENT_SOURCE_DIR}/metaobjectdump.py)
set(directories)
if(NOT NO_QT_TOOLS STREQUAL "yes")
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)
diff --git a/sources/pyside-tools/pyside_tool.py b/sources/pyside-tools/pyside_tool.py
index aa73affd0..e05a4d586 100644
--- a/sources/pyside-tools/pyside_tool.py
+++ b/sources/pyside-tools/pyside_tool.py
@@ -75,6 +75,13 @@ def qt_tool_wrapper(qt_tool, args, libexec=False):
sys.exit(proc.returncode)
+def pyside_script_wrapper(script_name):
+ """Launch a script shipped with PySide."""
+ script = Path(__file__).resolve().parent / script_name
+ command = [sys.executable, os.fspath(script)] + sys.argv[1:]
+ sys.exit(subprocess.call(command))
+
+
def lrelease():
qt_tool_wrapper("lrelease", sys.argv[1:])
@@ -162,5 +169,9 @@ def genpyi():
sys.exit(subprocess.call(command))
+def metaobjectdump():
+ pyside_script_wrapper("metaobjectdump.py")
+
+
if __name__ == "__main__":
main()
diff --git a/sources/pyside6/tests/CMakeLists.txt b/sources/pyside6/tests/CMakeLists.txt
index 86150ac1f..c0b15b595 100644
--- a/sources/pyside6/tests/CMakeLists.txt
+++ b/sources/pyside6/tests/CMakeLists.txt
@@ -40,6 +40,7 @@ endif()
add_subdirectory(registry)
add_subdirectory(signals)
add_subdirectory(support)
+add_subdirectory(tools/metaobjectdump)
foreach(shortname IN LISTS all_module_shortnames)
message(STATUS "preparing tests for module 'Qt${shortname}'")
diff --git a/sources/pyside6/tests/tools/metaobjectdump/CMakeLists.txt b/sources/pyside6/tests/tools/metaobjectdump/CMakeLists.txt
new file mode 100644
index 000000000..f1ad6ab16
--- /dev/null
+++ b/sources/pyside6/tests/tools/metaobjectdump/CMakeLists.txt
@@ -0,0 +1 @@
+PYSIDE_TEST(test_metaobjectdump.py)
diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_coercion_birthdayparty.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_coercion_birthdayparty.json
new file mode 100644
index 000000000..ceddbab32
--- /dev/null
+++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_coercion_birthdayparty.json
@@ -0,0 +1 @@
+[{"classes": [{"className": "BirthdayParty", "qualifiedClassName": "BirthdayParty", "object": true, "superClasses": [{"access": "public", "name": "QObject"}], "classInfos": [{"name": "QML.Element", "value": "auto"}], "properties": [{"name": "host", "type": "Person", "index": 0, "read": "host", "write": "host"}, {"name": "guests", "type": "QQmlListProperty<Person>", "index": 1}]}], "outputRevision": 68, "QML_IMPORT_NAME": "examples.coercion.people", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}] \ No newline at end of file
diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_coercion_person.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_coercion_person.json
new file mode 100644
index 000000000..8b414b5a2
--- /dev/null
+++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_coercion_person.json
@@ -0,0 +1 @@
+[{"classes": [{"className": "Person", "qualifiedClassName": "Person", "object": true, "superClasses": [{"access": "public", "name": "QObject"}], "classInfos": [{"name": "QML.Element", "value": "auto"}, {"name": "QML.Creatable", "value": "false"}, {"name": "QML.UncreatableReason", "value": "Person is an abstract base class."}], "properties": [{"name": "name", "type": "QString", "index": 0, "read": "name", "write": "name"}, {"name": "shoe_size", "type": "int", "index": 1, "read": "shoe_size", "write": "shoe_size"}]}, {"className": "Boy", "qualifiedClassName": "Boy", "object": true, "superClasses": [{"access": "public", "name": "Person"}], "classInfos": [{"name": "QML.Element", "value": "auto"}]}, {"className": "Girl", "qualifiedClassName": "Girl", "object": true, "superClasses": [{"access": "public", "name": "Person"}], "classInfos": [{"name": "QML.Element", "value": "auto"}]}], "outputRevision": 68, "QML_IMPORT_NAME": "examples.coercion.people", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}] \ No newline at end of file
diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_default_birthdayparty.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_default_birthdayparty.json
new file mode 100644
index 000000000..96335feb9
--- /dev/null
+++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_default_birthdayparty.json
@@ -0,0 +1 @@
+[{"classes": [{"className": "BirthdayParty", "qualifiedClassName": "BirthdayParty", "object": true, "superClasses": [{"access": "public", "name": "QObject"}], "classInfos": [{"name": "QML.Element", "value": "auto"}, {"name": "DefaultProperty", "value": "guests"}], "properties": [{"name": "host", "type": "Person", "index": 0, "read": "host", "write": "host"}, {"name": "guests", "type": "QQmlListProperty<Person>", "index": 1}]}], "outputRevision": 68, "QML_IMPORT_NAME": "examples.default.people", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}] \ No newline at end of file
diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_default_person.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_default_person.json
new file mode 100644
index 000000000..1b3a15275
--- /dev/null
+++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_default_person.json
@@ -0,0 +1 @@
+[{"classes": [{"className": "Person", "qualifiedClassName": "Person", "object": true, "superClasses": [{"access": "public", "name": "QObject"}], "classInfos": [{"name": "QML.Element", "value": "anonymous"}], "properties": [{"name": "name", "type": "QString", "index": 0, "read": "name", "write": "name"}, {"name": "shoe_size", "type": "int", "index": 1, "read": "shoe_size", "write": "shoe_size"}]}, {"className": "Boy", "qualifiedClassName": "Boy", "object": true, "superClasses": [{"access": "public", "name": "Person"}], "classInfos": [{"name": "QML.Element", "value": "auto"}]}, {"className": "Girl", "qualifiedClassName": "Girl", "object": true, "superClasses": [{"access": "public", "name": "Person"}], "classInfos": [{"name": "QML.Element", "value": "auto"}]}], "outputRevision": 68, "QML_IMPORT_NAME": "examples.default.people", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}] \ No newline at end of file
diff --git a/sources/pyside6/tests/tools/metaobjectdump/test_metaobjectdump.py b/sources/pyside6/tests/tools/metaobjectdump/test_metaobjectdump.py
new file mode 100644
index 000000000..e7f76cca3
--- /dev/null
+++ b/sources/pyside6/tests/tools/metaobjectdump/test_metaobjectdump.py
@@ -0,0 +1,93 @@
+#############################################################################
+##
+## Copyright (C) 2021 The Qt Company Ltd.
+## Contact: https://www.qt.io/licensing/
+##
+## This file is part of the test suite of Qt for Python.
+##
+## $QT_BEGIN_LICENSE:GPL-EXCEPT$
+## 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 General Public License Usage
+## Alternatively, this file may be used under the terms of the GNU
+## General Public License version 3 as published by the Free Software
+## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+## 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-3.0.html.
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import os
+import sys
+import subprocess
+import unittest
+
+from pathlib import Path
+
+"""Test for pyside6-metaobjectdump.
+
+The test prints commands to regenerate the base line."""
+
+
+def msg_regenerate(cmd, baseline):
+ cmd_str = " ".join(cmd)
+ return (f"# Regenerate {baseline}\n"
+ f"{cmd_str} > {baseline}")
+
+
[email protected](sys.version_info < (3, 8), "Needs a recent ast module")
+class TestMetaObjectDump(unittest.TestCase):
+ """Test for the metaobjectdump tool. Compares the output of metaobjectdump.py for some
+ example files in compact format."""
+
+ def setUp(self):
+ super().setUp()
+ self._dir = Path(__file__).parent.resolve()
+ pyside_root = self._dir.parents[4]
+ self._metaobjectdump_tool = pyside_root / "sources" / "pyside-tools" / "metaobjectdump.py"
+ self._examples_dir = (pyside_root / "examples" /
+ "declarative" / "referenceexamples")
+
+ # Compile a list of examples (tuple [file, base line, command])
+ examples = []
+ for d in ["coercion", "default"]:
+ example_dir = self._examples_dir / d
+ examples.append(example_dir / "birthdayparty.py")
+ examples.append(example_dir / "person.py")
+
+ metaobjectdump_cmd_root = [sys.executable, os.fspath(self._metaobjectdump_tool), "-c", "-s"]
+ self._examples = []
+ for example in examples:
+ name = example.parent.name
+ baseline_name = f"baseline_{name}_{example.stem}.json"
+ baseline_path = self._dir / baseline_name
+ cmd = metaobjectdump_cmd_root + [os.fspath(example)]
+ self._examples.append((example, baseline_path, cmd))
+ print(msg_regenerate(cmd, baseline_path))
+
+ def testMetaObjectDump(self):
+ self.assertTrue(self._examples_dir.is_dir())
+ self.assertTrue(self._metaobjectdump_tool.is_file())
+
+ for example, baseline, cmd in self._examples:
+ self.assertTrue(example.is_file())
+ self.assertTrue(baseline.is_file())
+ baseline_data = baseline.read_text()
+
+ popen = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+ actual = popen.communicate()[0].decode("UTF-8")
+ self.assertEqual(popen.returncode, 0)
+ self.assertEqual(baseline_data, actual)
+
+
+if __name__ == '__main__':
+ unittest.main()