diff options
author | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2022-04-04 16:55:52 +0200 |
---|---|---|
committer | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2022-06-14 12:34:08 +0200 |
commit | d78151f89b2c374af366bee536c8ceeae3b2ab5e (patch) | |
tree | 807473b4c90cd1c324f95ffead8516f1bb6f2150 /sources | |
parent | 73adefe22ffbfabe0ef213e9c2fe2c56efdd7488 (diff) |
tools: add pyside6-qml
- pyside6-qml is a tool that mimics the capabilities of qml utility and enables
quick prototyping for qml files. Most cli options of the qml tool are carried
forward to this tool.
example-usage:
pyside6-qml -a gui examples/declarative/editingmodel/main.qml
To see all the cli options available with this tool, do:
pyside6-qml --help
Task-number: PYSIDE-1878
Pick-to: 6.3
Change-Id: I98bd77ccf6a0a286bb54da264312e81bf2964dc7
Reviewed-by: Christian Tismer <tismer@stackless.com>
Diffstat (limited to 'sources')
-rw-r--r-- | sources/pyside-tools/CMakeLists.txt | 3 | ||||
-rw-r--r-- | sources/pyside-tools/pyside_tool.py | 5 | ||||
-rw-r--r-- | sources/pyside-tools/qml.py | 246 | ||||
-rw-r--r-- | sources/pyside6/tests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | sources/pyside6/tests/tools/__init__.py | 1 | ||||
-rw-r--r-- | sources/pyside6/tests/tools/pyside6-qml/CMakeLists.txt | 1 | ||||
-rw-r--r-- | sources/pyside6/tests/tools/pyside6-qml/test_pyside6_qml.py | 75 |
7 files changed, 331 insertions, 1 deletions
diff --git a/sources/pyside-tools/CMakeLists.txt b/sources/pyside-tools/CMakeLists.txt index c5195a1ee..281daf68a 100644 --- a/sources/pyside-tools/CMakeLists.txt +++ b/sources/pyside-tools/CMakeLists.txt @@ -5,7 +5,8 @@ include(cmake/PySideToolsSetup.cmake) set(files ${CMAKE_CURRENT_SOURCE_DIR}/pyside_tool.py ${CMAKE_CURRENT_SOURCE_DIR}/metaobjectdump.py - ${CMAKE_CURRENT_SOURCE_DIR}/project.py) + ${CMAKE_CURRENT_SOURCE_DIR}/project.py + ${CMAKE_CURRENT_SOURCE_DIR}/qml.py) set(directories) if(NOT NO_QT_TOOLS STREQUAL "yes") diff --git a/sources/pyside-tools/pyside_tool.py b/sources/pyside-tools/pyside_tool.py index 4864f365f..5e930f1e2 100644 --- a/sources/pyside-tools/pyside_tool.py +++ b/sources/pyside-tools/pyside_tool.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # 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 sys import os from pathlib import Path @@ -145,5 +146,9 @@ def project(): pyside_script_wrapper("project.py") +def qml(): + pyside_script_wrapper("qml.py") + + if __name__ == "__main__": main() diff --git a/sources/pyside-tools/qml.py b/sources/pyside-tools/qml.py new file mode 100644 index 000000000..61e0e8ff9 --- /dev/null +++ b/sources/pyside-tools/qml.py @@ -0,0 +1,246 @@ +# Copyright (C) 2018 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 + +"""pyside6-qml tool implementation. This tool mimics the capabilities of qml runtime utility +for python and enables quick protyping with python modules""" + +import argparse +import importlib.util +import logging +import sys +import os +from pathlib import Path +from pprint import pprint +from typing import List, Set + +from PySide6.QtCore import QCoreApplication, Qt, QLibraryInfo, QUrl, SignalInstance +from PySide6.QtGui import QGuiApplication, QSurfaceFormat +from PySide6.QtQml import QQmlApplicationEngine, QQmlComponent +from PySide6.QtQuick import QQuickView, QQuickWindow +from PySide6.QtWidgets import QApplication + + +def import_qml_modules(qml_parent_path: Path, module_paths: List[Path] = []): + ''' + Import all the python modules in the qml_parent_path. This way all the classes + containing the @QmlElement/@QmlNamedElement are also imported + + Parameters: + qml_parent_path (Path): Parent directory of the qml file + module_paths (int): user give import paths obtained through cli + ''' + + search_dir_paths = [] + search_file_paths = [] + + if not module_paths: + search_dir_paths.append(qml_parent_path) + else: + for module_path in module_paths: + if module_path.is_dir(): + search_dir_paths.append(module_path) + elif module_path.exists() and module_path.suffix == ".py": + search_file_paths.append(module_path) + + def import_module(import_module_paths: Set[Path]): + """Import the modules in 'import_module_paths'""" + for module_path in import_module_paths: + module_name = module_path.name[:-3] + _spec = importlib.util.spec_from_file_location(f"{module_name}", module_path) + _module = importlib.util.module_from_spec(_spec) + _spec.loader.exec_module(module=_module) + + modules_to_import = set() + for search_path in search_dir_paths: + possible_modules = list(search_path.glob("**/*.py")) + for possible_module in possible_modules: + if possible_module.is_file() and possible_module.name != "__init__.py": + module_parent = str(possible_module.parent) + if module_parent not in sys.path: + sys.path.append(module_parent) + modules_to_import.add(possible_module) + + for search_path in search_file_paths: + sys.path.append(str(search_path.parent)) + modules_to_import.add(search_path) + + import_module(import_module_paths=modules_to_import) + + +def print_configurations(): + return "Built-in configurations \n\t default \n\t resizeToItem" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="This tools mimics the capabilities of qml runtime utility by directly" + " invoking QQmlEngine/QQuickView. It enables quick prototyping with qml files.", + formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument( + "file", + type=lambda p: Path(p).absolute(), + help="Path to qml file to display", + ) + parser.add_argument( + "--module-paths", "-I", + type=lambda p: Path(p).absolute(), + nargs="+", + help="Specify space separated folder/file paths where the Qml classes are defined. By" + " default,the parent directory of the qml_path is searched recursively for all .py" + " files and they are imported. Otherwise only the paths give in module paths are" + " searched", + ) + parser.add_argument( + "--list-conf", + action="version", + help="List the built-in configurations.", + version=print_configurations() + ) + parser.add_argument( + "--apptype", "-a", + choices=["core", "gui", "widget"], + default="gui", + help="Select which application class to use. Default is gui", + ) + parser.add_argument( + "--config", "-c", + choices=["default", "resizeToItem"], + default="default", + help="Select the built-in configurations.", + ) + parser.add_argument( + "--rhi", "-r", + choices=["vulkan", "metal", "d3dll", "gl"], + help="Set the backend for the Qt graphics abstraction (RHI).", + ) + parser.add_argument( + "--core-profile", + action="store_true", + help="Force use of OpenGL Core Profile.", + ) + parser.add_argument( + '-v', '--verbose', + help="Print information about what qml is doing, like specific file URLs being loaded.", + action="store_const", dest="loglevel", const=logging.INFO, + ) + + gl_group = parser.add_mutually_exclusive_group(required=False) + gl_group.add_argument( + "--gles", + action="store_true", + help="Force use of GLES (AA_UseOpenGLES)", + ) + gl_group.add_argument( + "--desktop", + action="store_true", + help="Force use of desktop OpenGL (AA_UseDesktopOpenGL)", + ) + gl_group.add_argument( + "--software", + action="store_true", + help="Force use of software rendering(AA_UseSoftwareOpenGL)", + ) + gl_group.add_argument( + "--disable-context-sharing", + action="store_true", + help=" Disable the use of a shared GL context for QtQuick Windows", + ) + + args = parser.parse_args() + apptype = args.apptype + + qquick_present = False + + with open(args.file) as myfile: + if 'import QtQuick' in myfile.read(): + qquick_present = True + + # no import QtQuick => QQCoreApplication + if not qquick_present: + apptype = "core" + + import_qml_modules(args.file.parent, args.module_paths) + + logging.basicConfig(level=args.loglevel) + logging.info(f"qml: {QLibraryInfo.build()}") + logging.info(f"qml: Using built-in configuration: {args.config}") + + if args.rhi: + os.environ['QSG_RHI_BACKEND'] = args.rhi + + logging.info(f"qml: loading {args.file}") + qml_file = QUrl.fromLocalFile(str(args.file)) + + if apptype == "gui": + if args.gles: + logging.info("qml: Using attribute AA_UseOpenGLES") + QCoreApplication.setAttribute(Qt.AA_UseOpenGLES) + elif args.desktop: + logging.info("qml: Using attribute AA_UseDesktopOpenGL") + QCoreApplication.setAttribute(Qt.AA_UseDesktopOpenGL) + elif args.software: + logging.info("qml: Using attribute AA_UseSoftwareOpenGL") + QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL) + + # context-sharing is enabled by default + if not args.disable_context_sharing: + logging.info("qml: Using attribute AA_ShareOpenGLContexts") + QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) + + if apptype == "core": + logging.info("qml: Core application") + app = QCoreApplication(sys.argv) + elif apptype == "widgets": + logging.info("qml: Widget application") + app = QApplication(sys.argv) + else: + logging.info("qml: Gui application") + app = QGuiApplication(sys.argv) + + engine = QQmlApplicationEngine() + + # set OpenGLContextProfile + if apptype == "gui" and args.core_profile: + logging.info("qml: Set profile for QSurfaceFormat as CoreProfile") + surfaceFormat = QSurfaceFormat() + surfaceFormat.setStencilBufferSize(8) + surfaceFormat.setDepthBufferSize(24) + surfaceFormat.setVersion(4, 1) + surfaceFormat.setProfile(QSurfaceFormat.CoreProfile) + QSurfaceFormat.setDefaultFormat(surfaceFormat) + + # in the case of QCoreApplication we print the attributes of the object created via + # QQmlComponent and exit + if apptype == "core": + component = QQmlComponent(engine, qml_file) + obj = component.create() + filtered_attributes = {k: v for k, v in vars(obj).items() if type(v) != SignalInstance} + logging.info("qml: component object attributes are") + pprint(filtered_attributes) + del engine + sys.exit(0) + + engine.load(qml_file) + rootObjects = engine.rootObjects() + if not rootObjects: + sys.exit(-1) + + qquick_view = False + if type(rootObjects[0]) != QQuickWindow and qquick_present: + logging.info("qml: loading with QQuickView") + viewer = QQuickView() + viewer.setSource(qml_file) + if args.config != "resizeToItem": + viewer.setResizeMode(QQuickView.SizeRootObjectToView) + else: + viewer.setResizeMode(QQuickView.SizeViewToRootObject) + viewer.show() + qquick_view = True + + if not qquick_view: + logging.info("qml: loading with QQmlApplicationEngine") + if args.config == "resizeToItem": + logging.info("qml: Not a QQuickview item. resizeToItem is done by default") + + sys.exit(app.exec()) diff --git a/sources/pyside6/tests/CMakeLists.txt b/sources/pyside6/tests/CMakeLists.txt index a1bfecfd9..0d5b6fa54 100644 --- a/sources/pyside6/tests/CMakeLists.txt +++ b/sources/pyside6/tests/CMakeLists.txt @@ -49,6 +49,7 @@ add_subdirectory(registry) add_subdirectory(signals) add_subdirectory(support) add_subdirectory(tools/metaobjectdump) +add_subdirectory(tools/pyside6-qml) foreach(shortname IN LISTS all_module_shortnames) message(STATUS "preparing tests for module 'Qt${shortname}'") diff --git a/sources/pyside6/tests/tools/__init__.py b/sources/pyside6/tests/tools/__init__.py new file mode 100644 index 000000000..31f792369 --- /dev/null +++ b/sources/pyside6/tests/tools/__init__.py @@ -0,0 +1 @@ +from init_paths import init_test_paths diff --git a/sources/pyside6/tests/tools/pyside6-qml/CMakeLists.txt b/sources/pyside6/tests/tools/pyside6-qml/CMakeLists.txt new file mode 100644 index 000000000..4d801264a --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-qml/CMakeLists.txt @@ -0,0 +1 @@ +PYSIDE_TEST(test_pyside6_qml.py) diff --git a/sources/pyside6/tests/tools/pyside6-qml/test_pyside6_qml.py b/sources/pyside6/tests/tools/pyside6-qml/test_pyside6_qml.py new file mode 100644 index 000000000..701f8f215 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-qml/test_pyside6_qml.py @@ -0,0 +1,75 @@ +# Copyright (C) 2018 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 + +"""Test for pyside6-qml + +The tests does a unittest and some integration tests for pyside6-qml.""" + +from asyncio.subprocess import PIPE +import os +import sys +import unittest +import subprocess +import importlib.util + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[2])) +from init_paths import init_test_paths +init_test_paths(False) + + +class TestPySide6QmlUnit(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + self._dir = Path(__file__).parent.resolve() + self.pyside_root = self._dir.parents[4] + + self.pyqml_path = self.pyside_root / "sources" / "pyside-tools" / "qml.py" + self.core_qml_path = (self.pyside_root / "examples" / "declarative" / "referenceexamples" + / "adding") + + self.pyqml_run_cmd = [sys.executable, os.fspath(self.pyqml_path)] + + # self.pyqml_path will not abe able to find pyside and other related binaries, if not added + # to path explicitly. The following lines does that. + self.test_env = os.environ.copy() + self.test_env["PYTHONPATH"] = os.pathsep + os.pathsep.join(sys.path) + + def testImportQmlModules(self): + + # because pyside-tools has a hyphen, a normal 'from pyside-tools import qml' cannot be done + spec = importlib.util.spec_from_file_location("qml", self.pyqml_path) + pyqml = importlib.util.module_from_spec(spec) + spec.loader.exec_module(pyqml) + pyqml.import_qml_modules(self.core_qml_path) + + # path added to sys.path + self.assertIn(str(self.core_qml_path), sys.path) + + # module is imported + self.assertIn("person", sys.modules.keys()) + + # remove the imported modules + sys.path.remove(str(self.core_qml_path)) + del sys.modules["person"] + + # test with module_paths - dir + self.person_path = self.core_qml_path / "person.py" + pyqml.import_qml_modules(self.core_qml_path, module_paths=[self.core_qml_path]) + self.assertIn(str(self.core_qml_path), sys.path) + self.assertIn("person", sys.modules.keys()) + + # test with module_paths - file - in testCoreApplication(self) + + def testCoreApplication(self): + self.pyqml_run_cmd.extend(["--apptype", "core"]) + self.pyqml_run_cmd.append(str(self.core_qml_path / "example.qml")) + self.pyqml_run_cmd.extend(["-I", str(self.core_qml_path / "person.py")]) + + result = subprocess.run(self.pyqml_run_cmd, stdout=PIPE, env=self.test_env) + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout.rstrip(), b"{'_name': 'Bob Jones', '_shoe_size': 12}") + + +if __name__ == '__main__': + unittest.main() |