aboutsummaryrefslogtreecommitdiffstats
path: root/sources
diff options
context:
space:
mode:
authorShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2022-04-04 16:55:52 +0200
committerShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2022-06-14 12:34:08 +0200
commitd78151f89b2c374af366bee536c8ceeae3b2ab5e (patch)
tree807473b4c90cd1c324f95ffead8516f1bb6f2150 /sources
parent73adefe22ffbfabe0ef213e9c2fe2c56efdd7488 (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.txt3
-rw-r--r--sources/pyside-tools/pyside_tool.py5
-rw-r--r--sources/pyside-tools/qml.py246
-rw-r--r--sources/pyside6/tests/CMakeLists.txt1
-rw-r--r--sources/pyside6/tests/tools/__init__.py1
-rw-r--r--sources/pyside6/tests/tools/pyside6-qml/CMakeLists.txt1
-rw-r--r--sources/pyside6/tests/tools/pyside6-qml/test_pyside6_qml.py75
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()