aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/create_changelog.py70
-rw-r--r--tools/cross_compile_android/android_utilities.py297
-rw-r--r--tools/cross_compile_android/main.py309
-rw-r--r--tools/cross_compile_android/requirements.txt3
-rw-r--r--tools/cross_compile_android/templates/cross_compile.tmpl.sh29
-rw-r--r--tools/cross_compile_android/templates/toolchain_default.tmpl.cmake73
-rw-r--r--tools/doc_modules.py5
-rw-r--r--tools/example_gallery/main.py670
-rw-r--r--tools/missing_bindings-requirements.txt7
-rw-r--r--tools/missing_bindings/config.py28
-rw-r--r--tools/missing_bindings/main.py121
-rw-r--r--tools/missing_bindings/requirements.txt2
-rw-r--r--tools/scanqtclasses.py122
-rw-r--r--tools/snippets_translate/converter.py41
-rw-r--r--tools/snippets_translate/handlers.py35
-rw-r--r--tools/snippets_translate/main.py16
-rw-r--r--tools/snippets_translate/module_classes.py1
-rw-r--r--tools/snippets_translate/tests/test_converter.py64
-rw-r--r--tools/snippets_translate/tests/test_snippets.py34
19 files changed, 1684 insertions, 243 deletions
diff --git a/tools/create_changelog.py b/tools/create_changelog.py
index 2fdc2743f..6c24f417f 100644
--- a/tools/create_changelog.py
+++ b/tools/create_changelog.py
@@ -2,9 +2,11 @@
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
import re
+import os
import sys
import textwrap
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
+from pathlib import Path
from subprocess import PIPE, Popen, check_output
from typing import Dict, List, Tuple
@@ -37,10 +39,45 @@ description = """
PySide6 changelog tool
Example usage:
-tools/create_changelog.py -v v6.2.3..HEAD -r 6.2.4
+tools/create_changelog.py -v -r 6.5.3
"""
+def change_log(version: list) -> Path:
+ """Return path of the changelog of the version."""
+ name = f"changes-{version[0]}.{version[1]}.{version[2]}"
+ return Path(__file__).parents[1] / "doc" / "changelogs" / name
+
+
+def is_lts_version(version: list) -> bool:
+ return version[0] == 5 or version[1] in (2, 5)
+
+
+def version_tag(version: list) -> str:
+ """Return the version tag."""
+ tag = f"v{version[0]}.{version[1]}.{version[2]}"
+ return tag + "-lts" if is_lts_version(version) else tag
+
+
+def revision_range(version: list) -> str:
+ """Determine a git revision_range from the version. Either log from
+ the previous version tag or since the last update to the changelog."""
+ changelog = change_log(version)
+ if changelog.is_file():
+ output = check_output(["git", "log", "-n", "1", "--format=%H",
+ os.fspath(changelog)])
+ if output:
+ return output.strip().decode("utf-8") + "..HEAD"
+
+ last_version = version.copy()
+ if version[2] == 0:
+ adjust_idx = 0 if version[1] == 0 else 1
+ else:
+ adjust_idx = 2
+ last_version[adjust_idx] -= 1
+ return version_tag(last_version) + "..HEAD"
+
+
def parse_options() -> Namespace:
tag_msg = ("Tags, branches, or SHA to compare\n"
"e.g.: v5.12.1..5.12\n"
@@ -56,8 +93,7 @@ def parse_options() -> Namespace:
options.add_argument("-v",
"--versions",
type=str,
- help=tag_msg,
- required=True)
+ help=tag_msg)
options.add_argument("-r",
"--release",
type=str,
@@ -66,8 +102,7 @@ def parse_options() -> Namespace:
options.add_argument("-t",
"--type",
type=str,
- help="Release type: bug-fix, minor, or major",
- default="bug-fix")
+ help="Release type: bug-fix, minor, or major")
options.add_argument("-e",
"--exclude",
@@ -76,11 +111,34 @@ def parse_options() -> Namespace:
default=False)
args = options.parse_args()
+
+ release_version = list(int(v) for v in args.release.split("."))
+ if len(release_version) != 3:
+ print("Error: --release must be of form major.minor.patch")
+ sys.exit(-1)
+
+ # Some auto-detection smartness
+ if not args.type:
+ if release_version[2] == 0:
+ args.type = "major" if release_version[1] == 0 else "minor"
+ else:
+ args.type = "bug-fix"
+ # For major/minor releases, skip all fixes with "Pick-to: " since they
+ # appear in bug-fix releases.
+ if args.type != "bug-fix":
+ args.exclude = True
+ print(f'Assuming "{args.type}" version', file=sys.stderr)
+
if args.type not in ("bug-fix", "minor", "major"):
- print("Error:"
+ print("Error: "
"-y/--type needs to be: bug-fix (default), minor, or major")
sys.exit(-1)
+ if not args.versions:
+ args.versions = revision_range(release_version)
+ print(f"Assuming range {args.versions}", file=sys.stderr)
+
+ args.release_version = release_version
return args
diff --git a/tools/cross_compile_android/android_utilities.py b/tools/cross_compile_android/android_utilities.py
new file mode 100644
index 000000000..3d93abec2
--- /dev/null
+++ b/tools/cross_compile_android/android_utilities.py
@@ -0,0 +1,297 @@
+# Copyright (C) 2023 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 logging
+import shutil
+import re
+import os
+import stat
+import sys
+import subprocess
+
+from urllib import request
+from pathlib import Path
+from typing import List
+from packaging import version
+from tqdm import tqdm
+
+# the tag number does not matter much since we update the sdk later
+DEFAULT_SDK_TAG = 6514223
+ANDROID_NDK_VERSION = "26b"
+ANDROID_NDK_VERSION_NUMBER_SUFFIX = "10909125"
+
+
+def run_command(command: List[str], cwd: str = None, ignore_fail: bool = False,
+ dry_run: bool = False, accept_prompts: bool = False, show_stdout: bool = False,
+ capture_stdout: bool = False):
+
+ if capture_stdout and not show_stdout:
+ raise RuntimeError("capture_stdout should always be used together with show_stdout")
+
+ if dry_run:
+ print(" ".join(command))
+ return
+
+ input = None
+ if accept_prompts:
+ input = str.encode("y")
+
+ if show_stdout:
+ stdout = None
+ else:
+ stdout = subprocess.DEVNULL
+
+ result = subprocess.run(command, cwd=cwd, input=input, stdout=stdout,
+ capture_output=capture_stdout)
+
+ if result.returncode != 0 and not ignore_fail:
+ sys.exit(result.returncode)
+
+ if capture_stdout and not result.returncode:
+ return result.stdout.decode("utf-8")
+
+ return None
+
+
+class DownloadProgressBar(tqdm):
+ def update_to(self, b=1, bsize=1, tsize=None):
+ if tsize is not None:
+ self.total = tsize
+ self.update(b * bsize - self.n)
+
+
+class SdkManager:
+ def __init__(self, android_sdk_dir: Path, dry_run: bool = False):
+ self._sdk_manager = android_sdk_dir / "tools" / "bin" / "sdkmanager"
+
+ if not self._sdk_manager.exists():
+ raise RuntimeError(f"Unable to find SdkManager in {str(self._sdk_manager)}")
+
+ if not os.access(self._sdk_manager, os.X_OK):
+ current_permissions = stat.S_IMODE(os.lstat(self._sdk_manager).st_mode)
+ os.chmod(self._sdk_manager, current_permissions | stat.S_IEXEC)
+
+ self._android_sdk_dir = android_sdk_dir
+ self._dry_run = dry_run
+
+ def list_packages(self):
+ command = [self._sdk_manager, f"--sdk_root={self._android_sdk_dir}", "--list"]
+ return run_command(command=command, dry_run=self._dry_run, show_stdout=True,
+ capture_stdout=True)
+
+ def install(self, *args, accept_license: bool = False, show_stdout=False):
+ command = [str(self._sdk_manager), f"--sdk_root={self._android_sdk_dir}", *args]
+ run_command(command=command, dry_run=self._dry_run,
+ accept_prompts=accept_license, show_stdout=show_stdout)
+
+
+def extract_zip(file: Path, destination: Path):
+ """
+ Unpacks the zip file into destination preserving all permissions
+
+ TODO: Try to use zipfile module. Currently we cannot use zipfile module here because
+ extractAll() does not preserve permissions.
+
+ In case `unzip` is not available, the user is requested to install it manually
+ """
+ unzip = shutil.which("unzip")
+ if not unzip:
+ raise RuntimeError("Unable to find program unzip. Use `sudo apt-get install unzip`"
+ "to install it")
+
+ command = [unzip, file, "-d", destination]
+ run_command(command=command, show_stdout=True)
+
+
+def extract_dmg(file: Path, destination: Path):
+ output = run_command(['hdiutil', 'attach', '-nobrowse', '-readonly', file],
+ show_stdout=True, capture_stdout=True)
+
+ # find the mounted volume
+ mounted_vol_name = re.search(r'/Volumes/(.*)', output).group(1)
+ if not mounted_vol_name:
+ raise RuntimeError(f"Unable to find mounted volume for file {file}")
+
+ # copy files
+ shutil.copytree(f'/Volumes/{mounted_vol_name}/', destination, dirs_exist_ok=True)
+
+ # Detach mounted volume
+ run_command(['hdiutil', 'detach', f'/Volumes/{mounted_vol_name}'])
+
+
+def _download(url: str, destination: Path):
+ """
+ Download url to destination
+ """
+ headers, download_path = None, None
+ # https://github.com/tqdm/tqdm#hooks-and-callbacks
+ with DownloadProgressBar(unit='B', unit_scale=True, miniters=1, desc=url.split('/')[-1]) as t:
+ download_path, headers = request.urlretrieve(url=url, filename=destination,
+ reporthook=t.update_to)
+ assert Path(download_path).resolve() == destination
+
+
+def download_android_ndk(ndk_path: Path):
+ """
+ Downloads the given ndk_version into ndk_path
+ """
+ ndk_path = ndk_path / "android-ndk"
+ ndk_extension = "dmg" if sys.platform == "darwin" else "zip"
+ ndk_zip_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}"
+ ndk_version_path = ""
+ if sys.platform == "linux":
+ ndk_version_path = ndk_path / f"android-ndk-r{ANDROID_NDK_VERSION}"
+ elif sys.platform == "darwin":
+ ndk_version_path = (ndk_path
+ / f"AndroidNDK{ANDROID_NDK_VERSION_NUMBER_SUFFIX}.app/Contents/NDK")
+ else:
+ raise RuntimeError(f"Unsupported platform {sys.platform}")
+
+ if ndk_version_path.exists():
+ print(f"NDK path found in {str(ndk_version_path)}")
+ else:
+ ndk_path.mkdir(parents=True, exist_ok=True)
+ url = (f"https://dl.google.com/android/repository"
+ f"/android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}")
+
+ print(f"Downloading Android Ndk version r{ANDROID_NDK_VERSION}")
+ _download(url=url, destination=ndk_zip_path)
+
+ print("Unpacking Android Ndk")
+ if sys.platform == "darwin":
+ extract_dmg(file=(ndk_path
+ / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}"
+ ),
+ destination=ndk_path)
+ ndk_version_path = (ndk_version_path
+ / f"AndroidNDK{ANDROID_NDK_VERSION_NUMBER_SUFFIX}.app/Contents/NDK")
+ else:
+ extract_zip(file=(ndk_path
+ / f"android-ndk-r{ANDROID_NDK_VERSION}-{sys.platform}.{ndk_extension}"
+ ),
+ destination=ndk_path)
+
+ return ndk_version_path
+
+
+def download_android_commandlinetools(android_sdk_dir: Path):
+ """
+ Downloads Android commandline tools into cltools_path.
+ """
+ sdk_platform = sys.platform if sys.platform != "darwin" else "mac"
+ android_sdk_dir = android_sdk_dir / "android-sdk"
+ url = ("https://dl.google.com/android/repository/"
+ f"commandlinetools-{sdk_platform}-{DEFAULT_SDK_TAG}_latest.zip")
+ cltools_zip_path = (android_sdk_dir
+ / f"commandlinetools-{sdk_platform}-{DEFAULT_SDK_TAG}_latest.zip")
+ cltools_path = android_sdk_dir / "tools"
+
+ if cltools_path.exists():
+ print(f"Command-line tools found in {str(cltools_path)}")
+ else:
+ android_sdk_dir.mkdir(parents=True, exist_ok=True)
+
+ print("Download Android Command Line Tools: "
+ f"commandlinetools-{sys.platform}-{DEFAULT_SDK_TAG}_latest.zip")
+ _download(url=url, destination=cltools_zip_path)
+
+ print("Unpacking Android Command Line Tools")
+ extract_zip(file=cltools_zip_path, destination=android_sdk_dir)
+
+ return android_sdk_dir
+
+
+def android_list_build_tools_versions(sdk_manager: SdkManager):
+ """
+ List all the build-tools versions available for download
+ """
+ available_packages = sdk_manager.list_packages()
+ build_tools_versions = []
+ lines = available_packages.split('\n')
+
+ for line in lines:
+ if not line.strip().startswith('build-tools;'):
+ continue
+ package_name = line.strip().split(' ')[0]
+ if package_name.count(';') != 1:
+ raise RuntimeError(f"Unable to parse build-tools version: {package_name}")
+ ver = package_name.split(';')[1]
+
+ build_tools_versions.append(version.Version(ver))
+
+ return build_tools_versions
+
+
+def find_installed_buildtools_version(build_tools_dir: Path):
+ """
+ It is possible that the user has multiple build-tools installed. The newer version is generally
+ used. This function find the newest among the installed build-tools
+ """
+ versions = [version.Version(bt_dir.name) for bt_dir in build_tools_dir.iterdir()
+ if bt_dir.is_dir()]
+ return max(versions)
+
+
+def find_latest_buildtools_version(sdk_manager: SdkManager):
+ """
+ Uses sdk manager to find the latest build-tools version
+ """
+ available_build_tools_v = android_list_build_tools_versions(sdk_manager=sdk_manager)
+
+ if not available_build_tools_v:
+ raise RuntimeError('Unable to find any build tools available for download')
+
+ # find the latest build tools version that is not a release candidate
+ # release candidates end has rc in the version number
+ available_build_tools_v = [v for v in available_build_tools_v if "rc" not in str(v)]
+
+ return max(available_build_tools_v)
+
+
+def install_android_packages(android_sdk_dir: Path, android_api: str, dry_run: bool = False,
+ accept_license: bool = False, skip_update: bool = False):
+ """
+ Use the sdk manager to install build-tools, platform-tools and platform API
+ """
+ tools_dir = android_sdk_dir / "tools"
+ if not tools_dir.exists():
+ raise RuntimeError("Unable to find Android command-line tools in "
+ f"{str(tools_dir)}")
+
+ # incase of --verbose flag
+ show_output = (logging.getLogger().getEffectiveLevel() == logging.INFO)
+
+ sdk_manager = SdkManager(android_sdk_dir=android_sdk_dir, dry_run=dry_run)
+
+ # install/upgrade platform-tools
+ if not (android_sdk_dir / "platform-tools").exists():
+ print("Installing/Updating Android platform-tools")
+ sdk_manager.install("platform-tools", accept_license=accept_license,
+ show_stdout=show_output)
+ # The --update command is only relevant for platform tools
+ if not skip_update:
+ sdk_manager.install("--update", show_stdout=show_output)
+
+ # install/upgrade build-tools
+ buildtools_dir = android_sdk_dir / "build-tools"
+
+ if not buildtools_dir.exists():
+ latest_build_tools_v = find_latest_buildtools_version(sdk_manager=sdk_manager)
+ print(f"Installing Android build-tools version {latest_build_tools_v}")
+ sdk_manager.install(f"build-tools;{latest_build_tools_v}", show_stdout=show_output)
+ else:
+ if not skip_update:
+ latest_build_tools_v = find_latest_buildtools_version(sdk_manager=sdk_manager)
+ installed_build_tools_v = find_installed_buildtools_version(buildtools_dir)
+ if latest_build_tools_v > installed_build_tools_v:
+ print(f"Updating Android build-tools version to {latest_build_tools_v}")
+ sdk_manager.install(f"build-tools;{latest_build_tools_v}", show_stdout=show_output)
+ installed_build_tools_v = latest_build_tools_v
+
+ # install the platform API
+ platform_api_dir = android_sdk_dir / "platforms" / f"android-{android_api}"
+ if not platform_api_dir.exists():
+ print(f"Installing Android platform API {android_api}")
+ sdk_manager.install(f"platforms;android-{android_api}", show_stdout=show_output)
+
+ print("Android packages installation done")
diff --git a/tools/cross_compile_android/main.py b/tools/cross_compile_android/main.py
new file mode 100644
index 000000000..200f494cf
--- /dev/null
+++ b/tools/cross_compile_android/main.py
@@ -0,0 +1,309 @@
+# Copyright (C) 2023 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 logging
+import argparse
+import stat
+import warnings
+import shutil
+from dataclasses import dataclass
+
+from pathlib import Path
+from git import Repo, RemoteProgress
+from tqdm import tqdm
+from jinja2 import Environment, FileSystemLoader
+
+from android_utilities import (run_command, download_android_commandlinetools,
+ download_android_ndk, install_android_packages)
+
+# Note: Does not work with PyEnv. Your Host Python should contain openssl.
+# also update the version in ShibokenHelpers.cmake if Python version changes.
+PYTHON_VERSION = "3.11"
+
+SKIP_UPDATE_HELP = ("skip the updation of SDK packages build-tools, platform-tools to"
+ " latest version")
+
+ACCEPT_LICENSE_HELP = ('''
+Accepts license automatically for Android SDK installation. Otherwise,
+accept the license manually through command line.
+''')
+
+CLEAN_CACHE_HELP = ('''
+Cleans cache stored in $HOME/.pyside6_deploy_cache.
+Options:
+
+1. all - all the cache including Android Ndk, Android Sdk and Cross-compiled Python are deleted.
+2. ndk - Only the Android Ndk is deleted.
+3. sdk - Only the Android Sdk is deleted.
+4. python - The cross compiled Python for all platforms, the cloned CPython, the cross compilation
+ scripts for all platforms are deleted.
+5. toolchain - The CMake toolchain file required for cross-compiling Qt for Python, for all
+ platforms are deleted.
+
+If --clean-cache is used and no explicit value is suppied, then `all` is used as default.
+''')
+
+
+@dataclass
+class PlatformData:
+ plat_name: str
+ api_level: str
+ android_abi: str
+ qt_plat_name: str
+ gcc_march: str
+ plat_bits: str
+
+
+def occp_exists():
+ '''
+ check if '--only-cross-compile-python' exists in command line arguments
+ '''
+ return "-occp" in sys.argv or "--only-cross-compile-python" in sys.argv
+
+
+class CloneProgress(RemoteProgress):
+ def __init__(self):
+ super().__init__()
+ self.pbar = tqdm()
+
+ def update(self, op_code, cur_count, max_count=None, message=""):
+ self.pbar.total = max_count
+ self.pbar.n = cur_count
+ self.pbar.refresh()
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="This tool cross builds CPython for Android and uses that Python to cross build"
+ "Android Qt for Python wheels",
+ formatter_class=argparse.RawTextHelpFormatter,
+ )
+
+ parser.add_argument("-p", "--plat-name", type=str, nargs="*",
+ choices=["aarch64", "armv7a", "i686", "x86_64"],
+ default=["aarch64", "armv7a", "i686", "x86_64"], dest="plat_names",
+ help="Android target platforms")
+
+ parser.add_argument("-v", "--verbose", help="run in verbose mode", action="store_const",
+ dest="loglevel", const=logging.INFO)
+ parser.add_argument("--api-level", type=str, default="26",
+ help="Minimum Android API level to use")
+ parser.add_argument("--ndk-path", type=str, help="Path to Android NDK (Preferred r25c)")
+ # sdk path is needed to compile all the Qt Java Acitivity files into Qt6AndroidBindings.jar
+ parser.add_argument("--sdk-path", type=str, help="Path to Android SDK")
+ parser.add_argument("--qt-install-path", type=str, required=not occp_exists(),
+ help="Qt installation path eg: /home/Qt/6.5.0")
+
+ parser.add_argument("-occp", "--only-cross-compile-python", action="store_true",
+ help="Only cross compiles Python for the specified Android platform")
+
+ parser.add_argument("--dry-run", action="store_true", help="show the commands to be run")
+
+ parser.add_argument("--skip-update", action="store_true",
+ help=SKIP_UPDATE_HELP)
+
+ parser.add_argument("--auto-accept-license", action="store_true",
+ help=ACCEPT_LICENSE_HELP)
+
+ parser.add_argument("--clean-cache", type=str, nargs="?", const="all",
+ choices=["all", "python", "ndk", "sdk", "toolchain"],
+ help=CLEAN_CACHE_HELP)
+
+ args = parser.parse_args()
+
+ logging.basicConfig(level=args.loglevel)
+ pyside_setup_dir = Path(__file__).parents[2].resolve()
+ qt_install_path = args.qt_install_path
+ ndk_path = args.ndk_path
+ sdk_path = args.sdk_path
+ only_py_cross_compile = args.only_cross_compile_python
+ android_abi = None
+ gcc_march = None
+ plat_bits = None
+ dry_run = args.dry_run
+ plat_names = args.plat_names
+ api_level = args.api_level
+ skip_update = args.skip_update
+ auto_accept_license = args.auto_accept_license
+ clean_cache = args.clean_cache
+
+ # auto download Android NDK and SDK
+ pyside6_deploy_cache = Path.home() / ".pyside6_android_deploy"
+ logging.info(f"Cache created at {str(pyside6_deploy_cache.resolve())}")
+ pyside6_deploy_cache.mkdir(exist_ok=True)
+
+ if pyside6_deploy_cache.exists() and clean_cache:
+ if clean_cache == "all":
+ shutil.rmtree(pyside6_deploy_cache)
+ elif clean_cache == "ndk":
+ cached_ndk_dir = pyside6_deploy_cache / "android-ndk"
+ if cached_ndk_dir.exists():
+ shutil.rmtree(cached_ndk_dir)
+ elif clean_cache == "sdk":
+ cached_sdk_dir = pyside6_deploy_cache / "android-sdk"
+ if cached_sdk_dir.exists():
+ shutil.rmtree(cached_sdk_dir)
+ elif clean_cache == "python":
+ cached_cpython_dir = pyside6_deploy_cache / "cpython"
+ if cached_cpython_dir.exists():
+ shutil.rmtree(pyside6_deploy_cache / "cpython")
+ for cc_python_path in pyside6_deploy_cache.glob("Python-*"):
+ if cc_python_path.is_dir():
+ shutil.rmtree(cc_python_path)
+ elif clean_cache == "toolchain":
+ for toolchain_path in pyside6_deploy_cache.glob("toolchain_*"):
+ if toolchain_path.is_file():
+ toolchain_path.unlink()
+
+ if not ndk_path:
+ # Download android ndk
+ ndk_path = download_android_ndk(pyside6_deploy_cache)
+
+ if not sdk_path:
+ # download and unzip command-line tools
+ sdk_path = download_android_commandlinetools(pyside6_deploy_cache)
+ # install and update required android packages
+ install_android_packages(android_sdk_dir=sdk_path, android_api=api_level, dry_run=dry_run,
+ accept_license=auto_accept_license, skip_update=skip_update)
+
+ templates_path = Path(__file__).parent / "templates"
+
+ for plat_name in plat_names:
+ # for armv7a the API level dependent binaries like clang are named
+ # armv7a-linux-androideabi27-clang, as opposed to other platforms which
+ # are named like x86_64-linux-android27-clang
+ platform_data = None
+ if plat_name == "armv7a":
+ platform_data = PlatformData("armv7a", api_level, "armeabi-v7a", "armv7",
+ "armv7", "32")
+ elif plat_name == "aarch64":
+ platform_data = PlatformData("aarch64", api_level, "arm64-v8a", "arm64_v8a", "armv8-a",
+ "64")
+ elif plat_name == "i686":
+ platform_data = PlatformData("i686", api_level, "x86", "x86", "i686", "32")
+ else: # plat_name is x86_64
+ platform_data = PlatformData("x86_64", api_level, "x86_64", "x86_64", "x86-64", "64")
+
+ # python path is valid, if Python for android installation exists in python_path
+ python_path = (pyside6_deploy_cache
+ / f"Python-{platform_data.plat_name}-linux-android" / "_install")
+ valid_python_path = python_path.exists()
+ if Path(python_path).exists():
+ expected_dirs = ["lib", "include"]
+ for expected_dir in expected_dirs:
+ if not (Path(python_path) / expected_dir).is_dir():
+ valid_python_path = False
+ warnings.warn(
+ f"{str(python_path.resolve())} is corrupted. New Python for {plat_name} "
+ f"android will be cross-compiled into {str(pyside6_deploy_cache.resolve())}"
+ )
+ break
+
+ environment = Environment(loader=FileSystemLoader(templates_path))
+ if not valid_python_path:
+ # clone cpython and checkout 3.10
+ cpython_dir = pyside6_deploy_cache / "cpython"
+ python_ccompile_script = cpython_dir / f"cross_compile_{plat_name}.sh"
+
+ if not cpython_dir.exists():
+ logging.info(f"cloning cpython {PYTHON_VERSION}")
+ Repo.clone_from(
+ "https://github.com/python/cpython.git",
+ cpython_dir,
+ progress=CloneProgress(),
+ branch=PYTHON_VERSION,
+ )
+
+ if not python_ccompile_script.exists():
+ host_system_config_name = run_command("./config.guess", cwd=cpython_dir,
+ dry_run=dry_run, show_stdout=True,
+ capture_stdout=True).strip()
+
+ # use jinja2 to create cross_compile.sh script
+ template = environment.get_template("cross_compile.tmpl.sh")
+ content = template.render(
+ plat_name=platform_data.plat_name,
+ ndk_path=ndk_path,
+ api_level=platform_data.api_level,
+ android_py_install_path_prefix=pyside6_deploy_cache,
+ host_python_path=sys.executable,
+ python_version=PYTHON_VERSION,
+ host_system_name=host_system_config_name,
+ host_platform_name=sys.platform
+ )
+
+ logging.info(f"Writing Python cross compile script into {python_ccompile_script}")
+ with open(python_ccompile_script, mode="w", encoding="utf-8") as ccompile_script:
+ ccompile_script.write(content)
+
+ # give run permission to cross compile script
+ python_ccompile_script.chmod(python_ccompile_script.stat().st_mode | stat.S_IEXEC)
+
+ # clean built files
+ logging.info("Cleaning CPython built files")
+ run_command(["make", "distclean"], cwd=cpython_dir, dry_run=dry_run, ignore_fail=True)
+
+ # run the cross compile script
+ logging.info(f"Running Python cross-compile for platform {platform_data.plat_name}")
+ run_command([f"./{python_ccompile_script.name}"], cwd=cpython_dir, dry_run=dry_run,
+ show_stdout=True)
+
+ logging.info(
+ f"Cross compile Python for Android platform {platform_data.plat_name}. "
+ f"Final installation in {python_path}"
+ )
+
+ if only_py_cross_compile:
+ continue
+
+ if only_py_cross_compile:
+ requested_platforms = ",".join(plat_names)
+ print(f"Python for Android platforms: {requested_platforms} cross compiled "
+ f"to {str(pyside6_deploy_cache)}")
+ sys.exit(0)
+
+ qfp_toolchain = pyside6_deploy_cache / f"toolchain_{platform_data.plat_name}.cmake"
+
+ if not qfp_toolchain.exists():
+ template = environment.get_template("toolchain_default.tmpl.cmake")
+ content = template.render(
+ ndk_path=ndk_path,
+ sdk_path=sdk_path,
+ api_level=platform_data.api_level,
+ qt_install_path=qt_install_path,
+ plat_name=platform_data.plat_name,
+ android_abi=platform_data.android_abi,
+ qt_plat_name=platform_data.qt_plat_name,
+ gcc_march=platform_data.gcc_march,
+ plat_bits=platform_data.plat_bits,
+ python_version=PYTHON_VERSION,
+ target_python_path=python_path
+ )
+
+ logging.info(f"Writing Qt for Python toolchain file into {qfp_toolchain}")
+ with open(qfp_toolchain, mode="w", encoding="utf-8") as ccompile_script:
+ ccompile_script.write(content)
+
+ # give run permission to cross compile script
+ qfp_toolchain.chmod(qfp_toolchain.stat().st_mode | stat.S_IEXEC)
+
+ if sys.platform == "linux":
+ host_qt_install_suffix = "gcc_64"
+ elif sys.platform == "darwin":
+ host_qt_install_suffix = "macos"
+ else:
+ raise RuntimeError("Qt for Python cross compilation not supported on this platform")
+
+ # run the cross compile script
+ logging.info(f"Running Qt for Python cross-compile for platform {platform_data.plat_name}")
+ qfp_ccompile_cmd = [sys.executable, "setup.py", "bdist_wheel", "--parallel=9",
+ "--standalone",
+ f"--cmake-toolchain-file={str(qfp_toolchain.resolve())}",
+ f"--qt-host-path={qt_install_path}/{host_qt_install_suffix}",
+ f"--plat-name=android_{platform_data.plat_name}",
+ f"--python-target-path={python_path}",
+ (f"--qt-target-path={qt_install_path}/"
+ f"android_{platform_data.qt_plat_name}"),
+ "--no-qt-tools"]
+ run_command(qfp_ccompile_cmd, cwd=pyside_setup_dir, dry_run=dry_run, show_stdout=True)
diff --git a/tools/cross_compile_android/requirements.txt b/tools/cross_compile_android/requirements.txt
new file mode 100644
index 000000000..62e8ee3b0
--- /dev/null
+++ b/tools/cross_compile_android/requirements.txt
@@ -0,0 +1,3 @@
+gitpython
+Jinja2
+tqdm
diff --git a/tools/cross_compile_android/templates/cross_compile.tmpl.sh b/tools/cross_compile_android/templates/cross_compile.tmpl.sh
new file mode 100644
index 000000000..784e822ca
--- /dev/null
+++ b/tools/cross_compile_android/templates/cross_compile.tmpl.sh
@@ -0,0 +1,29 @@
+#!/bin/bash
+# Copyright (C) 2023 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
+set -x -e
+export HOST_ARCH={{ plat_name }}-linux-android
+export TOOLCHAIN={{ ndk_path }}/toolchains/llvm/prebuilt/{{ host_platform_name }}-x86_64/bin
+export TOOL_PREFIX=$TOOLCHAIN/$HOST_ARCH
+export PLATFORM_API={{ api_level }}
+{% if plat_name == "armv7a" -%}
+export CXX=${TOOL_PREFIX}eabi${PLATFORM_API}-clang++
+export CPP="${TOOL_PREFIX}eabi${PLATFORM_API}-clang++ -E"
+export CC=${TOOL_PREFIX}eabi${PLATFORM_API}-clang
+{% else %}
+export CXX=${TOOL_PREFIX}${PLATFORM_API}-clang++
+export CPP="${TOOL_PREFIX}${PLATFORM_API}-clang++ -E"
+export CC=${TOOL_PREFIX}${PLATFORM_API}-clang
+{% endif %}
+export AR=$TOOLCHAIN/llvm-ar
+export RANLIB=$TOOLCHAIN/llvm-ranlib
+export LD=$TOOLCHAIN/ld
+export READELF=$TOOLCHAIN/llvm-readelf
+export CFLAGS='-fPIC -DANDROID'
+./configure --host=$HOST_ARCH --target=$HOST_ARCH --build={{ host_system_name }} \
+--with-build-python={{ host_python_path }} --enable-shared \
+--enable-ipv6 ac_cv_file__dev_ptmx=yes ac_cv_file__dev_ptc=no --without-ensurepip \
+ac_cv_little_endian_double=yes
+make BLDSHARED="$CC -shared" CROSS-COMPILE=$TOOL_PREFIX- CROSS_COMPILE_TARGET=yes \
+INSTSONAME=libpython{{ python_version }}.so
+make install prefix={{ android_py_install_path_prefix }}/Python-$HOST_ARCH/_install
diff --git a/tools/cross_compile_android/templates/toolchain_default.tmpl.cmake b/tools/cross_compile_android/templates/toolchain_default.tmpl.cmake
new file mode 100644
index 000000000..3c9752f43
--- /dev/null
+++ b/tools/cross_compile_android/templates/toolchain_default.tmpl.cmake
@@ -0,0 +1,73 @@
+# Copyright (C) 2023 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
+
+# toolchain file to cross compile Qt for Python wheels for Android
+cmake_minimum_required(VERSION 3.23)
+include_guard(GLOBAL)
+set(CMAKE_SYSTEM_NAME Android)
+{% if plat_name == "armv7a" -%}
+set(CMAKE_SYSTEM_PROCESSOR armv7-a)
+{% else %}
+set(CMAKE_SYSTEM_PROCESSOR {{ plat_name }})
+{% endif %}
+set(CMAKE_ANDROID_API {{ api_level }})
+set(CMAKE_ANDROID_NDK {{ ndk_path }})
+set(CMAKE_ANDROID_ARCH_ABI {{ android_abi }})
+set(CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION clang)
+set(CMAKE_ANDROID_STL_TYPE c++_shared)
+if(NOT DEFINED ANDROID_PLATFORM AND NOT DEFINED ANDROID_NATIVE_API_LEVEL)
+ set(ANDROID_PLATFORM "android-{{ api_level }}" CACHE STRING "")
+endif()
+set(ANDROID_SDK_ROOT {{ sdk_path }})
+{% if plat_name == "armv7a" -%}
+set(_TARGET_NAME_ENDING "eabi{{ api_level }}")
+{% else %}
+set(_TARGET_NAME_ENDING "{{ api_level }}")
+{% endif %}
+set(QT_COMPILER_FLAGS "--target={{ plat_name }}-linux-android${_TARGET_NAME_ENDING} \
+ -fomit-frame-pointer \
+ -march={{ gcc_march }} \
+ -msse4.2 \
+ -mpopcnt \
+ -m{{ plat_bits }} \
+ -fPIC \
+ -I{{ target_python_path }}/include/python{{ python_version }} \
+ -Wno-unused-command-line-argument")
+set(QT_COMPILER_FLAGS_RELEASE "-O2 -pipe")
+
+# FIXME
+# https://gitlab.kitware.com/cmake/cmake/-/issues/23670
+# The CMake Android toolchain does not allow RPATHS. Hence CMAKE_INSTALL_RPATH does not work.
+# Currently the linker flags are set directly as -Wl,-rpath='$ORIGIN' -Wl,-rpath='$ORIGIN/Qt/lib'
+# set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
+# set(CMAKE_INSTALL_RPATH "$ORIGIN")
+
+set(QT_LINKER_FLAGS "-Wl,-O1 -Wl,--hash-style=gnu -Wl,-rpath='$ORIGIN' -Wl,-rpath='$ORIGIN/Qt/lib' \
+ -Wl,--as-needed -L{{ qt_install_path }}/android_{{ qt_plat_name }}/lib \
+ -L{{ qt_install_path }}/android_{{ qt_plat_name }}/plugins/platforms \
+ -L{{ target_python_path }}/lib \
+ -lpython{{ python_version }}")
+set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+
+add_compile_definitions(ANDROID)
+
+include(CMakeInitializeConfigs)
+function(cmake_initialize_per_config_variable _PREFIX _DOCSTRING)
+ if (_PREFIX MATCHES "CMAKE_(C|CXX|ASM)_FLAGS")
+ set(CMAKE_${CMAKE_MATCH_1}_FLAGS_INIT "${QT_COMPILER_FLAGS}")
+ foreach (config DEBUG RELEASE MINSIZEREL RELWITHDEBINFO)
+ if (DEFINED QT_COMPILER_FLAGS_${config})
+ set(CMAKE_${CMAKE_MATCH_1}_FLAGS_${config}_INIT "${QT_COMPILER_FLAGS_${config}}")
+ endif()
+ endforeach()
+ endif()
+ if (_PREFIX MATCHES "CMAKE_(SHARED|MODULE|EXE)_LINKER_FLAGS")
+ foreach (config SHARED MODULE EXE)
+ set(CMAKE_${config}_LINKER_FLAGS_INIT "${QT_LINKER_FLAGS}")
+ endforeach()
+ endif()
+ _cmake_initialize_per_config_variable(${ARGV})
+endfunction()
diff --git a/tools/doc_modules.py b/tools/doc_modules.py
index 7738e21bd..d46f4db02 100644
--- a/tools/doc_modules.py
+++ b/tools/doc_modules.py
@@ -54,7 +54,7 @@ def required_typesystems(module):
parser.setContentHandler(handler)
parser.parse(typesystem_file)
except Exception as e:
- print(f"Error parsing {typesystem_file}: {e}", file=sys.stderr)
+ print(f"Warning: XML error parsing {typesystem_file}: {e}", file=sys.stderr)
return handler.required_modules
@@ -189,6 +189,9 @@ if __name__ == "__main__":
module_dependency_dict = {}
for m in SOURCE_DIR.glob("Qt*"):
module = m.name
+ # QtGraphs duplicates symbols from QtDataVisualization causing shiboken errors
+ if module == "QtDataVisualization":
+ continue
qt_include_path = qt_include_dir / module
if qt_include_path.is_dir():
module_dependency_dict[module] = required_typesystems(module)
diff --git a/tools/example_gallery/main.py b/tools/example_gallery/main.py
index 5e9029f27..b5aa632c0 100644
--- a/tools/example_gallery/main.py
+++ b/tools/example_gallery/main.py
@@ -14,13 +14,43 @@ since there is no special requirements.
import json
import math
+import os
import shutil
+import zipfile
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
+from dataclasses import dataclass
+from enum import IntEnum, Enum
from pathlib import Path
from textwrap import dedent
+
+class Format(Enum):
+ RST = 0
+ MD = 1
+
+
+class ModuleType(IntEnum):
+ ESSENTIALS = 0
+ ADDONS = 1
+ M2M = 2
+
+
+SUFFIXES = {Format.RST: "rst", Format.MD: "md"}
+
+
opt_quiet = False
+
+
+LITERAL_INCLUDE = ".. literalinclude::"
+
+
+IMAGE_SUFFIXES = (".png", ".jpg", ".jpeg", ".gif", ".svg", ".svgz", ".webp")
+
+
+IGNORED_SUFFIXES = IMAGE_SUFFIXES + (".pdf", ".pyc", ".obj", ".mesh")
+
+
suffixes = {
".h": "cpp",
".cpp": "cpp",
@@ -31,9 +61,25 @@ suffixes = {
".qrc": "xml",
".ui": "xml",
".xbel": "xml",
+ ".xml": "xml",
}
+BASE_CONTENT = """\
+Examples
+========
+
+ A collection of examples are provided with |project| to help new users
+ to understand different use cases of the module.
+
+ You can find all these examples inside the
+ `pyside-setup <https://code.qt.io/cgit/pyside/pyside-setup.git/>`_ repository
+ on the `examples <https://code.qt.io/cgit/pyside/pyside-setup.git/tree/examples>`_
+ directory.
+
+"""
+
+
def ind(x):
return " " * 4 * x
@@ -41,10 +87,8 @@ def ind(x):
def get_lexer(path):
if path.name == "CMakeLists.txt":
return "cmake"
- suffix = path.suffix
- if suffix in suffixes:
- return suffixes[suffix]
- return "text"
+ lexer = suffixes.get(path.suffix)
+ return lexer if lexer else "text"
def add_indent(s, level):
@@ -57,6 +101,110 @@ def add_indent(s, level):
return new_s
+def check_img_ext(i):
+ """Check whether path is an image."""
+ return i.suffix in IMAGE_SUFFIXES
+
+
+@dataclass
+class ModuleDescription:
+ """Specifies a sort key and type for a Qt module."""
+ sort_key: int = 0
+ module_type: ModuleType = ModuleType.ESSENTIALS
+ description: str = ''
+
+
+MODULE_DESCRIPTIONS = {
+ "async": ModuleDescription(16, ModuleType.ESSENTIALS, ''),
+ "corelib": ModuleDescription(15, ModuleType.ESSENTIALS, ''),
+ "dbus": ModuleDescription(22, ModuleType.ESSENTIALS, ''),
+ "designer": ModuleDescription(11, ModuleType.ESSENTIALS, ''),
+ "gui": ModuleDescription(25, ModuleType.ESSENTIALS, ''),
+ "network": ModuleDescription(20, ModuleType.ESSENTIALS, ''),
+ "opengl": ModuleDescription(26, ModuleType.ESSENTIALS, ''),
+ "qml": ModuleDescription(0, ModuleType.ESSENTIALS, ''),
+ "quick": ModuleDescription(1, ModuleType.ESSENTIALS, ''),
+ "quickcontrols": ModuleDescription(2, ModuleType.ESSENTIALS, ''),
+ "samplebinding": ModuleDescription(30, ModuleType.ESSENTIALS, ''),
+ "scriptableapplication": ModuleDescription(30, ModuleType.ESSENTIALS, ''),
+ "sql": ModuleDescription(21, ModuleType.ESSENTIALS, ''),
+ "uitools": ModuleDescription(12, ModuleType.ESSENTIALS, ''),
+ "widgetbinding": ModuleDescription(30, ModuleType.ESSENTIALS, ''),
+ "widgets": ModuleDescription(10, ModuleType.ESSENTIALS, ''),
+ "xml": ModuleDescription(24, ModuleType.ESSENTIALS, ''),
+ "Qt Demos": ModuleDescription(0, ModuleType.ADDONS, ''), # from Qt repos
+ "3d": ModuleDescription(30, ModuleType.ADDONS, ''),
+ "axcontainer": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "bluetooth": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "charts": ModuleDescription(12, ModuleType.ADDONS, ''),
+ "datavisualization": ModuleDescription(11, ModuleType.ADDONS, ''),
+ "demos": ModuleDescription(0, ModuleType.ADDONS, ''),
+ "external": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "graphs": ModuleDescription(10, ModuleType.ADDONS, ''),
+ "httpserver": ModuleDescription(0, ModuleType.ADDONS, ''),
+ "location": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "multimedia": ModuleDescription(12, ModuleType.ADDONS, ''),
+ "networkauth": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "pdf": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "pdfwidgets": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "quick3d": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "remoteobjects": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "serialbus": ModuleDescription(30, ModuleType.ADDONS, ''),
+ "serialport": ModuleDescription(30, ModuleType.ADDONS, ''),
+ "spatialaudio": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "speech": ModuleDescription(20, ModuleType.ADDONS, ''),
+ "statemachine": ModuleDescription(30, ModuleType.ADDONS, ''),
+ "webchannel": ModuleDescription(30, ModuleType.ADDONS, ''),
+ "webenginequick": ModuleDescription(15, ModuleType.ADDONS, ''),
+ "webenginewidgets": ModuleDescription(16, ModuleType.ADDONS, ''),
+ "coap": ModuleDescription(0, ModuleType.M2M, ''),
+ "mqtt": ModuleDescription(0, ModuleType.M2M, ''),
+ "opcua": ModuleDescription(0, ModuleType.M2M, '')
+}
+
+
+def module_sort_key(name):
+ """Return key for sorting modules."""
+ description = MODULE_DESCRIPTIONS.get(name)
+ module_type = int(description.module_type) if description else 5
+ sort_key = description.sort_key if description else 100
+ return f"{module_type}:{sort_key:04}:{name}"
+
+
+def module_title(name):
+ """Return title for a module."""
+ result = name.title()
+ description = MODULE_DESCRIPTIONS.get(name)
+ if description:
+ if description.description:
+ result += " - " + description.description
+ if description.module_type == ModuleType.M2M:
+ result += " (M2M)"
+ elif description.module_type == ModuleType.ADDONS:
+ result += " (Add-ons)"
+ else:
+ result += " (Essentials)"
+ return result
+
+
+@dataclass
+class ExampleData:
+ """Example data for formatting the gallery."""
+
+ def __init__(self):
+ self.headline = ""
+
+ example: str
+ module: str
+ extra: str
+ doc_file: str
+ file_format: Format
+ abs_path: str
+ has_doc: bool
+ img_doc: Path
+ headline: str
+
+
def get_module_gallery(examples):
"""
This function takes a list of dictionaries, that contain examples
@@ -64,45 +212,40 @@ def get_module_gallery(examples):
"""
gallery = (
- ".. panels::\n"
- f"{ind(1)}:container: container-lg pb-3\n"
- f"{ind(1)}:column: col-lg-3 col-md-6 col-sm-6 col-xs-12 p-2\n\n"
+ ".. grid:: 1 4 4 4\n"
+ f"{ind(1)}:gutter: 2\n\n"
)
# Iteration per rows
for i in range(math.ceil(len(examples))):
e = examples[i]
- url = e["rst"].replace(".rst", ".html")
- name = e["example"]
- underline = f'{e["module"]}'
+ suffix = SUFFIXES[e.file_format]
+ url = e.doc_file.replace(f".{suffix}", ".html")
+ name = e.example
+ underline = e.module
- if e["extra"]:
- underline += f'/{e["extra"]}'
+ if e.extra:
+ underline += f"/{e.extra}"
if i > 0:
- gallery += f"{ind(1)}---\n"
- elif e["img_doc"]:
- gallery += f"{ind(1)}---\n"
-
- if e["img_doc"]:
- img_name = e['img_doc'].name
- else:
- img_name = "../example_no_image.png"
-
- gallery += f"{ind(1)}:img-top: {img_name}\n"
- gallery += f"{ind(1)}:img-top-cls: + d-flex align-self-center\n\n"
+ gallery += "\n"
+ img_name = e.img_doc.name if e.img_doc else "../example_no_image.png"
# Fix long names
if name.startswith("chapter"):
name = name.replace("chapter", "c")
+ elif name.startswith("advanced"):
+ name = name.replace("advanced", "a")
- gallery += f"{ind(1)}`{name} <{url}>`_\n"
- gallery += f"{ind(1)}+++\n"
- gallery += f"{ind(1)}{underline}\n"
- gallery += f"\n{ind(1)}.. link-button:: {url}\n"
- gallery += f"{ind(2)}:type: url\n"
- gallery += f"{ind(2)}:text: Go to Example\n"
- gallery += f"{ind(2)}:classes: btn-qt btn-block stretched-link\n"
+ desc = e.headline
+ if not desc:
+ desc = f"found in the ``{underline}`` directory."
+
+ gallery += f"{ind(1)}.. grid-item-card:: {name}\n"
+ gallery += f"{ind(2)}:class-item: cover-img\n"
+ gallery += f"{ind(2)}:link: {url}\n"
+ gallery += f"{ind(2)}:img-top: {img_name}\n\n"
+ gallery += f"{ind(2)}{desc}\n"
return f"{gallery}\n"
@@ -116,18 +259,69 @@ def remove_licenses(s):
return "\n".join(new_s)
-def get_code_tabs(files, project_dir):
+def make_zip_archive(zip_name, src, skip_dirs=None):
+ src_path = Path(src).expanduser().resolve(strict=True)
+ if skip_dirs is None:
+ skip_dirs = []
+ if not isinstance(skip_dirs, list):
+ print("Error: A list needs to be passed for 'skip_dirs'")
+ return
+ with zipfile.ZipFile(src_path.parents[0] / Path(zip_name), 'w', zipfile.ZIP_DEFLATED) as zf:
+ for file in src_path.rglob('*'):
+ skip = False
+ _parts = file.relative_to(src_path).parts
+ for sd in skip_dirs:
+ if sd in _parts:
+ skip = True
+ break
+ if not skip:
+ zf.write(file, file.relative_to(src_path.parent))
+
+
+def doc_file(project_dir, project_file_entry):
+ """Return the (optional) .rstinc file describing a source file."""
+ rst_file = project_dir
+ if rst_file.name != "doc": # Special case: Dummy .pyproject file in doc dir
+ rst_file /= "doc"
+ rst_file /= Path(project_file_entry).name + ".rstinc"
+ return rst_file if rst_file.is_file() else None
+
+
+def get_code_tabs(files, project_dir, file_format):
content = "\n"
+ # Prepare ZIP file, and copy to final destination
+ zip_name = f"{project_dir.name}.zip"
+ make_zip_archive(zip_name, project_dir, skip_dirs=["doc"])
+ zip_src = f"{project_dir}.zip"
+ zip_dst = EXAMPLES_DOC / zip_name
+ shutil.move(zip_src, zip_dst)
+
+ if file_format == Format.RST:
+ content += f":download:`Download this example <{zip_name}>`\n\n"
+ else:
+ content += f"{{download}}`Download this example <{zip_name}>`\n\n"
+ content += "```{eval-rst}\n"
+
for i, project_file in enumerate(files):
+ if i == 0:
+ content += ".. tab-set::\n\n"
+
pfile = Path(project_file)
- if pfile.suffix in (".jpg", ".pdf", ".png", ".pyc", ".svg", ".svgz"):
+ if pfile.suffix in IGNORED_SUFFIXES:
continue
- content += f".. tabbed:: {project_file}\n\n"
+ content += f"{ind(1)}.. tab-item:: {project_file}\n\n"
+
+ doc_rstinc_file = doc_file(project_dir, project_file)
+ if doc_rstinc_file:
+ indent = ind(2)
+ for line in doc_rstinc_file.read_text("utf-8").split("\n"):
+ content += indent + line + "\n"
+ content += "\n"
lexer = get_lexer(pfile)
- content += add_indent(f".. code-block:: {lexer}", 1)
+ content += add_indent(f"{ind(1)}.. code-block:: {lexer}", 1)
content += "\n"
_path = project_dir / project_file
@@ -144,8 +338,12 @@ def get_code_tabs(files, project_dir):
file=sys.stderr)
raise
- content += add_indent(_file_content, 2)
+ content += add_indent(_file_content, 3)
content += "\n\n"
+
+ if file_format == Format.MD:
+ content += "```"
+
return content
@@ -163,6 +361,253 @@ def get_header_title(example_dir):
)
+def rel_path(from_path, to_path):
+ """Determine relative paths for paths that are not subpaths (where
+ relative_to() fails) via a common root."""
+ common = Path(*os.path.commonprefix([from_path.parts, to_path.parts]))
+ up_dirs = len(from_path.parts) - len(common.parts)
+ prefix = up_dirs * "../"
+ rel_to_common = os.fspath(to_path.relative_to(common))
+ return f"{prefix}{rel_to_common}"
+
+
+def read_rst_file(project_dir, project_files, doc_rst):
+ """Read the example .rst file and expand literal includes to project files
+ by relative paths to the example directory. Note: sphinx does not
+ handle absolute paths as expected, they need to be relative."""
+ content = ""
+ with open(doc_rst, encoding="utf-8") as doc_f:
+ content = doc_f.read()
+ if LITERAL_INCLUDE not in content:
+ return content
+
+ result = []
+ path_to_example = rel_path(EXAMPLES_DOC, project_dir)
+ for line in content.split("\n"):
+ if line.startswith(LITERAL_INCLUDE):
+ file = line[len(LITERAL_INCLUDE) + 1:].strip()
+ if file in project_files:
+ line = f"{LITERAL_INCLUDE} {path_to_example}/{file}"
+ result.append(line)
+ return "\n".join(result)
+
+
+def get_headline(text, file_format):
+ """Find the headline in the .rst file."""
+ if file_format == Format.RST:
+ underline = text.find("\n====")
+ if underline != -1:
+ start = text.rfind("\n", 0, underline - 1)
+ return text[start + 1:underline]
+ elif file_format == Format.MD:
+ headline = text.find("# ")
+ if headline != -1:
+ new_line = text.find("\n", headline + 1)
+ if new_line != -1:
+ return text[headline + 2:new_line].strip()
+ return ""
+
+
+def get_doc_source_file(original_doc_dir, example_name):
+ """Find the doc source file, return (Path, Format)."""
+ if original_doc_dir.is_dir():
+ for file_format in (Format.RST, Format.MD):
+ suffix = SUFFIXES[file_format]
+ result = original_doc_dir / f"{example_name}.{suffix}"
+ if result.is_file():
+ return result, file_format
+ return None, Format.RST
+
+
+def get_screenshot(image_dir, example_name):
+ """Find screen shot: We look for an image with the same
+ example_name first, if not, we select the first."""
+ if not image_dir.is_dir():
+ return None
+ images = [i for i in image_dir.glob("*") if i.is_file() and check_img_ext(i)]
+ example_images = [i for i in images if i.name.startswith(example_name)]
+ if example_images:
+ return example_images[0]
+ if images:
+ return images[0]
+ return None
+
+
+def write_resources(src_list, dst):
+ """Write a list of example resource paths to the dst path."""
+ for src in src_list:
+ resource_written = shutil.copy(src, dst / src.name)
+ if not opt_quiet:
+ print("Written resource:", resource_written)
+
+
+@dataclass
+class ExampleParameters:
+ """Parameters obtained from scanning the examples directory."""
+
+ def __init__(self):
+ self.file_format = Format.RST
+ self.src_doc_dir = self.src_doc_file_path = self.src_screenshot = None
+ self.extra_names = ""
+
+ example_dir: Path
+ module_name: str
+ example_name: str
+ extra_names: str
+ file_format: Format
+ target_doc_file: str
+ src_doc_dir: Path
+ src_doc_file_path: Path
+ src_screenshot: Path
+
+
+def detect_pyside_example(example_root, pyproject_file):
+ """Detemine parameters of a PySide example."""
+ p = ExampleParameters()
+
+ p.example_dir = pyproject_file.parent
+ if p.example_dir.name == "doc": # Dummy pyproject in doc dir (scriptableapplication)
+ p.example_dir = p.example_dir.parent
+
+ parts = p.example_dir.parts[len(example_root.parts):]
+ p.module_name = parts[0]
+ p.example_name = parts[-1]
+ # handling subdirectories besides the module level and the example
+ p.extra_names = "" if len(parts) == 2 else "_".join(parts[1:-1])
+
+ # Check for a 'doc' directory inside the example
+ src_doc_dir = p.example_dir / "doc"
+
+ if src_doc_dir.is_dir():
+ src_doc_file_path, fmt = get_doc_source_file(src_doc_dir, p.example_name)
+ if src_doc_file_path:
+ p.src_doc_file_path = src_doc_file_path
+ p.file_format = fmt
+ p.src_doc_dir = src_doc_dir
+ p.src_screenshot = get_screenshot(src_doc_dir, p.example_name)
+
+ target_suffix = SUFFIXES[p.file_format]
+ doc_file = f"example_{p.module_name}_{p.extra_names}_{p.example_name}.{target_suffix}"
+ p.target_doc_file = doc_file.replace("__", "_")
+ return p
+
+
+def detect_qt_example(example_root, pyproject_file):
+ """Detemine parameters of an example from a Qt repository."""
+ p = ExampleParameters()
+
+ p.example_dir = pyproject_file.parent
+ p.module_name = "Qt Demos"
+ p.example_name = p.example_dir.name
+ # Check for a 'doc' directory inside the example (qdoc)
+ doc_root = p.example_dir / "doc"
+ if doc_root.is_dir():
+ src_doc_file_path, fmt = get_doc_source_file(doc_root / "src", p.example_name)
+ if src_doc_file_path:
+ p.src_doc_file_path = src_doc_file_path
+ p.file_format = fmt
+ p.src_doc_dir = doc_root
+ p.src_screenshot = get_screenshot(doc_root / "images", p.example_name)
+
+ target_suffix = SUFFIXES[p.file_format]
+ p.target_doc_file = f"example_qtdemos_{p.example_name}.{target_suffix}"
+ return p
+
+
+def write_example(example_root, pyproject_file, pyside_example=True):
+ """Read the project file and documentation, create the .rst file and
+ copy the data. Return a tuple of module name and a dict of example data."""
+ p = (detect_pyside_example(example_root, pyproject_file) if pyside_example
+ else detect_qt_example(example_root, pyproject_file))
+
+ result = ExampleData()
+ result.example = p.example_name
+ result.module = p.module_name
+ result.extra = p.extra_names
+ result.doc_file = p.target_doc_file
+ result.file_format = p.file_format
+ result.abs_path = str(p.example_dir)
+ result.has_doc = bool(p.src_doc_file_path)
+ result.img_doc = p.src_screenshot
+
+ files = []
+ try:
+ with pyproject_file.open("r", encoding="utf-8") as pyf:
+ pyproject = json.load(pyf)
+ # iterate through the list of files in .pyproject and
+ # check if they exist, before appending to the list.
+ for f in pyproject["files"]:
+ if not Path(f).exists:
+ print(f"example_gallery: {f} listed in {pyproject_file} does not exist")
+ raise FileNotFoundError
+ else:
+ files.append(f)
+ except (json.JSONDecodeError, KeyError, FileNotFoundError) as e:
+ print(f"example_gallery: error reading {pyproject_file}: {e}")
+ raise
+
+ headline = ""
+ if files:
+ doc_file = EXAMPLES_DOC / p.target_doc_file
+ with open(doc_file, "w", encoding="utf-8") as out_f:
+ if p.src_doc_file_path:
+ content_f = read_rst_file(p.example_dir, files, p.src_doc_file_path)
+ headline = get_headline(content_f, p.file_format)
+ if not headline:
+ print(f"example_gallery: No headline found in {doc_file}",
+ file=sys.stderr)
+
+ # Copy other files in the 'doc' directory, but
+ # excluding the main '.rst' file and all the
+ # directories.
+ resources = []
+ if pyside_example:
+ for _f in p.src_doc_dir.glob("*"):
+ if _f != p.src_doc_file_path and not _f.is_dir():
+ resources.append(_f)
+ else: # Qt example: only use image.
+ if p.src_screenshot:
+ resources.append(p.src_screenshot)
+ write_resources(resources, EXAMPLES_DOC)
+ else:
+ content_f = get_header_title(p.example_dir)
+ content_f += get_code_tabs(files, pyproject_file.parent, p.file_format)
+ out_f.write(content_f)
+
+ if not opt_quiet:
+ print(f"Written: {doc_file}")
+ else:
+ if not opt_quiet:
+ print("Empty '.pyproject' file, skipping")
+
+ result.headline = headline
+
+ return (p.module_name, result)
+
+
+def example_sort_key(example: ExampleData):
+ name = example.example
+ return "AAA" + name if "gallery" in name else name
+
+
+def sort_examples(example):
+ result = {}
+ for module in example.keys():
+ result[module] = sorted(example.get(module), key=example_sort_key)
+ return result
+
+
+def scan_examples_dir(examples_dir, pyside_example=True):
+ """Scan a directory of examples."""
+ for pyproject_file in examples_dir.glob("**/*.pyproject"):
+ if pyproject_file.name != "examples.pyproject":
+ module_name, data = write_example(examples_dir, pyproject_file,
+ pyside_example)
+ if module_name not in examples:
+ examples[module_name] = []
+ examples[module_name].append(data)
+
+
if __name__ == "__main__":
# Only examples with a '.pyproject' file will be listed.
DIR = Path(__file__).parent
@@ -173,13 +618,14 @@ if __name__ == "__main__":
gallery = ""
parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)
- TARGET_HELP = f"Directory into which to generate RST files (default: {str(EXAMPLES_DOC)})"
+ TARGET_HELP = f"Directory into which to generate Doc files (default: {str(EXAMPLES_DOC)})"
parser.add_argument("--target", "-t", action="store", dest="target_dir", help=TARGET_HELP)
+ parser.add_argument("--qt-src-dir", "-s", action="store", help="Qt source directory")
parser.add_argument("--quiet", "-q", action="store_true", help="Quiet")
options = parser.parse_args()
opt_quiet = options.quiet
if options.target_dir:
- EXAMPLES_DOC = Path(options.target_dir)
+ EXAMPLES_DOC = Path(options.target_dir).resolve()
# This main loop will be in charge of:
# * Getting all the .pyproject files,
@@ -189,128 +635,22 @@ if __name__ == "__main__":
examples = {}
# Create the 'examples' directory if it doesn't exist
- if not EXAMPLES_DOC.is_dir():
- EXAMPLES_DOC.mkdir()
-
- for pyproject_file in EXAMPLES_DIR.glob("**/*.pyproject"):
- if pyproject_file.name == "examples.pyproject":
- continue
- example_dir = pyproject_file.parent
- if example_dir.name == "doc": # Dummy pyproject in doc dir (scriptableapplication)
- example_dir = example_dir.parent
-
- parts = example_dir.parts[len(EXAMPLES_DIR.parts):]
-
- module_name = parts[0]
- example_name = parts[-1]
- # handling subdirectories besides the module level and the example
- extra_names = "" if len(parts) == 2 else "_".join(parts[1:-1])
-
- rst_file = f"example_{module_name}_{extra_names}_{example_name}.rst"
-
- def check_img_ext(i):
- EXT = (".png", ".jpg", ".jpeg", ".gif")
- if i.suffix in EXT:
- return True
- return False
-
- # Check for a 'doc' directory inside the example
- has_doc = False
- img_doc = None
- original_doc_dir = Path(example_dir / "doc")
- if original_doc_dir.is_dir():
- has_doc = True
- images = [i for i in original_doc_dir.glob("*") if i.is_file() and check_img_ext(i)]
- if len(images) > 0:
- # We look for an image with the same example_name first, if not, we select the first
- image_path = [i for i in images if example_name in str(i)]
- if not image_path:
- image_path = images[0]
- else:
- img_doc = image_path[0]
-
- if module_name not in examples:
- examples[module_name] = []
-
- examples[module_name].append(
- {
- "example": example_name,
- "module": module_name,
- "extra": extra_names,
- "rst": rst_file,
- "abs_path": str(example_dir),
- "has_doc": has_doc,
- "img_doc": img_doc,
- }
- )
-
- files = []
- try:
- with pyproject_file.open("r", encoding="utf-8") as pyf:
- pyproject = json.load(pyf)
- # iterate through the list of files in .pyproject and
- # check if they exist, before appending to the list.
- for f in pyproject["files"]:
- if not Path(f).exists:
- print(f"example_gallery: {f} listed in {pyproject_file} does not exist")
- raise FileNotFoundError
- else:
- files.append(f)
- except (json.JSONDecodeError, KeyError, FileNotFoundError) as e:
- print(f"example_gallery: error reading {pyproject_file}: {e}")
- raise
-
- if files:
- rst_file_full = EXAMPLES_DOC / rst_file
-
- with open(rst_file_full, "w", encoding="utf-8") as out_f:
- if has_doc:
- doc_rst = original_doc_dir / f"{example_name}.rst"
-
- with open(doc_rst, encoding="utf-8") as doc_f:
- content_f = doc_f.read()
-
- # Copy other files in the 'doc' directory, but
- # excluding the main '.rst' file and all the
- # directories.
- for _f in original_doc_dir.glob("*"):
- if _f == doc_rst or _f.is_dir():
- continue
- src = _f
- dst = EXAMPLES_DOC / _f.name
-
- resource_written = shutil.copy(src, dst)
- if not opt_quiet:
- print("Written resource:", resource_written)
- else:
- content_f = get_header_title(example_dir)
- content_f += get_code_tabs(files, pyproject_file.parent)
- out_f.write(content_f)
-
- if not opt_quiet:
- print(f"Written: {EXAMPLES_DOC}/{rst_file}")
- else:
- if not opt_quiet:
- print("Empty '.pyproject' file, skipping")
-
- base_content = dedent(
- """\
- ..
- This file was auto-generated from the 'pyside-setup/tools/example_gallery'
- All editions in this file will be lost.
-
- |project| Examples
- ===================
-
- A collection of examples are provided with |project| to help new users
- to understand different use cases of the module.
-
- You can find all these examples inside the ``pyside-setup`` on the ``examples``
- directory, or you can access them after installing |pymodname| from ``pip``
- inside the ``site-packages/PySide6/examples`` directory.
-
- """
- )
+ # If it does exist, remove it and create a new one to start fresh
+ if EXAMPLES_DOC.is_dir():
+ shutil.rmtree(EXAMPLES_DOC, ignore_errors=True)
+ if not opt_quiet:
+ print("WARNING: Deleted old html directory")
+ EXAMPLES_DOC.mkdir(exist_ok=True)
+
+ scan_examples_dir(EXAMPLES_DIR)
+ if options.qt_src_dir:
+ qt_src = Path(options.qt_src_dir)
+ if not qt_src.is_dir():
+ print("Invalid Qt source directory: {}", file=sys.stderr)
+ sys.exit(-1)
+ scan_examples_dir(qt_src.parent / "qtdoc", pyside_example=False)
+
+ examples = sort_examples(examples)
# We generate a 'toctree' at the end of the file, to include the new
# 'example' rst files, so we get no warnings, and also that users looking
@@ -329,12 +669,14 @@ if __name__ == "__main__":
# Writing the main example rst file.
index_files = []
with open(f"{EXAMPLES_DOC}/index.rst", "w") as f:
- f.write(base_content)
- for module_name, e in sorted(examples.items()):
+ f.write(BASE_CONTENT)
+ for module_name in sorted(examples.keys(), key=module_sort_key):
+ e = examples.get(module_name)
for i in e:
- index_files.append(i["rst"])
- f.write(f"{module_name.title()}\n")
- f.write(f"{'*' * len(module_name.title())}\n")
+ index_files.append(i.doc_file)
+ title = module_title(module_name)
+ f.write(f"{title}\n")
+ f.write(f"{'*' * len(title)}\n")
f.write(get_module_gallery(e))
f.write("\n\n")
f.write(footer_index)
diff --git a/tools/missing_bindings-requirements.txt b/tools/missing_bindings-requirements.txt
deleted file mode 100644
index bbe8e7ac2..000000000
--- a/tools/missing_bindings-requirements.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-pyside6
-pyqt5
-beautifulsoup4
-pyqt3d
-pyqtchart
-pyqtdatavisualization
-pyqtwebengine
diff --git a/tools/missing_bindings/config.py b/tools/missing_bindings/config.py
index a301b9716..ddaf20685 100644
--- a/tools/missing_bindings/config.py
+++ b/tools/missing_bindings/config.py
@@ -1,7 +1,6 @@
# 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
-
modules_to_test = {
# 6.0
'QtCore': 'qtcore-module.html',
@@ -10,8 +9,8 @@ modules_to_test = {
'QtQml': 'qtqml-module.html',
'QtQuick': 'qtquick-module.html',
'QtQuickWidgets': 'qtquickwidgets-module.html',
- 'QtQuickControls2': 'qtquickcontrols2-module.html',
- 'QtQuick3D': 'qtquick3d-module.html',
+ # Broken in 6.5.0
+ #'QtQuickControls2': 'qtquickcontrols-module.html',
'QtSql': 'qtsql-module.html',
'QtWidgets': 'qtwidgets-module.html',
'QtConcurrent': 'qtconcurrent-module.html',
@@ -20,10 +19,10 @@ modules_to_test = {
'QtOpenGL': 'qtopengl-module.html',
'QtPrintSupport': 'qtprintsupport-module.html',
'QtSvg': 'qtsvg-module.html',
+ 'QtSvgWidgets': 'qtsvgwidgets-module.html',
'QtUiTools': 'qtuitools-module.html',
'QtXml': 'qtxml-module.html',
'QtTest': 'qttest-module.html',
- # 'QtXmlPatterns': 'qtxmlpatterns-module.html', # in Qt5 compat
'Qt3DCore': 'qt3dcore-module.html',
'Qt3DInput': 'qt3dinput-module.html',
'Qt3DLogic': 'qt3dlogic-module.html',
@@ -31,6 +30,7 @@ modules_to_test = {
'Qt3DAnimation': 'qt3danimation-module.html',
'Qt3DExtras': 'qt3dextras-module.html',
'QtNetworkAuth': 'qtnetworkauth-module.html',
+ 'QtStateMachine': 'qtstatemachine-module.html',
# 'QtCoAp' -- TODO
# 'QtMqtt' -- TODO
# 'QtOpcUA' -- TODO
@@ -52,17 +52,23 @@ modules_to_test = {
'QtWebEngineQuick': 'qtwebenginequick-module.html',
'QtWebEngineWidgets': 'qtwebenginewidgets-module.html',
'QtWebSockets': 'qtwebsockets-module.html',
+ 'QtHttpServer': 'qthttpserver-module.html',
- # 6.x
+ # 6.3
#'QtSpeech': 'qtspeech-module.html',
'QtMultimediaWidgets': 'qtmultimediawidgets-module.html',
- # 'QtLocation': 'qtlocation-module.html',
+ 'QtNfc': 'qtnfc-module.html',
+ 'QtQuick3D': 'qtquick3d-module.html',
+
+ # 6.4
+ 'QtPdf': 'qtpdf-module.html', # this include qtpdfwidgets
+ 'QtSpatialAudio': 'qtspatialaudio-module.html',
+
+ # 6.5
+ 'QtSerialBus': 'qtserialbus-module.html',
+ 'QtTextToSpeech': 'qttexttospeech-module.html',
+ 'QtLocation': 'qtlocation-module.html',
- # Not in 6
- # 'QtScriptTools': 'qtscripttools-module.html',
- # 'QtMacExtras': 'qtmacextras-module.html',
- # 'QtX11Extras': 'qtx11extras-module.html',
- # 'QtWinExtras': 'qtwinextras-module.html',
}
types_to_ignore = {
diff --git a/tools/missing_bindings/main.py b/tools/missing_bindings/main.py
index 3c7f68671..4c223050d 100644
--- a/tools/missing_bindings/main.py
+++ b/tools/missing_bindings/main.py
@@ -26,9 +26,11 @@ from pathlib import Path
from bs4 import BeautifulSoup
from config import modules_to_test, types_to_ignore
+import pandas as pd
+import matplotlib.pyplot as plt
qt_documentation_website_prefixes = {
- "6.3": "https://doc.qt.io/qt-6/",
+ "6.5": "https://doc.qt.io/qt-6/",
"dev": "https://doc-snapshots.qt.io/qt6-dev/",
}
@@ -57,8 +59,8 @@ def get_parser():
parser.add_argument(
"--qt-version",
"-v",
- default="6.3",
- choices=["6.3", "dev"],
+ default="6.5",
+ choices=["6.5", "dev"],
type=str,
dest="version",
help="the Qt version to use to check for types",
@@ -67,11 +69,17 @@ def get_parser():
"--which-missing",
"-w",
default="all",
- choices=["all", "in-pyqt", "not-in-pyqt"],
+ choices=["all", "in-pyqt", "not-in-pyqt", "in-pyside-not-in-pyqt"],
type=str,
dest="which_missing",
help="Which missing types to show (all, or just those that are not present in PyQt)",
)
+ parser.add_argument(
+ "--plot",
+ action="store_true",
+ help="Create module-wise bar plot comparisons for the missing bindings comparisons"
+ " between Qt, PySide6 and PyQt6",
+ )
return parser
@@ -94,7 +102,7 @@ def wikilog(*pargs, **kw):
computed_str = computed_str.replace(":", ":'''")
computed_str = f"{computed_str}'''\n"
elif style == "error":
- computed_str = computed_str.strip('\n')
+ computed_str = computed_str.strip("\n")
computed_str = f"''{computed_str}''\n"
elif style == "text_with_link":
computed_str = computed_str
@@ -118,9 +126,12 @@ if __name__ == "__main__":
pyside_package_name = "PySide6"
pyqt_package_name = "PyQt6"
+ data = {"module": [], "qt": [], "pyside": [], "pyqt": []}
total_missing_types_count = 0
total_missing_types_count_compared_to_pyqt = 0
total_missing_modules_count = 0
+ total_missing_pyqt_types_count = 0
+ total_missing_pyqt_modules_count = 0
wiki_file = open("missing_bindings_for_wiki_qt_io.txt", "w")
wiki_file.truncate()
@@ -199,6 +210,7 @@ if __name__ == "__main__":
f"Received error: {e_str}.\n",
style="error",
)
+ total_missing_pyqt_modules_count += 1
# Get C++ class list from documentation page.
page = request.urlopen(url)
@@ -213,49 +225,85 @@ if __name__ == "__main__":
if link_text not in types_to_ignore:
types_on_html_page.append(link_text)
- wikilog(f"Number of types in {module_name}: {len(types_on_html_page)}", style="bold_colon")
+ total_qt_types = len(types_on_html_page)
+ wikilog(f"Number of types in {module_name}: {total_qt_types}", style="bold_colon")
- missing_types_count = 0
+ missing_pyside_types_count = 0
+ missing_pyqt_types_count = 0
missing_types_compared_to_pyqt = 0
missing_types = []
for qt_type in types_on_html_page:
+ is_present_in_pyqt = False
+ is_present_in_pyside = False
+ missing_type = None
+
+ try:
+ pyqt_qualified_type = f"pyqt_tested_module.{qt_type}"
+ eval(pyqt_qualified_type)
+ is_present_in_pyqt = True
+ except Exception as e:
+ print(f"{type(e).__name__}: {e}")
+ missing_pyqt_types_count += 1
+ total_missing_pyqt_types_count += 1
+
try:
pyside_qualified_type = f"pyside_tested_module.{qt_type}"
eval(pyside_qualified_type)
+ is_present_in_pyside = True
except Exception as e:
print("Failed eval-in pyside qualified types")
print(f"{type(e).__name__}: {e}")
missing_type = qt_type
- missing_types_count += 1
+ missing_pyside_types_count += 1
total_missing_types_count += 1
- is_present_in_pyqt = False
- try:
- pyqt_qualified_type = f"pyqt_tested_module.{qt_type}"
- eval(pyqt_qualified_type)
+ if is_present_in_pyqt:
missing_type = f"{missing_type} (is present in PyQt6)"
missing_types_compared_to_pyqt += 1
total_missing_types_count_compared_to_pyqt += 1
- is_present_in_pyqt = True
- except Exception as e:
- print(f"{type(e).__name__}: {e}")
+ # missing in PySide
+ if not is_present_in_pyside:
if args.which_missing == "all":
missing_types.append(missing_type)
+ message = f"Missing types in PySide (all) {module_name}:"
+ # missing in PySide and present in pyqt
elif args.which_missing == "in-pyqt" and is_present_in_pyqt:
missing_types.append(missing_type)
+ message = f"Missing types in PySide6 (but present in PyQt6) {module_name}:"
+ # missing in both PyQt and PySide
elif args.which_missing == "not-in-pyqt" and not is_present_in_pyqt:
missing_types.append(missing_type)
+ message = f"Missing types in PySide6 (also missing in PyQt6) {module_name}:"
+ elif (
+ args.which_missing == "in-pyside-not-in-pyqt"
+ and not is_present_in_pyqt
+ ):
+ missing_types.append(qt_type)
+ message = f"Missing types in PyQt6 (but present in PySide6) {module_name}:"
if len(missing_types) > 0:
- wikilog(f"Missing types in {module_name}:", style="with_newline")
+ wikilog(message, style="with_newline")
missing_types.sort()
for missing_type in missing_types:
wikilog(missing_type, style="code")
wikilog("")
+ if args.which_missing != "in-pyside-not-in-pyqt":
+ missing_types_count = missing_pyside_types_count
+ else:
+ missing_types_count = missing_pyqt_types_count
+
+ if args.plot:
+ total_pyside_types = total_qt_types - missing_pyside_types_count
+ total_pyqt_types = total_qt_types - missing_pyqt_types_count
+ data["module"].append(module_name)
+ data["qt"].append(total_qt_types)
+ data["pyside"].append(total_pyside_types)
+ data["pyqt"].append(total_pyqt_types)
+
wikilog(f"Number of missing types: {missing_types_count}", style="bold_colon")
- if len(missing_types) > 0:
+ if len(missing_types) > 0 and args.which_missing != "in-pyside-not-in-pyqt":
wikilog(
"Number of missing types that are present in PyQt6: "
f"{missing_types_compared_to_pyqt}",
@@ -265,12 +313,37 @@ if __name__ == "__main__":
else:
wikilog("", style="end")
+ if args.plot:
+ df = pd.DataFrame(data=data, columns=["module", "qt", "pyside", "pyqt"])
+ df.set_index("module", inplace=True)
+ df.plot(kind="bar", title="Qt API Coverage plot")
+ plt.legend()
+ plt.xticks(rotation=45)
+ plt.ylabel("Types Count")
+ figure = plt.gcf()
+ figure.set_size_inches(32, 18) # set to full_screen
+ plt.savefig("missing_bindings_comparison_plot.png", bbox_inches='tight')
+ print(f"Plot saved in {Path.cwd() / 'missing_bindings_comparison_plot.png'}\n")
+
wikilog("Summary", style="heading5")
- wikilog(f"Total number of missing types: {total_missing_types_count}", style="bold_colon")
- wikilog(
- "Total number of missing types that are present in PyQt6: "
- f"{total_missing_types_count_compared_to_pyqt}",
- style="bold_colon",
- )
- wikilog(f"Total number of missing modules: {total_missing_modules_count}", style="bold_colon")
+
+ if args.which_missing != "in-pyside-not-in-pyqt":
+ wikilog(f"Total number of missing types: {total_missing_types_count}", style="bold_colon")
+ wikilog(
+ "Total number of missing types that are present in PyQt6: "
+ f"{total_missing_types_count_compared_to_pyqt}",
+ style="bold_colon",
+ )
+ wikilog(
+ f"Total number of missing modules: {total_missing_modules_count}", style="bold_colon"
+ )
+ else:
+ wikilog(
+ f"Total number of missing types in PyQt6: {total_missing_pyqt_types_count}",
+ style="bold_colon",
+ )
+ wikilog(
+ f"Total number of missing modules in PyQt6: {total_missing_pyqt_modules_count}",
+ style="bold_colon",
+ )
wiki_file.close()
diff --git a/tools/missing_bindings/requirements.txt b/tools/missing_bindings/requirements.txt
index f715bea38..08aa0a024 100644
--- a/tools/missing_bindings/requirements.txt
+++ b/tools/missing_bindings/requirements.txt
@@ -1,4 +1,6 @@
beautifulsoup4
+pandas
+matplotlib
# PySide
PySide6
diff --git a/tools/scanqtclasses.py b/tools/scanqtclasses.py
new file mode 100644
index 000000000..0f87d80bd
--- /dev/null
+++ b/tools/scanqtclasses.py
@@ -0,0 +1,122 @@
+# Copyright (C) 2024 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 pathlib import Path
+import os
+import re
+import subprocess
+import sys
+
+"""Scan the Qt C++ headers per module for classes that should be present
+ in the matching type system and print the missing classes."""
+
+
+VALUE_TYPE = re.compile(r'^\s*<value-type name="([^"]+)"')
+
+
+OBJECT_TYPE = re.compile(r'^\s*<object-type name="([^"]+)"')
+
+
+def query_qtpaths(keyword):
+ """Query qtpaths for a keyword."""
+ query_cmd = ["qtpaths", "-query", keyword]
+ output = subprocess.check_output(query_cmd, stderr=subprocess.STDOUT,
+ universal_newlines=True)
+ return output.strip()
+
+
+def is_class_exluded(name):
+ """Check for excluded classes that do not make sense in a typesystem."""
+ if len(name) < 2:
+ return True
+ if "Iterator" in name or "iterator" in name:
+ return True
+ if name.startswith("If") or name.startswith("Is") or name.startswith("When"):
+ return True
+ if name[:1].islower():
+ return True
+ if name.startswith("QOpenGLFunctions") and name.endswith("Backend"):
+ return True
+ return False
+
+
+def class_from_header_line(line):
+ """Extract a class name from a C++ header line."""
+ def _is_macro(token):
+ return "EXPORT" in token or "API" in token
+
+ def _fix_class_name(name):
+ pos = name.find('<') # Some template specialization "class Name<TemplateParam>"
+ if pos > 0:
+ name = name[:pos]
+ if name.endswith(':'):
+ name = name[:-1]
+ return name
+
+ if line.startswith('//') or line.endswith(';'): # comment/forward decl
+ return None
+ line = line.strip()
+ if not line.startswith("class ") and not line.startswith("struct "):
+ return None
+ tokens = line.split()
+ pos = 1
+ while pos < len(tokens) and _is_macro(tokens[pos]):
+ pos += 1
+ return _fix_class_name(tokens[pos]) if pos < len(tokens) else None
+
+
+def classes_from_header(header):
+ """Extract classes from C++ header file."""
+ result = []
+ for line in header.read_text("utf-8").splitlines():
+ name = class_from_header_line(line)
+ if name and not is_class_exluded(name):
+ result.append(name)
+ return sorted(result)
+
+
+def classes_from_typesystem(typesystem):
+ """Extract classes from typesystem XML file."""
+ result = []
+ for line in typesystem.read_text("utf-8").splitlines():
+ match = VALUE_TYPE.search(line) or OBJECT_TYPE.search(line)
+ if match:
+ result.append(match.group(1))
+ return sorted(result)
+
+
+def check_classes(qt_module_inc_dir, pyside_dir):
+ """Check classes of a module."""
+ module_name = qt_module_inc_dir.name
+ sys.stderr.write(f"Checking {module_name} ")
+ cpp_classes = []
+ typesystem_classes = []
+ for header in qt_module_inc_dir.glob("q*.h"):
+ if not header.name.endswith("_p.h"):
+ cpp_classes.extend(classes_from_header(header))
+ for typesystem in pyside_dir.glob("*.xml"):
+ typesystem_classes.extend(classes_from_typesystem(typesystem))
+
+ cpp_count = len(cpp_classes)
+ typesystem_count = len(typesystem_classes)
+ sys.stderr.write(f"found {cpp_count} C++ / {typesystem_count} typesystem classes")
+ if cpp_count <= typesystem_count:
+ sys.stderr.write(" ok\n")
+ else:
+ sys.stderr.write(f", {cpp_count-typesystem_count} missing\n")
+ for cpp_class in cpp_classes:
+ if cpp_class not in typesystem_classes:
+ wrapper_name = cpp_class.lower() + "_wrapper.cpp"
+ print(f"{module_name}:{cpp_class}:{wrapper_name}")
+
+
+if __name__ == '__main__':
+ qt_version = query_qtpaths("QT_VERSION")
+ qt_inc_dir = Path(query_qtpaths("QT_INSTALL_HEADERS"))
+ print(f"Qt {qt_version} at {os.fspath(qt_inc_dir.parent)}", file=sys.stderr)
+
+ dir = Path(__file__).parents[1].resolve()
+ for module_dir in (dir / "sources" / "pyside6" / "PySide6").glob("Qt*"):
+ qt_module_inc_dir = qt_inc_dir / module_dir.name
+ if qt_module_inc_dir.is_dir():
+ check_classes(qt_module_inc_dir, module_dir)
diff --git a/tools/snippets_translate/converter.py b/tools/snippets_translate/converter.py
index 93aab199f..d45bf277f 100644
--- a/tools/snippets_translate/converter.py
+++ b/tools/snippets_translate/converter.py
@@ -36,13 +36,24 @@ RETURN_TYPE_PATTERN = re.compile(r"^[a-zA-Z0-9]+(<.*?>)? [\w]+::[\w\*\&]+\(.*\)$
FUNCTION_PATTERN = re.compile(r"^[a-zA-Z0-9]+(<.*?>)? [\w\*\&]+\(.*\)$")
ITERATOR_PATTERN = re.compile(r"(std::)?[\w]+<[\w]+>::(const_)?iterator")
SCOPE_PATTERN = re.compile(r"[\w]+::")
+SWITCH_PATTERN = re.compile(r"^\s*switch\s*\(([a-zA-Z0-9_\.]+)\)\s*{.*$")
+CASE_PATTERN = re.compile(r"^(\s*)case\s+([a-zA-Z0-9_:\.]+):.*$")
+DEFAULT_PATTERN = re.compile(r"^(\s*)default:.*$")
QUALIFIERS = {"public:", "protected:", "private:", "public slots:",
"protected slots:", "private slots:", "signals:"}
+FUNCTION_QUALIFIERS = ["virtual ", " override", "inline ", " noexcept"]
+
+
+switch_var = None
+switch_branch = 0
+
+
def snippet_translate(x):
+ global switch_var, switch_branch
## Cases which are not C++
## TODO: Maybe expand this with lines that doesn't need to be translated
@@ -52,7 +63,8 @@ def snippet_translate(x):
## General Rules
# Remove ';' at the end of the lines
- if x.endswith(";"):
+ has_semicolon = x.endswith(";")
+ if has_semicolon:
x = x[:-1]
# Remove lines with only '{' or '}'
@@ -136,6 +148,25 @@ def snippet_translate(x):
if "throw" in x:
x = handle_keywords(x, "throw", "raise")
+ switch_match = SWITCH_PATTERN.match(x)
+ if switch_match:
+ switch_var = switch_match.group(1)
+ switch_branch = 0
+ return ""
+
+ switch_match = CASE_PATTERN.match(x)
+ if switch_match:
+ indent = switch_match.group(1)
+ value = switch_match.group(2).replace("::", ".")
+ cond = "if" if switch_branch == 0 else "elif"
+ switch_branch += 1
+ return f"{indent}{cond} {switch_var} == {value}:"
+
+ switch_match = DEFAULT_PATTERN.match(x)
+ if switch_match:
+ indent = switch_match.group(1)
+ return f"{indent}else:"
+
# handle 'void Class::method(...)' and 'void method(...)'
if VOID_METHOD_PATTERN.search(x):
x = handle_void_functions(x)
@@ -233,7 +264,8 @@ def snippet_translate(x):
# At the end we skip methods with the form:
# QStringView Message::body()
# to threat them as methods.
- if (VAR1_PATTERN.search(xs)
+ if (has_semicolon and VAR1_PATTERN.search(xs)
+ and not ([f for f in FUNCTION_QUALIFIERS if f in x])
and xs.split()[0] not in ("def", "return", "and", "or")
and not VAR2_PATTERN.search(xs)
and ("{" not in x and "}" not in x)):
@@ -264,7 +296,10 @@ def snippet_translate(x):
# so we need to add '()' at the end if it's just a word
# with only alpha numeric content
if VAR4_PATTERN.search(xs) and not xs.endswith(")"):
- x = f"{x.rstrip()}()"
+ v = x.rstrip()
+ if (not v.endswith(" True") and not v.endswith(" False")
+ and not v.endswith(" None")):
+ x = f"{v}()"
return dstrip(x)
# For constructors, that we now the shape is:
diff --git a/tools/snippets_translate/handlers.py b/tools/snippets_translate/handlers.py
index 1af97ff64..34e969a62 100644
--- a/tools/snippets_translate/handlers.py
+++ b/tools/snippets_translate/handlers.py
@@ -30,8 +30,13 @@ ARRAY_DECLARATION_PATTERN = re.compile(r"^[a-zA-Z0-9\<\>]+ ([\w\*]+) *\[?\]?")
RETURN_TYPE_PATTERN = re.compile(r"^ *[a-zA-Z0-9]+ [\w]+::([\w\*\&]+\(.*\)$)")
CAPTURE_PATTERN = re.compile(r"^ *([a-zA-Z0-9]+) ([\w\*\&]+\(.*\)$)")
USELESS_QT_CLASSES_PATTERNS = [
- re.compile(r"QLatin1String\((.*)\)"),
- re.compile(r"QLatin1Char\((.*)\)")
+ re.compile(r'QLatin1StringView\(("[^"]*")\)'),
+ re.compile(r'QLatin1String\(("[^"]*")\)'),
+ re.compile(r'QString\.fromLatin1\(("[^"]*")\)'),
+ re.compile(r"QLatin1Char\(('[^']*')\)"),
+ re.compile(r'QStringLiteral\(("[^"]*")\)'),
+ re.compile(r'QString\.fromUtf8\(("[^"]*")\)'),
+ re.compile(r'u("[^"]*")_s')
]
COMMENT1_PATTERN = re.compile(r" *# *[\w\ ]+$")
COMMENT2_PATTERN = re.compile(r" *# *(.*)$")
@@ -510,10 +515,13 @@ def handle_functions(x):
def handle_useless_qt_classes(x):
for c in USELESS_QT_CLASSES_PATTERNS:
- content = c.search(x)
- if content:
- x = x.replace(content.group(0), content.group(1))
- return x
+ while True:
+ match = c.search(x)
+ if match:
+ x = x[0:match.start()] + match.group(1) + x[match.end():]
+ else:
+ break
+ return x.replace('"_L1', '"').replace("u'", "'")
def handle_new(x):
@@ -547,13 +555,19 @@ def handle_new(x):
INSTANCE_PMF_RE = re.compile(r"&?(\w+),\s*&\w+::(\w+)")
-CONNECT_RE = re.compile(r"^(\s*)(QObject::)?connect\((\w+\.\w+),\s*")
+CONNECT_RE = re.compile(r"^(\s*)(QObject::)?connect\(([A-Za-z0-9_\.]+),\s*")
-def handle_qt_connects(line):
- if not INSTANCE_PMF_RE.search(line):
+def handle_qt_connects(line_in):
+ if not INSTANCE_PMF_RE.search(line_in):
return None
# 1st pass, "fontButton, &QAbstractButton::clicked" -> "fontButton.clicked"
+
+ is_connect = "connect(" in line_in
+ line = line_in
+ # Remove any smart pointer access, etc in connect statements
+ if is_connect:
+ line = line.replace(".get()", "").replace(".data()", "").replace("->", ".")
last_pos = 0
result = ""
for match in INSTANCE_PMF_RE.finditer(line):
@@ -567,6 +581,9 @@ def handle_qt_connects(line):
result += f"{instance}.{member_fun}"
result += line[last_pos:]
+ if not is_connect:
+ return result
+
# 2nd pass, reorder connect.
connect_match = CONNECT_RE.match(result)
if not connect_match:
diff --git a/tools/snippets_translate/main.py b/tools/snippets_translate/main.py
index 12b6e54f1..01ea06c5e 100644
--- a/tools/snippets_translate/main.py
+++ b/tools/snippets_translate/main.py
@@ -213,7 +213,9 @@ def get_snippet_override(start_id: str, rel_path: str) -> List[str]:
return overriden_snippet_lines(lines, start_id)
-def _get_snippets(lines: List[str], pattern: re.Pattern) -> Dict[str, List[str]]:
+def _get_snippets(lines: List[str],
+ comment: str,
+ pattern: re.Pattern) -> Dict[str, List[str]]:
"""Helper to extract (potentially overlapping) snippets from a C++ file
indicated by pattern ("//! [1]") and return them as a dict by <id>."""
snippets: Dict[str, List[str]] = {}
@@ -231,8 +233,12 @@ def _get_snippets(lines: List[str], pattern: re.Pattern) -> Dict[str, List[str]]
start_id = start_ids.pop(0)
if start_id in done_snippets:
continue
+
+ # Reconstruct a single ID line to avoid repetitive ID lines
+ # by consecutive snippets with multi-ID lines like "//! [1] [2]"
+ id_line = f"{comment}! [{start_id}]"
done_snippets.append(start_id)
- snippet = [line] # The snippet starts with this id
+ snippet = [id_line] # The snippet starts with this id
# Find the end of the snippet
j = i
@@ -246,6 +252,7 @@ def _get_snippets(lines: List[str], pattern: re.Pattern) -> Dict[str, List[str]]
# Check if the snippet is complete
if start_id in get_snippet_ids(l, pattern):
# End of snippet
+ snippet[len(snippet) - 1] = id_line
snippets[start_id] = snippet
break
@@ -260,7 +267,7 @@ def get_python_example_snippet_override(start_id: str, rel_path: str) -> List[st
return []
path, id = value
file_lines = path.read_text().splitlines()
- snippet_dict = _get_snippets(file_lines, PYTHON_SNIPPET_PATTERN)
+ snippet_dict = _get_snippets(file_lines, '#', PYTHON_SNIPPET_PATTERN)
lines = snippet_dict.get(id)
if not lines:
raise RuntimeError(f'Snippet "{id}" not found in "{os.fspath(path)}"')
@@ -271,7 +278,7 @@ def get_python_example_snippet_override(start_id: str, rel_path: str) -> List[st
def get_snippets(lines: List[str], rel_path: str) -> List[List[str]]:
"""Extract (potentially overlapping) snippets from a C++ file indicated
by '//! [1]'."""
- result = _get_snippets(lines, CPP_SNIPPET_PATTERN)
+ result = _get_snippets(lines, '//', CPP_SNIPPET_PATTERN)
id_list = result.keys()
for snippet_id in id_list:
# Check file overrides and example overrides
@@ -371,6 +378,7 @@ def translate_file(file_path, final_path, qt_path, debug, write):
target_file.parent.mkdir(parents=True, exist_ok=True)
with target_file.open("w", encoding="utf-8") as out_f:
+ out_f.write("//! [AUTO]\n\n")
out_f.write(license_header)
out_f.write("\n\n")
diff --git a/tools/snippets_translate/module_classes.py b/tools/snippets_translate/module_classes.py
index 4992e170b..df4c7557c 100644
--- a/tools/snippets_translate/module_classes.py
+++ b/tools/snippets_translate/module_classes.py
@@ -532,6 +532,7 @@ module_classes = {
"QAccessibleEvent",
"QAccessibleInterface",
"QAccessibleObject",
+ "QAccessibleSelectionInterface",
"QAccessibleStateChangeEvent",
"QAccessibleTableCellInterface",
"QAccessibleTableModelChangeEvent",
diff --git a/tools/snippets_translate/tests/test_converter.py b/tools/snippets_translate/tests/test_converter.py
index 4cf614d1e..084cc8a6d 100644
--- a/tools/snippets_translate/tests/test_converter.py
+++ b/tools/snippets_translate/tests/test_converter.py
@@ -4,6 +4,11 @@
from converter import snippet_translate as st
+def multi_st(lines):
+ result = [st(l) for l in lines.split("\n")]
+ return "\n".join(result)
+
+
def test_comments():
assert st("// This is a comment") == "# This is a comment"
assert st("// double slash // inside") == "# double slash // inside"
@@ -112,6 +117,21 @@ def test_double_colon():
assert st("this, &MyClass::slotError);") == "self.slotError)"
+def test_connects():
+ assert (
+ st("connect(button, &QPushButton::clicked, this, &MyClass::slotClicked);")
+ == "button.clicked.connect(self.slotClicked)"
+ )
+ assert (
+ st("connect(m_ui->button, &QPushButton::clicked, this, &MyClass::slotClicked);")
+ == "m_ui.button.clicked.connect(self.slotClicked)"
+ )
+ assert (
+ st("connect(button.get(), &QPushButton::clicked, this, &MyClass::slotClicked);")
+ == "button.clicked.connect(self.slotClicked)"
+ )
+
+
def test_cout_endl():
assert st("cout << 'hello' << 'world' << endl") == "print('hello', 'world')"
assert st(" cout << 'hallo' << 'welt' << endl") == " print('hallo', 'welt')"
@@ -146,19 +166,25 @@ def test_cout_endl():
def test_variable_declaration():
assert st("QLabel label;") == "label = QLabel()"
- assert st('QLabel label("Hello")') == 'label = QLabel("Hello")'
+ assert st('QLabel label("Hello");') == 'label = QLabel("Hello")'
assert st("Widget w;") == "w = Widget()"
assert st('QLabel *label = new QLabel("Hello");') == 'label = QLabel("Hello")'
assert st('QLabel label = a_function("Hello");') == 'label = a_function("Hello")'
assert st('QString a = "something";') == 'a = "something"'
assert st("int var;") == "var = int()"
assert st("float v = 0.1;") == "v = 0.1"
- assert st("QSome<thing> var") == "var = QSome()"
+ assert st("QSome<thing> var;") == "var = QSome()"
assert st("QQueue<int> queue;") == "queue = QQueue()"
assert st("QVBoxLayout *layout = new QVBoxLayout;") == "layout = QVBoxLayout()"
assert st("QPointer<QLabel> label = new QLabel;") == "label = QLabel()"
assert st("QMatrix4x4 matrix;") == "matrix = QMatrix4x4()"
assert st("QList<QImage> collage =") == "collage ="
+ assert st("bool b = true;") == "b = True"
+ assert st("Q3DBars *m_graph = nullptr;") == "m_graph = None"
+ # Do not fall for member function definitions
+ assert st("Q3DBars *Graph::bars() const") == "Q3DBars Graph.bars()"
+ # Do not fall for member function declarations
+ assert st("virtual Q3DBars *bars();") == "virtual Q3DBars bars()"
def test_for():
@@ -368,7 +394,12 @@ def test_ternary_operator():
def test_useless_qt_classes():
assert st('result += QLatin1String("; ");') == 'result += "; "'
+ assert st('result += QString::fromLatin1("; ");') == 'result += "; "'
+ assert (
+ st('result = QStringLiteral("A") + QStringLiteral("B");')
+ == 'result = "A" + "B"')
assert st("<< QLatin1Char('\0') << endl;") == "print('\0')"
+ assert st('result = u"A"_s;') == 'result = "A"'
def test_special_cases():
@@ -416,6 +447,35 @@ def test_lambdas():
pass
+def test_switch_case():
+ source = """switch (v) {
+case 1:
+ f1();
+ break;
+case ClassName::EnumValue:
+ f2();
+ break;
+default:
+ f3();
+ break;
+}
+"""
+ expected = """
+if v == 1:
+ f1()
+ break
+elif v == ClassName.EnumValue:
+ f2()
+ break
+else:
+ f3()
+ break
+
+"""
+
+ assert multi_st(source) == expected
+
+
def test_std_function():
# std::function<QImage(const QImage &)> scale = [](const QImage &img) {
pass
diff --git a/tools/snippets_translate/tests/test_snippets.py b/tools/snippets_translate/tests/test_snippets.py
index 3c29fcbf5..84897d815 100644
--- a/tools/snippets_translate/tests/test_snippets.py
+++ b/tools/snippets_translate/tests/test_snippets.py
@@ -4,6 +4,9 @@
from main import _get_snippets, get_snippet_ids, CPP_SNIPPET_PATTERN
+C_COMMENT = "//"
+
+
def test_stacking():
lines = [
"//! [A] //! [B] ",
@@ -12,7 +15,7 @@ def test_stacking():
"//! [C] //! [A] ",
"//! [B] //! [D] //! [E]",
]
- snippets = _get_snippets(lines, CPP_SNIPPET_PATTERN)
+ snippets = _get_snippets(lines, C_COMMENT, CPP_SNIPPET_PATTERN)
assert len(snippets) == 5
snippet_a = snippets["A"]
@@ -41,7 +44,7 @@ def test_nesting():
"//! [C]",
"//! [B]",
]
- snippets = _get_snippets(lines, CPP_SNIPPET_PATTERN)
+ snippets = _get_snippets(lines, C_COMMENT, CPP_SNIPPET_PATTERN)
assert len(snippets) == 3
snippet_a = snippets["A"]
@@ -58,24 +61,27 @@ def test_nesting():
def test_overlapping():
+ a_id = "//! [A]"
+ b_id = "//! [B]"
lines = [
"pretext",
- "//! [A]",
+ a_id,
"l1",
"//! [C]",
"//! [A] //! [B]",
"l2",
"l3 // Comment",
- "//! [B]",
+ b_id,
"posttext",
"//! [C]",
]
- snippets = _get_snippets(lines, CPP_SNIPPET_PATTERN)
+ snippets = _get_snippets(lines, C_COMMENT, CPP_SNIPPET_PATTERN)
assert len(snippets) == 3
+ # Simple snippet ID lines are generated
snippet_a = snippets["A"]
assert len(snippet_a) == 4
- assert snippet_a == lines[1:5]
+ assert snippet_a == lines[1:4] + [a_id]
snippet_c = snippets["C"]
assert len(snippet_c) == 7
@@ -83,31 +89,35 @@ def test_overlapping():
snippet_b = snippets["B"]
assert len(snippet_b) == 4
- assert snippet_b == lines[4:8]
+ assert snippet_b == [b_id] + lines[5:8]
def test_snippets():
+ a_id = "//! [A]"
+ b_id = "//! [B]"
+
lines = [
"pretext",
- "//! [A]",
+ a_id,
"l1",
"//! [A] //! [B]",
"l2",
"l3 // Comment",
- "//! [B]",
+ b_id,
"posttext"
]
- snippets = _get_snippets(lines, CPP_SNIPPET_PATTERN)
+ snippets = _get_snippets(lines, C_COMMENT, CPP_SNIPPET_PATTERN)
assert len(snippets) == 2
snippet_a = snippets["A"]
+
assert len(snippet_a) == 3
- assert snippet_a == lines[1:4]
+ assert snippet_a == lines[1:3] + [a_id]
snippet_b = snippets["B"]
assert len(snippet_b) == 4
- assert snippet_b == lines[3:7]
+ assert snippet_b == [b_id] + lines[4:7]
def test_snippet_ids():