aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside-tools/project.py
diff options
context:
space:
mode:
authorShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2022-10-14 16:27:11 +0200
committerShyamnath Premnadh <Shyamnath.Premnadh@qt.io>2022-10-20 13:14:37 +0200
commit10715102f01bfee9c0122f21680f05414a947357 (patch)
tree47ec6c1eefc99d178e7cfba05ea224e4d59ea288 /sources/pyside-tools/project.py
parent55993006f96e5d9d668b33eb4befa31f50e931a4 (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.py377
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__":