aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside-tools/project
diff options
context:
space:
mode:
Diffstat (limited to 'sources/pyside-tools/project')
-rw-r--r--sources/pyside-tools/project/__init__.py46
-rw-r--r--sources/pyside-tools/project/newproject.py165
-rw-r--r--sources/pyside-tools/project/project_data.py244
-rw-r--r--sources/pyside-tools/project/utils.py107
4 files changed, 562 insertions, 0 deletions
diff --git a/sources/pyside-tools/project/__init__.py b/sources/pyside-tools/project/__init__.py
new file mode 100644
index 000000000..e57a9ff88
--- /dev/null
+++ b/sources/pyside-tools/project/__init__.py
@@ -0,0 +1,46 @@
+# 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 dataclasses import dataclass
+
+QTPATHS_CMD = "qtpaths6"
+MOD_CMD = "pyside6-metaobjectdump"
+
+PROJECT_FILE_SUFFIX = ".pyproject"
+QMLDIR_FILE = "qmldir"
+
+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"
+
+METATYPES_JSON_SUFFIX = "metatypes.json"
+TRANSLATION_SUFFIX = ".ts"
+SHADER_SUFFIXES = ".vert", ".frag"
+
+
+class Singleton(type):
+ _instances = {}
+
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls._instances:
+ cls._instances[cls] = super().__call__(*args, **kwargs)
+ return cls._instances[cls]
+
+
+@dataclass(frozen=True)
+class ClOptions(metaclass=Singleton):
+ """
+ Dataclass to store the cl options that needs to be passed as arguments.
+ """
+ dry_run: bool
+ quiet: bool
+ force: bool
+ qml_module: bool
+
+
+from .utils import (run_command, requires_rebuild, remove_path, package_dir, qtpaths,
+ qt_metatype_json_dir, resolve_project_file)
+from .project_data import (is_python_file, ProjectData, QmlProjectData,
+ check_qml_decorators)
+from .newproject import new_project, ProjectType
diff --git a/sources/pyside-tools/project/newproject.py b/sources/pyside-tools/project/newproject.py
new file mode 100644
index 000000000..c363a9fc0
--- /dev/null
+++ b/sources/pyside-tools/project/newproject.py
@@ -0,0 +1,165 @@
+# 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 json
+import os
+import sys
+from enum import Enum
+from pathlib import Path
+from typing import List, Tuple
+
+"""New project generation code."""
+
+
+Project = List[Tuple[str, str]] # tuple of (filename, contents).
+
+
+class ProjectType(Enum):
+ WIDGET_FORM = 1
+ WIDGET = 2
+ QUICK = 3
+
+
+_WIDGET_MAIN = """if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ window = MainWindow()
+ window.show()
+ sys.exit(app.exec())
+"""
+
+
+_WIDGET_IMPORTS = """import sys
+from PySide6.QtWidgets import QApplication, QMainWindow
+"""
+
+
+_WIDGET_CLASS_DEFINITION = """class MainWindow(QMainWindow):
+ def __init__(self):
+ super().__init__()
+"""
+
+
+_WIDGET_SETUP_UI_CODE = """ self._ui = Ui_MainWindow()
+ self._ui.setupUi(self)
+"""
+
+
+_MAINWINDOW_FORM = """<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>600</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>MainWindow</string>
+ </property>
+ <widget class="QWidget" name="centralwidget"/>
+ <widget class="QMenuBar" name="menubar">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>800</width>
+ <height>22</height>
+ </rect>
+ </property>
+ </widget>
+ <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+</ui>
+"""
+
+
+_QUICK_FORM = """import QtQuick
+import QtQuick.Controls
+
+ApplicationWindow {
+ id: window
+ width: 1024
+ height: 600
+ visible: true
+}
+"""
+
+_QUICK_MAIN = """import sys
+from pathlib import Path
+
+from PySide6.QtGui import QGuiApplication
+from PySide6.QtCore import QUrl
+from PySide6.QtQml import QQmlApplicationEngine
+
+
+if __name__ == "__main__":
+ app = QGuiApplication()
+ engine = QQmlApplicationEngine()
+ qml_file = Path(__file__).parent / 'main.qml'
+ engine.load(QUrl.fromLocalFile(qml_file))
+ if not engine.rootObjects():
+ sys.exit(-1)
+ exit_code = app.exec()
+ del engine
+ sys.exit(exit_code)
+"""
+
+
+def _write_project(directory: Path, files: Project):
+ """Write out the project."""
+ file_list = []
+ for file, contents in files:
+ (directory / file).write_text(contents)
+ print(f"Wrote {directory.name}{os.sep}{file}.")
+ file_list.append(file)
+ pyproject = {"files": file_list}
+ pyproject_file = f"{directory}.pyproject"
+ (directory / pyproject_file).write_text(json.dumps(pyproject))
+ print(f"Wrote {directory.name}{os.sep}{pyproject_file}.")
+
+
+def _widget_project() -> Project:
+ """Create a (form-less) widgets project."""
+ main_py = (_WIDGET_IMPORTS + "\n\n" + _WIDGET_CLASS_DEFINITION + "\n\n"
+ + _WIDGET_MAIN)
+ return [("main.py", main_py)]
+
+
+def _ui_form_project() -> Project:
+ """Create a Qt Designer .ui form based widgets project."""
+ main_py = (_WIDGET_IMPORTS
+ + "\nfrom ui_mainwindow import Ui_MainWindow\n\n\n"
+ + _WIDGET_CLASS_DEFINITION + _WIDGET_SETUP_UI_CODE
+ + "\n\n" + _WIDGET_MAIN)
+ return [("main.py", main_py),
+ ("mainwindow.ui", _MAINWINDOW_FORM)]
+
+
+def _qml_project() -> Project:
+ """Create a QML project."""
+ return [("main.py", _QUICK_MAIN),
+ ("main.qml", _QUICK_FORM)]
+
+
+def new_project(directory_s: str,
+ project_type: ProjectType = ProjectType.WIDGET_FORM) -> int:
+ directory = Path(directory_s)
+ if directory.exists():
+ print(f"{directory_s} already exists.", file=sys.stderr)
+ return -1
+ directory.mkdir(parents=True)
+
+ if project_type == ProjectType.WIDGET_FORM:
+ project = _ui_form_project()
+ elif project_type == ProjectType.QUICK:
+ project = _qml_project()
+ else:
+ project = _widget_project()
+ _write_project(directory, project)
+ if project_type == ProjectType.WIDGET_FORM:
+ print(f'Run "pyside6-project build {directory_s}" to build the project')
+ print(f'Run "python {directory.name}{os.sep}main.py" to run the project')
+ return 0
diff --git a/sources/pyside-tools/project/project_data.py b/sources/pyside-tools/project/project_data.py
new file mode 100644
index 000000000..52e20be3f
--- /dev/null
+++ b/sources/pyside-tools/project/project_data.py
@@ -0,0 +1,244 @@
+# 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 json
+import os
+import subprocess
+import sys
+from typing import List, Tuple
+from pathlib import Path
+from . import (METATYPES_JSON_SUFFIX, PROJECT_FILE_SUFFIX, TRANSLATION_SUFFIX,
+ qt_metatype_json_dir, MOD_CMD, QML_IMPORT_MAJOR_VERSION,
+ QML_IMPORT_MINOR_VERSION, QML_IMPORT_NAME, QT_MODULES)
+
+
+def is_python_file(file: Path) -> bool:
+ return (file.suffix == ".py"
+ or sys.platform == "win32" and file.suffix == ".pyw")
+
+
+class ProjectData:
+ def __init__(self, project_file: Path) -> None:
+ """Parse the project."""
+ self._project_file = project_file
+ self._sub_projects_files: List[Path] = []
+
+ # All sources except subprojects
+ self._files: List[Path] = []
+ # QML files
+ self._qml_files: List[Path] = []
+ # Python files
+ self.main_file: Path = None
+ self._python_files: List[Path] = []
+ # ui files
+ self._ui_files: List[Path] = []
+ # qrc files
+ self._qrc_files: List[Path] = []
+ # ts files
+ self._ts_files: List[Path] = []
+
+ with project_file.open("r") as pyf:
+ pyproject = json.load(pyf)
+ for f in pyproject["files"]:
+ file = Path(project_file.parent / f)
+ if file.suffix == PROJECT_FILE_SUFFIX:
+ self._sub_projects_files.append(file)
+ else:
+ self._files.append(file)
+ if file.suffix == ".qml":
+ self._qml_files.append(file)
+ elif is_python_file(file):
+ if file.stem == "main":
+ self.main_file = file
+ self._python_files.append(file)
+ elif file.suffix == ".ui":
+ self._ui_files.append(file)
+ elif file.suffix == ".qrc":
+ self._qrc_files.append(file)
+ elif file.suffix == TRANSLATION_SUFFIX:
+ self._ts_files.append(file)
+
+ if not self.main_file:
+ self._find_main_file()
+
+ @property
+ def project_file(self):
+ return self._project_file
+
+ @property
+ def files(self):
+ return self._files
+
+ @property
+ def main_file(self):
+ return self._main_file
+
+ @main_file.setter
+ def main_file(self, main_file):
+ self._main_file = main_file
+
+ @property
+ def python_files(self):
+ return self._python_files
+
+ @property
+ def ui_files(self):
+ return self._ui_files
+
+ @property
+ def qrc_files(self):
+ return self._qrc_files
+
+ @property
+ def qml_files(self):
+ return self._qml_files
+
+ @property
+ def ts_files(self):
+ return self._ts_files
+
+ @property
+ def sub_projects_files(self):
+ return self._sub_projects_files
+
+ def _find_main_file(self) -> str:
+ """Find the entry point file containing the main function"""
+
+ def is_main(file):
+ return "__main__" in file.read_text(encoding="utf-8")
+
+ if not self.main_file:
+ for python_file in self.python_files:
+ if is_main(python_file):
+ self.main_file = python_file
+ return str(python_file)
+
+ # __main__ not found
+ print(
+ "Python file with main function not found. Add the file to" f" {self.project_file}",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+
+class QmlProjectData:
+ """QML relevant project data."""
+
+ def __init__(self):
+ self._import_name: str = ""
+ self._import_major_version: int = 0
+ self._import_minor_version: int = 0
+ self._qt_modules: List[str] = []
+
+ def registrar_options(self):
+ result = [
+ "--import-name",
+ self._import_name,
+ "--major-version",
+ str(self._import_major_version),
+ "--minor-version",
+ str(self._import_minor_version),
+ ]
+ if self._qt_modules:
+ # Add Qt modules as foreign types
+ foreign_files: List[str] = []
+ meta_dir = qt_metatype_json_dir()
+ for mod in self._qt_modules:
+ mod_id = mod[2:].lower()
+ pattern = f"qt6{mod_id}_*"
+ if sys.platform != "win32":
+ pattern += "_" # qt6core_debug_metatypes.json (Linux)
+ pattern += METATYPES_JSON_SUFFIX
+ for f in meta_dir.glob(pattern):
+ foreign_files.append(os.fspath(f))
+ break
+ if foreign_files:
+ foreign_files_str = ",".join(foreign_files)
+ result.append(f"--foreign-types={foreign_files_str}")
+ return result
+
+ @property
+ def import_name(self):
+ return self._import_name
+
+ @import_name.setter
+ def import_name(self, n):
+ self._import_name = n
+
+ @property
+ def import_major_version(self):
+ return self._import_major_version
+
+ @import_major_version.setter
+ def import_major_version(self, v):
+ self._import_major_version = v
+
+ @property
+ def import_minor_version(self):
+ return self._import_minor_version
+
+ @import_minor_version.setter
+ def import_minor_version(self, v):
+ self._import_minor_version = v
+
+ @property
+ def qt_modules(self):
+ return self._qt_modules
+
+ @qt_modules.setter
+ def qt_modules(self, v):
+ self._qt_modules = v
+
+ def __str__(self) -> str:
+ vmaj = self._import_major_version
+ vmin = self._import_minor_version
+ return f'"{self._import_name}" v{vmaj}.{vmin}'
+
+ def __bool__(self) -> bool:
+ return len(self._import_name) > 0 and self._import_major_version > 0
+
+
+def _has_qml_decorated_class(class_list: List) -> bool:
+ """Check for QML-decorated classes in the moc json output."""
+ for d in class_list:
+ class_infos = d.get("classInfos")
+ if class_infos:
+ for e in class_infos:
+ if "QML" in e["name"]:
+ return True
+ return False
+
+
+def check_qml_decorators(py_file: Path) -> Tuple[bool, QmlProjectData]:
+ """Check if a Python file has QML-decorated classes by running a moc check
+ and return whether a class was found and the QML data."""
+ data = None
+ try:
+ cmd = [MOD_CMD, "--quiet", os.fspath(py_file)]
+ with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
+ data = json.load(proc.stdout)
+ proc.wait()
+ except Exception as e:
+ t = type(e).__name__
+ print(f"{t}: running {MOD_CMD} on {py_file}: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ qml_project_data = QmlProjectData()
+ if not data:
+ return (False, qml_project_data) # No classes in file
+
+ first = data[0]
+ class_list = first["classes"]
+ has_class = _has_qml_decorated_class(class_list)
+ if has_class:
+ v = first.get(QML_IMPORT_NAME)
+ if v:
+ qml_project_data.import_name = v
+ v = first.get(QML_IMPORT_MAJOR_VERSION)
+ if v:
+ qml_project_data.import_major_version = v
+ qml_project_data.import_minor_version = first.get(QML_IMPORT_MINOR_VERSION)
+ v = first.get(QT_MODULES)
+ if v:
+ qml_project_data.qt_modules = v
+ return (has_class, qml_project_data)
diff --git a/sources/pyside-tools/project/utils.py b/sources/pyside-tools/project/utils.py
new file mode 100644
index 000000000..d2bff65af
--- /dev/null
+++ b/sources/pyside-tools/project/utils.py
@@ -0,0 +1,107 @@
+# 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 subprocess
+from pathlib import Path
+from typing import List, Dict, Optional
+
+from . import QTPATHS_CMD, PROJECT_FILE_SUFFIX, ClOptions
+
+
+def run_command(command: List[str], cwd: str = None, ignore_fail: bool = False):
+ """Run a command observing quiet/dry run"""
+ cloptions = ClOptions()
+ if not cloptions.quiet or cloptions.dry_run:
+ print(" ".join(command))
+ if not cloptions.dry_run:
+ ex = subprocess.call(command, cwd=cwd)
+ if ex != 0 and not ignore_fail:
+ sys.exit(ex)
+
+
+def requires_rebuild(sources: List[Path], artifact: Path) -> bool:
+ """Returns whether artifact needs to be rebuilt depending on sources"""
+ if not artifact.is_file():
+ return True
+ artifact_mod_time = artifact.stat().st_mtime
+ for source in sources:
+ if source.stat().st_mtime > artifact_mod_time:
+ return True
+ return False
+
+
+def _remove_path_recursion(path: Path):
+ """Recursion to remove a file or directory."""
+ if path.is_file():
+ path.unlink()
+ elif path.is_dir():
+ for item in path.iterdir():
+ _remove_path_recursion(item)
+ path.rmdir()
+
+
+def remove_path(path: Path):
+ """Remove path (file or directory) observing opt_dry_run."""
+ cloptions = ClOptions()
+ if not path.exists():
+ return
+ if not cloptions.quiet:
+ print(f"Removing {path.name}...")
+ if cloptions.dry_run:
+ return
+ _remove_path_recursion(path)
+
+
+def package_dir() -> Path:
+ """Return the PySide6 root."""
+ return Path(__file__).resolve().parents[2]
+
+
+_qtpaths_info: Dict[str, str] = {}
+
+
+def qtpaths() -> Dict[str, str]:
+ """Run qtpaths and return a dict of values."""
+ global _qtpaths_info
+ if not _qtpaths_info:
+ output = subprocess.check_output([QTPATHS_CMD, "--query"])
+ for line in output.decode("utf-8").split("\n"):
+ tokens = line.strip().split(":", maxsplit=1) # "Path=C:\..."
+ if len(tokens) == 2:
+ _qtpaths_info[tokens[0]] = tokens[1]
+ return _qtpaths_info
+
+
+_qt_metatype_json_dir: Optional[Path] = None
+
+
+def qt_metatype_json_dir() -> Path:
+ """Return the location of the Qt QML metatype files."""
+ global _qt_metatype_json_dir
+ if not _qt_metatype_json_dir:
+ qt_dir = package_dir()
+ if sys.platform != "win32":
+ qt_dir /= "Qt"
+ metatypes_dir = qt_dir / "metatypes"
+ if metatypes_dir.is_dir(): # Fully installed case
+ _qt_metatype_json_dir = metatypes_dir
+ else:
+ # Fallback for distro builds/development.
+ print(
+ f"Falling back to {QTPATHS_CMD} to determine metatypes directory.", file=sys.stderr
+ )
+ _qt_metatype_json_dir = Path(qtpaths()["QT_INSTALL_ARCHDATA"]) / "metatypes"
+ return _qt_metatype_json_dir
+
+
+def resolve_project_file(cmdline: str) -> Optional[Path]:
+ """Return the project file from the command line value, either
+ from the file argument or directory"""
+ project_file = Path(cmdline).resolve() if cmdline else Path.cwd()
+ if project_file.is_file():
+ return project_file
+ if project_file.is_dir():
+ for m in project_file.glob(f"*{PROJECT_FILE_SUFFIX}"):
+ return m
+ return None