project_data.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. # Copyright (C) 2022 The Qt Company Ltd.
  2. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
  3. from __future__ import annotations
  4. import json
  5. import os
  6. import subprocess
  7. import sys
  8. from pathlib import Path
  9. from . import (METATYPES_JSON_SUFFIX, PYPROJECT_JSON_PATTERN, PYPROJECT_TOML_PATTERN,
  10. PYPROJECT_FILE_PATTERNS, TRANSLATION_SUFFIX, qt_metatype_json_dir, MOD_CMD,
  11. QML_IMPORT_MAJOR_VERSION, QML_IMPORT_MINOR_VERSION, QML_IMPORT_NAME, QT_MODULES)
  12. from .pyproject_toml import parse_pyproject_toml
  13. from .pyproject_json import parse_pyproject_json
  14. def is_python_file(file: Path) -> bool:
  15. return (file.suffix == ".py"
  16. or sys.platform == "win32" and file.suffix == ".pyw")
  17. class ProjectData:
  18. def __init__(self, project_file: Path) -> None:
  19. """Parse the project file."""
  20. self._project_file = project_file.resolve()
  21. self._sub_projects_files: list[Path] = []
  22. # All sources except subprojects
  23. self._files: list[Path] = []
  24. # QML files
  25. self._qml_files: list[Path] = []
  26. # Python files
  27. self.main_file: Path = None
  28. self._python_files: list[Path] = []
  29. # ui files
  30. self._ui_files: list[Path] = []
  31. # qrc files
  32. self._qrc_files: list[Path] = []
  33. # ts files
  34. self._ts_files: list[Path] = []
  35. if project_file.match(PYPROJECT_JSON_PATTERN):
  36. project_file_data = parse_pyproject_json(project_file)
  37. elif project_file.match(PYPROJECT_TOML_PATTERN):
  38. project_file_data = parse_pyproject_toml(project_file)
  39. else:
  40. print(f"Unknown project file format: {project_file}", file=sys.stderr)
  41. sys.exit(1)
  42. if project_file_data.errors:
  43. print(f"Invalid project file: {project_file}. Errors found:", file=sys.stderr)
  44. for error in project_file_data.errors:
  45. print(f"{error}", file=sys.stderr)
  46. sys.exit(1)
  47. for f in project_file_data.files:
  48. file = Path(project_file.parent / f)
  49. if any(file.match(pattern) for pattern in PYPROJECT_FILE_PATTERNS):
  50. self._sub_projects_files.append(file)
  51. continue
  52. self._files.append(file)
  53. if file.suffix == ".qml":
  54. self._qml_files.append(file)
  55. elif is_python_file(file):
  56. if file.stem == "main":
  57. self.main_file = file
  58. self._python_files.append(file)
  59. elif file.suffix == ".ui":
  60. self._ui_files.append(file)
  61. elif file.suffix == ".qrc":
  62. self._qrc_files.append(file)
  63. elif file.suffix == TRANSLATION_SUFFIX:
  64. self._ts_files.append(file)
  65. if not self.main_file:
  66. self._find_main_file()
  67. @property
  68. def project_file(self):
  69. return self._project_file
  70. @property
  71. def files(self):
  72. return self._files
  73. @property
  74. def main_file(self):
  75. return self._main_file
  76. @main_file.setter
  77. def main_file(self, main_file):
  78. self._main_file = main_file
  79. @property
  80. def python_files(self):
  81. return self._python_files
  82. @property
  83. def ui_files(self):
  84. return self._ui_files
  85. @property
  86. def qrc_files(self):
  87. return self._qrc_files
  88. @property
  89. def qml_files(self):
  90. return self._qml_files
  91. @property
  92. def ts_files(self):
  93. return self._ts_files
  94. @property
  95. def sub_projects_files(self):
  96. return self._sub_projects_files
  97. def _find_main_file(self) -> str:
  98. """Find the entry point file containing the main function"""
  99. def is_main(file):
  100. return "__main__" in file.read_text(encoding="utf-8")
  101. if not self.main_file:
  102. for python_file in self.python_files:
  103. if is_main(python_file):
  104. self.main_file = python_file
  105. return str(python_file)
  106. # __main__ not found
  107. print(
  108. f"Python file with main function not found. Add the file to {self.project_file}",
  109. file=sys.stderr,
  110. )
  111. sys.exit(1)
  112. class QmlProjectData:
  113. """QML relevant project data."""
  114. def __init__(self):
  115. self._import_name: str = ""
  116. self._import_major_version: int = 0
  117. self._import_minor_version: int = 0
  118. self._qt_modules: list[str] = []
  119. def registrar_options(self):
  120. result = [
  121. "--import-name",
  122. self._import_name,
  123. "--major-version",
  124. str(self._import_major_version),
  125. "--minor-version",
  126. str(self._import_minor_version),
  127. ]
  128. if self._qt_modules:
  129. # Add Qt modules as foreign types
  130. foreign_files: list[str] = []
  131. meta_dir = qt_metatype_json_dir()
  132. for mod in self._qt_modules:
  133. mod_id = mod[2:].lower()
  134. pattern = f"qt6{mod_id}_*"
  135. if sys.platform != "win32":
  136. pattern += "_" # qt6core_debug_metatypes.json (Linux)
  137. pattern += METATYPES_JSON_SUFFIX
  138. for f in meta_dir.glob(pattern):
  139. foreign_files.append(os.fspath(f))
  140. break
  141. if foreign_files:
  142. foreign_files_str = ",".join(foreign_files)
  143. result.append(f"--foreign-types={foreign_files_str}")
  144. return result
  145. @property
  146. def import_name(self):
  147. return self._import_name
  148. @import_name.setter
  149. def import_name(self, n):
  150. self._import_name = n
  151. @property
  152. def import_major_version(self):
  153. return self._import_major_version
  154. @import_major_version.setter
  155. def import_major_version(self, v):
  156. self._import_major_version = v
  157. @property
  158. def import_minor_version(self):
  159. return self._import_minor_version
  160. @import_minor_version.setter
  161. def import_minor_version(self, v):
  162. self._import_minor_version = v
  163. @property
  164. def qt_modules(self):
  165. return self._qt_modules
  166. @qt_modules.setter
  167. def qt_modules(self, v):
  168. self._qt_modules = v
  169. def __str__(self) -> str:
  170. vmaj = self._import_major_version
  171. vmin = self._import_minor_version
  172. return f'"{self._import_name}" v{vmaj}.{vmin}'
  173. def __bool__(self) -> bool:
  174. return len(self._import_name) > 0 and self._import_major_version > 0
  175. def _has_qml_decorated_class(class_list: list) -> bool:
  176. """Check for QML-decorated classes in the moc json output."""
  177. for d in class_list:
  178. class_infos = d.get("classInfos")
  179. if class_infos:
  180. for e in class_infos:
  181. if "QML" in e["name"]:
  182. return True
  183. return False
  184. def check_qml_decorators(py_file: Path) -> tuple[bool, QmlProjectData]:
  185. """Check if a Python file has QML-decorated classes by running a moc check
  186. and return whether a class was found and the QML data."""
  187. data = None
  188. try:
  189. cmd = [MOD_CMD, "--quiet", os.fspath(py_file)]
  190. with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
  191. data = json.load(proc.stdout)
  192. proc.wait()
  193. except Exception as e:
  194. t = type(e).__name__
  195. print(f"{t}: running {MOD_CMD} on {py_file}: {e}", file=sys.stderr)
  196. sys.exit(1)
  197. qml_project_data = QmlProjectData()
  198. if not data:
  199. return (False, qml_project_data) # No classes in file
  200. first = data[0]
  201. class_list = first["classes"]
  202. has_class = _has_qml_decorated_class(class_list)
  203. if has_class:
  204. v = first.get(QML_IMPORT_NAME)
  205. if v:
  206. qml_project_data.import_name = v
  207. v = first.get(QML_IMPORT_MAJOR_VERSION)
  208. if v:
  209. qml_project_data.import_major_version = v
  210. qml_project_data.import_minor_version = first.get(QML_IMPORT_MINOR_VERSION)
  211. v = first.get(QT_MODULES)
  212. if v:
  213. qml_project_data.qt_modules = v
  214. return (has_class, qml_project_data)