diff options
| author | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2022-10-14 16:27:11 +0200 |
|---|---|---|
| committer | Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> | 2022-10-20 13:14:37 +0200 |
| commit | 10715102f01bfee9c0122f21680f05414a947357 (patch) | |
| tree | 47ec6c1eefc99d178e7cfba05ea224e4d59ea288 /sources/pyside-tools/project.py | |
| parent | 55993006f96e5d9d668b33eb4befa31f50e931a4 (diff) | |
Project Tool: Split
- Split classes into separate Python files - utils and project_data
- Project operation still inside project.py
- Created class ProjectData out of class Project to store the
data of the project
Pick-to: 6.4.0
Change-Id: I542b74b90b7a4a01cf415d6d2080cbd6ea914e1d
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'sources/pyside-tools/project.py')
| -rw-r--r-- | sources/pyside-tools/project.py | 377 |
1 files changed, 41 insertions, 336 deletions
diff --git a/sources/pyside-tools/project.py b/sources/pyside-tools/project.py index 5e157945e..0daedbbb4 100644 --- a/sources/pyside-tools/project.py +++ b/sources/pyside-tools/project.py @@ -5,7 +5,7 @@ """ Builds a '.pyproject' file -Builds Qt Designer forms, resource files and QML type files. +Builds Qt Designer forms, resource files and QML type files Deploys the application by creating an executable for the corresponding platform @@ -19,17 +19,16 @@ created and populated with .qmltypes and qmldir files for use by code analysis tools. Currently, only one QML module consisting of several classes can be handled per project file. """ - -import json -import os -import subprocess import sys - -from argparse import ArgumentParser, RawTextHelpFormatter +import os +from typing import List, Tuple, Optional from pathlib import Path -from typing import Dict, List, Optional, Tuple +from argparse import ArgumentParser, RawTextHelpFormatter -from project_lib.newproject import new_project, ProjectType +from project import (QmlProjectData, check_qml_decorators, QMLDIR_FILE, + MOD_CMD, METATYPES_JSON_SUFFIX, requires_rebuild, run_command, + remove_path, ProjectData, resolve_project_file, new_project, + ProjectType) MODE_HELP = """build Builds the project run Builds the project and runs the first file") @@ -41,321 +40,39 @@ new-widget Creates a new QtWidgets project with a main window new-quick Creates a new QtQuick project """ - -opt_quiet = False -opt_dry_run = False -opt_force = False -opt_qml_module = False - - UIC_CMD = "pyside6-uic" RCC_CMD = "pyside6-rcc" -MOD_CMD = "pyside6-metaobjectdump" QMLTYPEREGISTRAR_CMD = "pyside6-qmltyperegistrar" QMLLINT_CMD = "pyside6-qmllint" DEPLOY_CMD = "pyside6-deploy" -QTPATHS_CMD = "qtpaths6" - - -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" - NEW_PROJECT_TYPES = {"new-quick": ProjectType.QUICK, "new-ui": ProjectType.WIDGET_FORM, "new-widget": ProjectType.WIDGET} - -def run_command(command: List[str], cwd: str = None, ignore_fail: bool = False): - """Run a command observing quiet/dry run""" - if not opt_quiet or opt_dry_run: - print(" ".join(command)) - if not opt_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.""" - if not path.exists(): - return - if not opt_quiet: - print(f"Removing {path.name}...") - if opt_dry_run: - return - _remove_path_recursion(path) - - -def package_dir() -> Path: - """Return the PySide6 root.""" - return Path(__file__).resolve().parents[1] - - -_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(":") - 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 / "lib" / "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_LIBS"]) / "metatypes" - return _qt_metatype_json_dir - - -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}_*{METATYPES_JSON_SUFFIX}" - for f in meta_dir.glob(pattern): - foreign_files.append(os.fspath(f)) - break - list = ",".join(foreign_files) - result.append(f"--foreign-types={list}") - 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) - - class Project: + """ + Class to wrap the various operations on Project + """ def __init__(self, project_file: Path): - """Parse the project.""" - self._project_file = project_file - - # All sources except subprojects - self._files: List[Path] = [] - # QML files - self._qml_files: List[Path] = [] - self._sub_projects: List[Project] = [] - # Python files - self._main_file: Path = None - self._python_files: List[Path] = [] + self.project = ProjectData(project_file=project_file) # Files for QML modules using the QmlElement decorators self._qml_module_sources: List[Path] = [] self._qml_module_dir: Optional[Path] = None self._qml_dir_file: Optional[Path] = None self._qml_project_data = QmlProjectData() - - 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.append(Project(file)) - else: - self._files.append(file) - if file.suffix == ".qml": - self._qml_files.append(file) - elif file.suffix == ".py": - if file.name == "main.py": - self.main_file = file - self._python_files.append(file) - if not self.main_file: - self._find_main_file() self._qml_module_check() - @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 - - 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" {project_file}", file=sys.stderr) - sys.exit(1) - def _qml_module_check(self): """Run a pre-check on Python source files and find the ones with QML - decorators (representing a QML module).""" + decorators (representing a QML module).""" # Quick check for any QML files (to avoid running moc for no reason). - if not opt_qml_module and not self._qml_files: + if not opt_qml_module and not self.project.qml_files: return - for file in self.files: + for file in self.project.files: if file.suffix == ".py": - has_class, data = _check_qml_decorators(file) + has_class, data = check_qml_decorators(file) if has_class: self._qml_module_sources.append(file) if data: @@ -364,11 +81,10 @@ class Project: if not self._qml_module_sources: return if not self._qml_project_data: - print("Detected QML-decorated files, " - "but was unable to detect QML_IMPORT_NAME") + print("Detected QML-decorated files, " "but was unable to detect QML_IMPORT_NAME") sys.exit(1) - self._qml_module_dir = self._project_file.parent + self._qml_module_dir = self.project.project_file.parent for uri_dir in self._qml_project_data.import_name.split("."): self._qml_module_dir /= uri_dir print(self._qml_module_dir) @@ -376,7 +92,8 @@ class Project: if not opt_quiet: count = len(self._qml_module_sources) - print(f"{self._project_file.name}, {count} QML file(s), {self._qml_project_data}") + print(f"{self.project.project_file.name}, {count} QML file(s)," + f" {self._qml_project_data}") def _get_artifact(self, file: Path) -> Tuple[Optional[Path], Optional[List[str]]]: """Return path and command for a file's artifact""" @@ -398,7 +115,7 @@ class Project: stem = file.name[: len(file.name) - len(METATYPES_JSON_SUFFIX)] qmltypes_file = self._qml_module_dir / f"{stem}.qmltypes" cmd = [QMLTYPEREGISTRAR_CMD, "--generate-qmltypes", - os.fspath(qmltypes_file),"-o", os.devnull, os.fspath(file)] + os.fspath(qmltypes_file), "-o", os.devnull, os.fspath(file)] cmd.extend(self._qml_project_data.registrar_options()) return (qmltypes_file, cmd) @@ -420,24 +137,24 @@ class Project: if not artifact: return if opt_force or requires_rebuild([source], artifact): - run_command(command, cwd=self._project_file.parent) + run_command(command, cwd=self.project.project_file.parent) self._build_file(artifact) # Recurse for QML (json->qmltypes) def build(self): """Build.""" - for sub_project in self._sub_projects: - sub_project.build() + for sub_project_file in self.project.sub_projects_files: + Project(project_file=sub_project_file).build() if self._qml_module_dir: self._qml_module_dir.mkdir(exist_ok=True, parents=True) - for file in self._files: + for file in self.project.files: self._build_file(file) self._regenerate_qmldir() def run(self): """Runs the project""" self.build() - cmd = [sys.executable, str(self.main_file)] - run_command(cmd, cwd=self._project_file.parent) + cmd = [sys.executable, str(self.project.main_file)] + run_command(cmd, cwd=self.project.project_file.parent) def _clean_file(self, source: Path): """Clean an artifact.""" @@ -448,56 +165,44 @@ class Project: def clean(self): """Clean build artifacts.""" - for sub_project in self._sub_projects: - sub_project.clean() - for file in self._files: + for sub_project_file in self.project.sub_projects_files: + Project(project_file=sub_project_file).clean() + for file in self.project.files: self._clean_file(file) if self._qml_module_dir and self._qml_module_dir.is_dir(): remove_path(self._qml_module_dir) # In case of a dir hierarchy ("a.b" -> a/b), determine and delete # the root directory - if self._qml_module_dir.parent != self._project_file.parent: - project_dir_parts = len(self._project_file.parent.parts) + if self._qml_module_dir.parent != self.project.project_file.parent: + project_dir_parts = len(self.project.project_file.parent.parts) first_module_dir = self._qml_module_dir.parts[project_dir_parts] - remove_path(self._project_file.parent / first_module_dir) + remove_path(self.project.project_file.parent / first_module_dir) def _qmllint(self): """Helper for running qmllint on .qml files (non-recursive).""" - if not self._qml_files: - print(f"{self._project_file.name}: No QML files found", file=sys.stderr) + if not self.project.qml_files: + print(f"{self.project.project_file.name}: No QML files found", file=sys.stderr) return cmd = [QMLLINT_CMD] if self._qml_dir_file: cmd.extend(["-i", os.fspath(self._qml_dir_file)]) - for f in self._qml_files: + for f in self.project.qml_files: cmd.append(os.fspath(f)) - run_command(cmd, cwd=self._project_file.parent, ignore_fail=True) + run_command(cmd, cwd=self.project.project_file.parent, ignore_fail=True) def qmllint(self): """Run qmllint on .qml files.""" self.build() - for sub_project in self._sub_projects: - sub_project._qmllint() + for sub_project_file in self.project.sub_projects_files: + Project(project_file=sub_project_file)._qmllint() self._qmllint() def deploy(self): """Deploys the application""" cmd = [DEPLOY_CMD] - cmd.extend([str(self.main_file), "-f"]) - run_command(cmd, cwd=self._project_file.parent) - - -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 + cmd.extend([str(self.project.main_file), "-f"]) + run_command(cmd, cwd=self.project.project_file.parent) if __name__ == "__main__": |
