First upload, 18 controller version
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
218
.venv_nopip/Lib/site-packages/PySide6/scripts/deploy.py
Normal file
218
.venv_nopip/Lib/site-packages/PySide6/scripts/deploy.py
Normal file
@@ -0,0 +1,218 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
""" pyside6-deploy deployment tool
|
||||
|
||||
Deployment tool that uses Nuitka to deploy PySide6 applications to various desktop (Windows,
|
||||
Linux, macOS) platforms.
|
||||
|
||||
How does it work?
|
||||
|
||||
Command: pyside6-deploy path/to/main_file
|
||||
pyside6-deploy (incase main file is called main.py)
|
||||
pyside6-deploy -c /path/to/config_file
|
||||
|
||||
Platforms supported: Linux, Windows, macOS
|
||||
Module binary inclusion:
|
||||
1. for non-QML cases, only required modules are included
|
||||
2. for QML cases, all modules are included because of all QML plugins getting included
|
||||
with nuitka
|
||||
|
||||
Config file:
|
||||
On the first run of the tool, it creates a config file called pysidedeploy.spec which
|
||||
controls the various characteristic of the deployment. Users can simply change the value
|
||||
in this config file to achieve different properties ie. change the application name,
|
||||
deployment platform etc.
|
||||
|
||||
Note: This file is used by both pyside6-deploy and pyside6-android-deploy
|
||||
"""
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
from deploy_lib import (MAJOR_VERSION, DesktopConfig, cleanup, config_option_exists,
|
||||
finalize, create_config_file, PythonExecutable, Nuitka,
|
||||
HELP_EXTRA_MODULES, HELP_EXTRA_IGNORE_DIRS)
|
||||
|
||||
|
||||
TOOL_DESCRIPTION = dedent(f"""
|
||||
This tool deploys PySide{MAJOR_VERSION} to desktop (Windows, Linux,
|
||||
macOS) platforms. The following types of executables are produced as per
|
||||
the platform:
|
||||
|
||||
Windows = .exe
|
||||
macOS = .app
|
||||
Linux = .bin
|
||||
""")
|
||||
|
||||
HELP_MODE = dedent("""
|
||||
The mode in which the application is deployed. The options are: onefile,
|
||||
standalone. The default value is onefile.
|
||||
|
||||
This options translates to the mode Nuitka uses to create the executable.
|
||||
|
||||
macOS by default uses the --standalone option.
|
||||
""")
|
||||
|
||||
|
||||
def main(main_file: Path = None, name: str = None, config_file: Path = None, init: bool = False,
|
||||
loglevel=logging.WARNING, dry_run: bool = False, keep_deployment_files: bool = False,
|
||||
force: bool = False, extra_ignore_dirs: str = None, extra_modules_grouped: str = None,
|
||||
mode: str = None) -> str | None:
|
||||
"""
|
||||
Entry point for pyside6-deploy command.
|
||||
|
||||
:return: If successful, the Nuitka command that was executed. None otherwise.
|
||||
"""
|
||||
|
||||
logging.basicConfig(level=loglevel)
|
||||
|
||||
# In case pyside6-deploy is run from a completely different location than the project directory
|
||||
if main_file and main_file.exists():
|
||||
config_file = main_file.parent / "pysidedeploy.spec"
|
||||
|
||||
if config_file and not config_file.exists() and not main_file.exists():
|
||||
raise RuntimeError(dedent("""
|
||||
Directory does not contain main.py file.
|
||||
Please specify the main Python entry point file or the pysidedeploy.spec config file.
|
||||
Run "pyside6-deploy --help" to see info about CLI options.
|
||||
|
||||
pyside6-deploy exiting..."""))
|
||||
|
||||
logging.info("[DEPLOY] Start")
|
||||
|
||||
if extra_ignore_dirs:
|
||||
extra_ignore_dirs = extra_ignore_dirs.split(",")
|
||||
|
||||
extra_modules = []
|
||||
if extra_modules_grouped:
|
||||
tmp_extra_modules = extra_modules_grouped.split(",")
|
||||
for extra_module in tmp_extra_modules:
|
||||
if extra_module.startswith("Qt"):
|
||||
extra_modules.append(extra_module[2:])
|
||||
else:
|
||||
extra_modules.append(extra_module)
|
||||
|
||||
python = PythonExecutable(dry_run=dry_run, init=init, force=force)
|
||||
config_file_exists = config_file and config_file.exists()
|
||||
|
||||
if config_file_exists:
|
||||
logging.info(f"[DEPLOY] Using existing config file {config_file}")
|
||||
else:
|
||||
config_file = create_config_file(main_file=main_file, dry_run=dry_run)
|
||||
|
||||
config = DesktopConfig(config_file=config_file, source_file=main_file, python_exe=python.exe,
|
||||
dry_run=dry_run, existing_config_file=config_file_exists,
|
||||
extra_ignore_dirs=extra_ignore_dirs, mode=mode, name=name)
|
||||
|
||||
cleanup(config=config)
|
||||
|
||||
python.install_dependencies(config=config, packages="packages")
|
||||
|
||||
# required by Nuitka for pyenv Python
|
||||
add_arg = " --static-libpython=no"
|
||||
if python.is_pyenv_python() and add_arg not in config.extra_args:
|
||||
config.extra_args += add_arg
|
||||
|
||||
config.modules += list(set(extra_modules).difference(set(config.modules)))
|
||||
|
||||
# Do not save the config changes if --dry-run is specified
|
||||
if not dry_run:
|
||||
config.update_config()
|
||||
|
||||
if config.qml_files:
|
||||
logging.info("[DEPLOY] Included QML files: "
|
||||
f"{[str(qml_file) for qml_file in config.qml_files]}")
|
||||
|
||||
if init:
|
||||
# Config file created above. Exiting.
|
||||
logging.info(f"[DEPLOY]: Config file {config.config_file} created")
|
||||
return
|
||||
|
||||
# If modules contain QtSql and the platform is macOS, then pyside6-deploy will not work
|
||||
# currently. The fix ideally will have to come from Nuitka.
|
||||
# See PYSIDE-2835
|
||||
# TODO: Remove this check once the issue is fixed in Nuitka
|
||||
# Nuitka Issue: https://github.com/Nuitka/Nuitka/issues/3079
|
||||
if "Sql" in config.modules and sys.platform == "darwin":
|
||||
print("[DEPLOY] QtSql Application is not supported on macOS with pyside6-deploy")
|
||||
return
|
||||
|
||||
command_str = None
|
||||
try:
|
||||
# Run the Nuitka command to create the executable
|
||||
if not dry_run:
|
||||
logging.info("[DEPLOY] Deploying application")
|
||||
|
||||
nuitka = Nuitka(nuitka=[python.exe, "-m", "nuitka"])
|
||||
command_str = nuitka.create_executable(source_file=config.source_file,
|
||||
extra_args=config.extra_args,
|
||||
qml_files=config.qml_files,
|
||||
qt_plugins=config.qt_plugins,
|
||||
excluded_qml_plugins=config.excluded_qml_plugins,
|
||||
icon=config.icon,
|
||||
dry_run=dry_run,
|
||||
permissions=config.permissions,
|
||||
mode=config.mode)
|
||||
if not dry_run:
|
||||
logging.info("[DEPLOY] Successfully deployed application")
|
||||
except Exception:
|
||||
print(f"[DEPLOY] Exception occurred: {traceback.format_exc()}")
|
||||
finally:
|
||||
if config.generated_files_path:
|
||||
if not dry_run:
|
||||
finalize(config=config)
|
||||
if not keep_deployment_files:
|
||||
cleanup(config=config)
|
||||
|
||||
logging.info("[DEPLOY] End")
|
||||
return command_str
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=TOOL_DESCRIPTION)
|
||||
|
||||
parser.add_argument("-c", "--config-file", type=lambda p: Path(p).absolute(),
|
||||
default=(Path.cwd() / "pysidedeploy.spec"),
|
||||
help="Path to the .spec config file")
|
||||
|
||||
parser.add_argument(
|
||||
type=lambda p: Path(p).absolute(),
|
||||
help="Path to main python file", nargs="?", dest="main_file",
|
||||
default=None if config_option_exists() else Path.cwd() / "main.py")
|
||||
|
||||
parser.add_argument(
|
||||
"--init", action="store_true",
|
||||
help="Create pysidedeploy.spec file, if it doesn't already exists")
|
||||
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", help="Run in verbose mode", action="store_const",
|
||||
dest="loglevel", const=logging.INFO)
|
||||
|
||||
parser.add_argument("--dry-run", action="store_true", help="Show the commands to be run")
|
||||
|
||||
parser.add_argument(
|
||||
"--keep-deployment-files", action="store_true",
|
||||
help="Keep the generated deployment files generated")
|
||||
|
||||
parser.add_argument("-f", "--force", action="store_true", help="Force all input prompts")
|
||||
|
||||
parser.add_argument("--name", type=str, help="Application name")
|
||||
|
||||
parser.add_argument("--extra-ignore-dirs", type=str, help=HELP_EXTRA_IGNORE_DIRS)
|
||||
|
||||
parser.add_argument("--extra-modules", type=str, help=HELP_EXTRA_MODULES)
|
||||
|
||||
parser.add_argument("--mode", choices=["onefile", "standalone"], default="onefile",
|
||||
help=HELP_MODE)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.main_file, args.name, args.config_file, args.init, args.loglevel, args.dry_run,
|
||||
args.keep_deployment_files, args.force, args.extra_ignore_dirs, args.extra_modules,
|
||||
args.mode)
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
MAJOR_VERSION = 6
|
||||
|
||||
if sys.platform == "win32":
|
||||
IMAGE_FORMAT = ".ico"
|
||||
EXE_FORMAT = ".exe"
|
||||
elif sys.platform == "darwin":
|
||||
IMAGE_FORMAT = ".icns"
|
||||
EXE_FORMAT = ".app"
|
||||
else:
|
||||
IMAGE_FORMAT = ".jpg"
|
||||
EXE_FORMAT = ".bin"
|
||||
|
||||
DEFAULT_APP_ICON = str((Path(__file__).parent / f"pyside_icon{IMAGE_FORMAT}").resolve())
|
||||
DEFAULT_IGNORE_DIRS = {"site-packages", "deployment", ".git", ".qtcreator", "build", "dist",
|
||||
"tests", "doc", "docs", "examples", ".vscode", "__pycache__"}
|
||||
|
||||
IMPORT_WARNING_PYSIDE = (f"[DEPLOY] Found 'import PySide6' in file {0}"
|
||||
". Use 'from PySide6 import <module>' or pass the module"
|
||||
" needed using --extra-modules command line argument")
|
||||
HELP_EXTRA_IGNORE_DIRS = dedent("""
|
||||
Comma-separated directory names inside the project dir. These
|
||||
directories will be skipped when searching for Python files
|
||||
relevant to the project.
|
||||
|
||||
Example usage: --extra-ignore-dirs=doc,translations
|
||||
""")
|
||||
|
||||
HELP_EXTRA_MODULES = dedent("""
|
||||
Comma-separated list of Qt modules to be added to the application,
|
||||
in case they are not found automatically.
|
||||
|
||||
This occurs when you have 'import PySide6' in your code instead
|
||||
'from PySide6 import <module>'. The module name is specified
|
||||
by either omitting the prefix of Qt or including it.
|
||||
|
||||
Example usage 1: --extra-modules=Network,Svg
|
||||
Example usage 2: --extra-modules=QtNetwork,QtSvg
|
||||
""")
|
||||
|
||||
# plugins to be removed from the --include-qt-plugins option because these plugins
|
||||
# don't exist in site-package under PySide6/Qt/plugins
|
||||
PLUGINS_TO_REMOVE = ["accessiblebridge", "platforms/darwin", "networkaccess", "scenegraph"]
|
||||
|
||||
|
||||
def get_all_pyside_modules():
|
||||
"""
|
||||
Returns all the modules installed with PySide6
|
||||
"""
|
||||
import PySide6
|
||||
# They all start with `Qt` as the prefix. Removing this prefix and getting the actual
|
||||
# module name
|
||||
return [module[2:] for module in PySide6.__all__]
|
||||
|
||||
|
||||
from .commands import run_command, run_qmlimportscanner
|
||||
from .dependency_util import find_pyside_modules, find_permission_categories, QtDependencyReader
|
||||
from .nuitka_helper import Nuitka
|
||||
from .config import BaseConfig, Config, DesktopConfig
|
||||
from .python_helper import PythonExecutable
|
||||
from .deploy_util import cleanup, finalize, create_config_file, config_option_exists
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,63 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from functools import lru_cache
|
||||
from . import DEFAULT_IGNORE_DIRS
|
||||
|
||||
|
||||
"""
|
||||
All utility functions for deployment
|
||||
"""
|
||||
|
||||
|
||||
def run_command(command, dry_run: bool, fetch_output: bool = False):
|
||||
command_str = " ".join([str(cmd) for cmd in command])
|
||||
output = None
|
||||
is_windows = (sys.platform == "win32")
|
||||
try:
|
||||
if not dry_run:
|
||||
if fetch_output:
|
||||
output = subprocess.check_output(command, shell=is_windows)
|
||||
else:
|
||||
subprocess.check_call(command, shell=is_windows)
|
||||
else:
|
||||
print(command_str + "\n")
|
||||
except FileNotFoundError as error:
|
||||
raise FileNotFoundError(f"[DEPLOY] {error.filename} not found")
|
||||
except subprocess.CalledProcessError as error:
|
||||
raise RuntimeError(
|
||||
f"[DEPLOY] Command {command_str} failed with error {error} and return_code"
|
||||
f"{error.returncode}"
|
||||
)
|
||||
except Exception as error:
|
||||
raise RuntimeError(f"[DEPLOY] Command {command_str} failed with error {error}")
|
||||
|
||||
return command_str, output
|
||||
|
||||
|
||||
@lru_cache
|
||||
def run_qmlimportscanner(project_dir: Path, dry_run: bool):
|
||||
"""
|
||||
Runs pyside6-qmlimportscanner to find all the imported qml modules in project_dir
|
||||
"""
|
||||
qml_modules = []
|
||||
cmd = ["pyside6-qmlimportscanner", "-rootPath", str(project_dir)]
|
||||
|
||||
for ignore_dir in DEFAULT_IGNORE_DIRS:
|
||||
cmd.extend(["-exclude", ignore_dir])
|
||||
|
||||
if dry_run:
|
||||
run_command(command=cmd, dry_run=True)
|
||||
|
||||
# Run qmlimportscanner during dry_run as well to complete the command being run by nuitka
|
||||
_, json_string = run_command(command=cmd, dry_run=False, fetch_output=True)
|
||||
json_string = json_string.decode("utf-8")
|
||||
json_array = json.loads(json_string)
|
||||
qml_modules = [item['name'] for item in json_array if item['type'] == "module"]
|
||||
|
||||
return qml_modules
|
||||
@@ -0,0 +1,532 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import configparser
|
||||
import logging
|
||||
import tempfile
|
||||
import warnings
|
||||
from configparser import ConfigParser
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
from project_lib import ProjectData, DesignStudioProject, resolve_valid_project_file
|
||||
from . import (DEFAULT_APP_ICON, DEFAULT_IGNORE_DIRS, find_pyside_modules,
|
||||
find_permission_categories, QtDependencyReader, run_qmlimportscanner)
|
||||
|
||||
# Some QML plugins like QtCore are excluded from this list as they don't contribute much to
|
||||
# executable size. Excluding them saves the extra processing of checking for them in files
|
||||
EXCLUDED_QML_PLUGINS = {"QtQuick", "QtQuick3D", "QtCharts", "QtWebEngine", "QtTest", "QtSensors"}
|
||||
|
||||
PERMISSION_MAP = {"Bluetooth": "NSBluetoothAlwaysUsageDescription:BluetoothAccess",
|
||||
"Camera": "NSCameraUsageDescription:CameraAccess",
|
||||
"Microphone": "NSMicrophoneUsageDescription:MicrophoneAccess",
|
||||
"Contacts": "NSContactsUsageDescription:ContactsAccess",
|
||||
"Calendar": "NSCalendarsUsageDescription:CalendarAccess",
|
||||
# for iOS NSLocationWhenInUseUsageDescription and
|
||||
# NSLocationAlwaysAndWhenInUseUsageDescription are also required.
|
||||
"Location": "NSLocationUsageDescription:LocationAccess",
|
||||
}
|
||||
|
||||
|
||||
class BaseConfig:
|
||||
"""Wrapper class around any .spec file with function to read and set values for the .spec file
|
||||
"""
|
||||
|
||||
def __init__(self, config_file: Path, comment_prefixes: str = "/",
|
||||
existing_config_file: bool = False) -> None:
|
||||
self.config_file = config_file
|
||||
self.existing_config_file = existing_config_file
|
||||
self.parser = ConfigParser(comment_prefixes=comment_prefixes, strict=False,
|
||||
allow_no_value=True)
|
||||
self.parser.read(self.config_file)
|
||||
|
||||
def update_config(self):
|
||||
logging.info(f"[DEPLOY] Updating config file {self.config_file}")
|
||||
|
||||
# This section of code is done to preserve the formatting of the original deploy.spec
|
||||
# file where there is blank line before the comments
|
||||
with tempfile.NamedTemporaryFile('w+', delete=False) as temp_file:
|
||||
self.parser.write(temp_file, space_around_delimiters=True)
|
||||
temp_file_path = temp_file.name
|
||||
|
||||
# Read the temporary file and write back to the original file with blank lines before
|
||||
# comments
|
||||
with open(temp_file_path, 'r') as temp_file, open(self.config_file, 'w') as config_file:
|
||||
previous_line = None
|
||||
for line in temp_file:
|
||||
if (line.lstrip().startswith('#') and previous_line is not None
|
||||
and not previous_line.lstrip().startswith('#')):
|
||||
config_file.write('\n')
|
||||
config_file.write(line)
|
||||
previous_line = line
|
||||
|
||||
# Clean up the temporary file
|
||||
Path(temp_file_path).unlink()
|
||||
|
||||
def set_value(self, section: str, key: str, new_value: str, raise_warning: bool = True) -> None:
|
||||
try:
|
||||
current_value = self.get_value(section, key, ignore_fail=True)
|
||||
if current_value != new_value:
|
||||
self.parser.set(section, key, new_value)
|
||||
except configparser.NoOptionError:
|
||||
if not raise_warning:
|
||||
return
|
||||
logging.warning(f"[DEPLOY] Set key '{key}': Key does not exist in section '{section}'")
|
||||
except configparser.NoSectionError:
|
||||
if not raise_warning:
|
||||
return
|
||||
logging.warning(f"[DEPLOY] Section '{section}' does not exist")
|
||||
|
||||
def get_value(self, section: str, key: str, ignore_fail: bool = False) -> str | None:
|
||||
try:
|
||||
return self.parser.get(section, key)
|
||||
except configparser.NoOptionError:
|
||||
if ignore_fail:
|
||||
return None
|
||||
logging.warning(f"[DEPLOY] Get key '{key}': Key does not exist in section {section}")
|
||||
except configparser.NoSectionError:
|
||||
if ignore_fail:
|
||||
return None
|
||||
logging.warning(f"[DEPLOY] Section '{section}': does not exist")
|
||||
|
||||
|
||||
class Config(BaseConfig):
|
||||
"""
|
||||
Wrapper class around pysidedeploy.spec file, whose options are used to control the executable
|
||||
creation
|
||||
"""
|
||||
|
||||
def __init__(self, config_file: Path, source_file: Path, python_exe: Path, dry_run: bool,
|
||||
existing_config_file: bool = False, extra_ignore_dirs: list[str] = None,
|
||||
name: str = None):
|
||||
super().__init__(config_file=config_file, existing_config_file=existing_config_file)
|
||||
|
||||
self.extra_ignore_dirs = extra_ignore_dirs
|
||||
self._dry_run = dry_run
|
||||
self.qml_modules = set()
|
||||
|
||||
self.source_file = Path(
|
||||
self.set_or_fetch(property_value=source_file, property_key="input_file")
|
||||
).resolve()
|
||||
|
||||
self.python_path = Path(
|
||||
self.set_or_fetch(
|
||||
property_value=python_exe,
|
||||
property_key="python_path",
|
||||
property_group="python",
|
||||
)
|
||||
)
|
||||
|
||||
self.title = self.set_or_fetch(property_value=name, property_key="title")
|
||||
|
||||
config_icon = self.get_value("app", "icon")
|
||||
if config_icon:
|
||||
self._icon = str(Path(config_icon).resolve())
|
||||
else:
|
||||
self.icon = DEFAULT_APP_ICON
|
||||
|
||||
proj_dir = self.get_value("app", "project_dir")
|
||||
if proj_dir:
|
||||
self._project_dir = Path(proj_dir).resolve()
|
||||
else:
|
||||
self.project_dir = self._find_project_dir()
|
||||
|
||||
exe_directory = self.get_value("app", "exec_directory")
|
||||
if exe_directory:
|
||||
self._exe_dir = Path(exe_directory).absolute()
|
||||
else:
|
||||
self.exe_dir = self._find_exe_dir()
|
||||
|
||||
self._project_file = None
|
||||
proj_file = self.get_value("app", "project_file")
|
||||
if proj_file:
|
||||
self._project_file = self.project_dir / proj_file
|
||||
else:
|
||||
proj_file = self._find_project_file()
|
||||
if proj_file:
|
||||
self.project_file = proj_file
|
||||
|
||||
self.project_data = None
|
||||
if self.project_file and self.project_file.exists():
|
||||
self.project_data = ProjectData(project_file=self.project_file)
|
||||
|
||||
self._qml_files = []
|
||||
# Design Studio projects include the qml files using Qt resources
|
||||
if source_file and not DesignStudioProject.is_ds_project(source_file):
|
||||
config_qml_files = self.get_value("qt", "qml_files")
|
||||
if config_qml_files and self.project_dir and self.existing_config_file:
|
||||
self._qml_files = [Path(self.project_dir)
|
||||
/ file for file in config_qml_files.split(",")]
|
||||
else:
|
||||
self.qml_files = self._find_qml_files()
|
||||
|
||||
self._excluded_qml_plugins = []
|
||||
excl_qml_plugins = self.get_value("qt", "excluded_qml_plugins")
|
||||
if excl_qml_plugins and self.existing_config_file:
|
||||
self._excluded_qml_plugins = excl_qml_plugins.split(",")
|
||||
else:
|
||||
self.excluded_qml_plugins = self._find_excluded_qml_plugins()
|
||||
|
||||
self._generated_files_path = self.source_file.parent / "deployment"
|
||||
|
||||
self.modules = []
|
||||
|
||||
def set_or_fetch(self, property_value, property_key, property_group="app") -> str:
|
||||
"""
|
||||
If a new property value is provided, store it in the config file
|
||||
Otherwise return the existing value in the config file.
|
||||
Raise an exception if neither are available.
|
||||
|
||||
:param property_value: The value to set if provided.
|
||||
:param property_key: The configuration key.
|
||||
:param property_group: The configuration group (default is "app").
|
||||
:return: The configuration value.
|
||||
:raises RuntimeError: If no value is provided and no existing value is found.
|
||||
"""
|
||||
existing_value = self.get_value(property_group, property_key)
|
||||
|
||||
if property_value:
|
||||
self.set_value(property_group, property_key, str(property_value))
|
||||
return property_value
|
||||
if existing_value:
|
||||
return existing_value
|
||||
|
||||
raise RuntimeError(
|
||||
f"[DEPLOY] No value for {property_key} specified in config file or as cli option"
|
||||
)
|
||||
|
||||
@property
|
||||
def dry_run(self) -> bool:
|
||||
return self._dry_run
|
||||
|
||||
@property
|
||||
def generated_files_path(self) -> Path:
|
||||
return self._generated_files_path
|
||||
|
||||
@property
|
||||
def qml_files(self) -> list[Path]:
|
||||
return self._qml_files
|
||||
|
||||
@qml_files.setter
|
||||
def qml_files(self, qml_files: list[Path]):
|
||||
self._qml_files = qml_files
|
||||
qml_files = [str(file.absolute().relative_to(self.project_dir.absolute()))
|
||||
if file.absolute().is_relative_to(self.project_dir) else str(file.absolute())
|
||||
for file in self.qml_files]
|
||||
qml_files.sort()
|
||||
self.set_value("qt", "qml_files", ",".join(qml_files))
|
||||
|
||||
@property
|
||||
def project_dir(self) -> Path:
|
||||
return self._project_dir
|
||||
|
||||
@project_dir.setter
|
||||
def project_dir(self, project_dir: Path) -> None:
|
||||
rel_path = (
|
||||
project_dir.relative_to(self.config_file.parent)
|
||||
if project_dir.is_relative_to(self.config_file.parent)
|
||||
else project_dir
|
||||
)
|
||||
self._project_dir = project_dir
|
||||
self.set_value("app", "project_dir", str(rel_path))
|
||||
|
||||
@property
|
||||
def project_file(self) -> Path:
|
||||
return self._project_file
|
||||
|
||||
@project_file.setter
|
||||
def project_file(self, project_file: Path):
|
||||
self._project_file = project_file
|
||||
self.set_value("app", "project_file", str(project_file.relative_to(self.project_dir)))
|
||||
|
||||
@property
|
||||
def title(self) -> str:
|
||||
return self._title
|
||||
|
||||
@title.setter
|
||||
def title(self, title: str):
|
||||
self._title = title
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
return self._icon
|
||||
|
||||
@icon.setter
|
||||
def icon(self, icon: str):
|
||||
self._icon = icon
|
||||
self.set_value("app", "icon", icon)
|
||||
|
||||
@property
|
||||
def source_file(self) -> Path:
|
||||
return self._source_file
|
||||
|
||||
@source_file.setter
|
||||
def source_file(self, source_file: Path) -> None:
|
||||
rel_path = (
|
||||
source_file.relative_to(self.config_file.parent)
|
||||
if source_file.is_relative_to(self.config_file.parent)
|
||||
else source_file
|
||||
)
|
||||
self._source_file = source_file
|
||||
self.set_value("app", "input_file", str(rel_path))
|
||||
|
||||
@property
|
||||
def python_path(self) -> Path:
|
||||
return self._python_path
|
||||
|
||||
@python_path.setter
|
||||
def python_path(self, python_path: Path):
|
||||
self._python_path = python_path
|
||||
|
||||
@property
|
||||
def extra_args(self) -> str:
|
||||
return self.get_value("nuitka", "extra_args")
|
||||
|
||||
@extra_args.setter
|
||||
def extra_args(self, extra_args: str):
|
||||
self.set_value("nuitka", "extra_args", extra_args)
|
||||
|
||||
@property
|
||||
def excluded_qml_plugins(self) -> list[str]:
|
||||
return self._excluded_qml_plugins
|
||||
|
||||
@excluded_qml_plugins.setter
|
||||
def excluded_qml_plugins(self, excluded_qml_plugins: list[str]):
|
||||
self._excluded_qml_plugins = excluded_qml_plugins
|
||||
if excluded_qml_plugins: # check required for Android
|
||||
excluded_qml_plugins.sort()
|
||||
self.set_value("qt", "excluded_qml_plugins", ",".join(excluded_qml_plugins))
|
||||
|
||||
@property
|
||||
def exe_dir(self) -> Path:
|
||||
return self._exe_dir
|
||||
|
||||
@exe_dir.setter
|
||||
def exe_dir(self, exe_dir: Path):
|
||||
self._exe_dir = exe_dir
|
||||
self.set_value("app", "exec_directory", str(exe_dir))
|
||||
|
||||
@property
|
||||
def modules(self) -> list[str]:
|
||||
return self._modules
|
||||
|
||||
@modules.setter
|
||||
def modules(self, modules: list[str]):
|
||||
self._modules = modules
|
||||
modules.sort()
|
||||
self.set_value("qt", "modules", ",".join(modules))
|
||||
|
||||
def _find_qml_files(self):
|
||||
"""
|
||||
Fetches all the qml_files in the folder and sets them if the
|
||||
field qml_files is empty in the config_file
|
||||
"""
|
||||
|
||||
if self.project_data:
|
||||
qml_files = [(self.project_dir / str(qml_file)) for qml_file in
|
||||
self.project_data.qml_files]
|
||||
for sub_project_file in self.project_data.sub_projects_files:
|
||||
qml_files.extend([self.project_dir / str(qml_file) for qml_file in
|
||||
ProjectData(project_file=sub_project_file).qml_files])
|
||||
else:
|
||||
# Filter out files from DEFAULT_IGNORE_DIRS
|
||||
qml_files = [
|
||||
file for file in self.project_dir.glob("**/*.qml")
|
||||
if all(part not in file.parts for part in DEFAULT_IGNORE_DIRS)
|
||||
]
|
||||
|
||||
if len(qml_files) > 500:
|
||||
warnings.warn(
|
||||
"You seem to include a lot of QML files from "
|
||||
f"{self.project_dir}. This can lead to errors in deployment."
|
||||
)
|
||||
|
||||
return qml_files
|
||||
|
||||
def _find_project_dir(self) -> Path:
|
||||
if DesignStudioProject.is_ds_project(self.source_file):
|
||||
return DesignStudioProject(self.source_file).project_dir
|
||||
|
||||
# There is no other way to find the project_dir than assume it is the parent directory
|
||||
# of source_file
|
||||
return self.source_file.parent
|
||||
|
||||
def _find_project_file(self) -> Path | None:
|
||||
if not self.source_file:
|
||||
raise RuntimeError("[DEPLOY] Source file not set in config file")
|
||||
|
||||
if DesignStudioProject.is_ds_project(self.source_file):
|
||||
pyproject_location = self.source_file.parent
|
||||
else:
|
||||
pyproject_location = self.project_dir
|
||||
|
||||
try:
|
||||
return resolve_valid_project_file(pyproject_location)
|
||||
except ValueError as e:
|
||||
logging.warning(f"[DEPLOY] Unable to resolve a valid project file. Proceeding without a"
|
||||
f" project file. Details:\n{e}.")
|
||||
return None
|
||||
|
||||
def _find_excluded_qml_plugins(self) -> list[str] | None:
|
||||
if not self.qml_files and not DesignStudioProject.is_ds_project(self.source_file):
|
||||
return None
|
||||
|
||||
self.qml_modules = set(run_qmlimportscanner(project_dir=self.project_dir,
|
||||
dry_run=self.dry_run))
|
||||
excluded_qml_plugins = EXCLUDED_QML_PLUGINS.difference(self.qml_modules)
|
||||
|
||||
# sorting needed for dry_run testing
|
||||
return sorted(excluded_qml_plugins)
|
||||
|
||||
def _find_exe_dir(self) -> Path:
|
||||
if self.project_dir == Path.cwd():
|
||||
return self.project_dir.relative_to(Path.cwd())
|
||||
|
||||
return self.project_dir
|
||||
|
||||
def _find_pysidemodules(self) -> list[str]:
|
||||
modules = find_pyside_modules(project_dir=self.project_dir,
|
||||
extra_ignore_dirs=self.extra_ignore_dirs,
|
||||
project_data=self.project_data)
|
||||
logging.info("The following PySide modules were found from the Python files of "
|
||||
f"the project {modules}")
|
||||
return modules
|
||||
|
||||
def _find_qtquick_modules(self) -> list[str]:
|
||||
"""Identify if QtQuick is used in QML files and add them as dependency
|
||||
"""
|
||||
extra_modules = []
|
||||
if not self.qml_modules and self.qml_files:
|
||||
self.qml_modules = set(run_qmlimportscanner(project_dir=self.project_dir,
|
||||
dry_run=self.dry_run))
|
||||
|
||||
if "QtQuick" in self.qml_modules:
|
||||
extra_modules.append("Quick")
|
||||
|
||||
if "QtQuick.Controls" in self.qml_modules:
|
||||
extra_modules.append("QuickControls2")
|
||||
|
||||
return extra_modules
|
||||
|
||||
|
||||
class DesktopConfig(Config):
|
||||
"""Wrapper class around pysidedeploy.spec, but specific to Desktop deployment
|
||||
"""
|
||||
|
||||
class NuitkaMode(Enum):
|
||||
ONEFILE = "onefile"
|
||||
STANDALONE = "standalone"
|
||||
|
||||
def __init__(self, config_file: Path, source_file: Path, python_exe: Path, dry_run: bool,
|
||||
existing_config_file: bool = False, extra_ignore_dirs: list[str] = None,
|
||||
mode: str = "onefile", name: str = None):
|
||||
super().__init__(config_file, source_file, python_exe, dry_run, existing_config_file,
|
||||
extra_ignore_dirs, name=name)
|
||||
self.dependency_reader = QtDependencyReader(dry_run=self.dry_run)
|
||||
modules = self.get_value("qt", "modules")
|
||||
if modules:
|
||||
self._modules = modules.split(",")
|
||||
else:
|
||||
modules = self._find_pysidemodules()
|
||||
modules += self._find_qtquick_modules()
|
||||
modules += self._find_dependent_qt_modules(modules=modules)
|
||||
# remove duplicates
|
||||
self.modules = list(set(modules))
|
||||
|
||||
self._qt_plugins = []
|
||||
if self.get_value("qt", "plugins"):
|
||||
self._qt_plugins = self.get_value("qt", "plugins").split(",")
|
||||
else:
|
||||
self.qt_plugins = self.dependency_reader.find_plugin_dependencies(self.modules,
|
||||
python_exe)
|
||||
|
||||
self._permissions = []
|
||||
if sys.platform == "darwin":
|
||||
nuitka_macos_permissions = self.get_value("nuitka", "macos.permissions")
|
||||
if nuitka_macos_permissions:
|
||||
self._permissions = nuitka_macos_permissions.split(",")
|
||||
else:
|
||||
self.permissions = self._find_permissions()
|
||||
|
||||
self._mode = self.NuitkaMode.ONEFILE
|
||||
if self.get_value("nuitka", "mode") == self.NuitkaMode.STANDALONE.value:
|
||||
self._mode = self.NuitkaMode.STANDALONE
|
||||
elif mode == self.NuitkaMode.STANDALONE.value:
|
||||
self.mode = self.NuitkaMode.STANDALONE
|
||||
|
||||
if DesignStudioProject.is_ds_project(self.source_file):
|
||||
ds_project = DesignStudioProject(self.source_file)
|
||||
if not ds_project.compiled_resources_available():
|
||||
raise RuntimeError(f"[DEPLOY] Compiled resources file not found: "
|
||||
f"{ds_project.compiled_resources_file.absolute()}. "
|
||||
f"Build the project using 'pyside6-project build' or compile "
|
||||
f"the resources manually using pyside6-rcc")
|
||||
|
||||
@property
|
||||
def qt_plugins(self) -> list[str]:
|
||||
return self._qt_plugins
|
||||
|
||||
@qt_plugins.setter
|
||||
def qt_plugins(self, qt_plugins: list[str]):
|
||||
self._qt_plugins = qt_plugins
|
||||
qt_plugins.sort()
|
||||
self.set_value("qt", "plugins", ",".join(qt_plugins))
|
||||
|
||||
@property
|
||||
def permissions(self) -> list[str]:
|
||||
return self._permissions
|
||||
|
||||
@permissions.setter
|
||||
def permissions(self, permissions: list[str]):
|
||||
self._permissions = permissions
|
||||
permissions.sort()
|
||||
self.set_value("nuitka", "macos.permissions", ",".join(permissions))
|
||||
|
||||
@property
|
||||
def mode(self) -> NuitkaMode:
|
||||
return self._mode
|
||||
|
||||
@mode.setter
|
||||
def mode(self, mode: NuitkaMode):
|
||||
self._mode = mode
|
||||
self.set_value("nuitka", "mode", mode.value)
|
||||
|
||||
def _find_dependent_qt_modules(self, modules: list[str]) -> list[str]:
|
||||
"""
|
||||
Given pysidedeploy_config.modules, find all the other dependent Qt modules.
|
||||
"""
|
||||
all_modules = set(modules)
|
||||
|
||||
if not self.dependency_reader.lib_reader:
|
||||
warnings.warn(f"[DEPLOY] Unable to find {self.dependency_reader.lib_reader_name}. This "
|
||||
f"tool helps to find the Qt module dependencies of the application. "
|
||||
f"Skipping checking for dependencies.", category=RuntimeWarning)
|
||||
return []
|
||||
|
||||
for module_name in modules:
|
||||
self.dependency_reader.find_dependencies(module=module_name, used_modules=all_modules)
|
||||
|
||||
return list(all_modules)
|
||||
|
||||
def _find_permissions(self) -> list[str]:
|
||||
"""
|
||||
Finds and sets the usage description string required for each permission requested by the
|
||||
macOS application.
|
||||
"""
|
||||
permissions = []
|
||||
perm_categories = find_permission_categories(project_dir=self.project_dir,
|
||||
extra_ignore_dirs=self.extra_ignore_dirs,
|
||||
project_data=self.project_data)
|
||||
|
||||
perm_categories_str = ",".join(perm_categories)
|
||||
logging.info(f"[DEPLOY] Usage descriptions for the {perm_categories_str} will be added to "
|
||||
"the Info.plist file of the macOS application bundle")
|
||||
|
||||
# Handling permissions
|
||||
for perm_category in perm_categories:
|
||||
if perm_category in PERMISSION_MAP:
|
||||
permissions.append(PERMISSION_MAP[perm_category])
|
||||
|
||||
return permissions
|
||||
@@ -0,0 +1,98 @@
|
||||
[app]
|
||||
|
||||
# Title of your application
|
||||
title = pyside_app_demo
|
||||
|
||||
# Project root directory. Default: The parent directory of input_file
|
||||
project_dir =
|
||||
|
||||
# Source file entry point path. Default: main.py
|
||||
input_file =
|
||||
|
||||
# Directory where the executable output is generated
|
||||
exec_directory =
|
||||
|
||||
# Path to the project file relative to project_dir
|
||||
project_file =
|
||||
|
||||
# Application icon
|
||||
icon =
|
||||
|
||||
[python]
|
||||
|
||||
# Python path
|
||||
python_path =
|
||||
|
||||
# Python packages to install
|
||||
packages = Nuitka==2.7.11
|
||||
|
||||
# Buildozer: for deploying Android application
|
||||
android_packages = buildozer==1.5.0,cython==0.29.33
|
||||
|
||||
[qt]
|
||||
|
||||
# Paths to required QML files. Comma separated
|
||||
# Normally all the QML files required by the project are added automatically
|
||||
# Design Studio projects include the QML files using Qt resources
|
||||
qml_files =
|
||||
|
||||
# Excluded qml plugin binaries
|
||||
excluded_qml_plugins =
|
||||
|
||||
# Qt modules used. Comma separated
|
||||
modules =
|
||||
|
||||
# Qt plugins used by the application. Only relevant for desktop deployment
|
||||
# For Qt plugins used in Android application see [android][plugins]
|
||||
plugins =
|
||||
|
||||
[android]
|
||||
|
||||
# Path to PySide wheel
|
||||
wheel_pyside =
|
||||
|
||||
# Path to Shiboken wheel
|
||||
wheel_shiboken =
|
||||
|
||||
# Plugins to be copied to libs folder of the packaged application. Comma separated
|
||||
plugins =
|
||||
|
||||
[nuitka]
|
||||
|
||||
# Usage description for permissions requested by the app as found in the Info.plist file
|
||||
# of the app bundle. Comma separated
|
||||
# eg: NSCameraUsageDescription:CameraAccess
|
||||
macos.permissions =
|
||||
|
||||
# Mode of using Nuitka. Accepts standalone or onefile. Default: onefile
|
||||
mode = onefile
|
||||
|
||||
# Specify any extra nuitka arguments
|
||||
# eg: extra_args = --show-modules --follow-stdlib
|
||||
extra_args = --quiet --noinclude-qt-translations
|
||||
|
||||
[buildozer]
|
||||
|
||||
# Build mode
|
||||
# Possible values: [release, debug]
|
||||
# Release creates a .aab, while debug creates a .apk
|
||||
mode = debug
|
||||
|
||||
# Path to PySide6 and shiboken6 recipe dir
|
||||
recipe_dir =
|
||||
|
||||
# Path to extra Qt Android .jar files to be loaded by the application
|
||||
jars_dir =
|
||||
|
||||
# If empty, uses default NDK path downloaded by buildozer
|
||||
ndk_path =
|
||||
|
||||
# If empty, uses default SDK path downloaded by buildozer
|
||||
sdk_path =
|
||||
|
||||
# Other libraries to be loaded at app startup. Comma separated.
|
||||
local_libs =
|
||||
|
||||
# Architecture of deployed platform
|
||||
# Possible values: ["aarch64", "armv7a", "i686", "x86_64"]
|
||||
arch =
|
||||
@@ -0,0 +1,337 @@
|
||||
# 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 __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import os
|
||||
import site
|
||||
import json
|
||||
import warnings
|
||||
import logging
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from functools import lru_cache
|
||||
|
||||
from . import IMPORT_WARNING_PYSIDE, DEFAULT_IGNORE_DIRS, run_command
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_py_files(project_dir: Path, extra_ignore_dirs: tuple[Path] = None, project_data=None):
|
||||
"""Finds and returns all the Python files in the project
|
||||
"""
|
||||
py_candidates = []
|
||||
ignore_dirs = DEFAULT_IGNORE_DIRS.copy()
|
||||
|
||||
if project_data:
|
||||
py_candidates = project_data.python_files
|
||||
ui_candidates = project_data.ui_files
|
||||
qrc_candidates = project_data.qrc_files
|
||||
|
||||
def add_uic_qrc_candidates(candidates, candidate_type):
|
||||
possible_py_candidates = []
|
||||
missing_files = []
|
||||
for file in candidates:
|
||||
py_file = file.parent / f"{candidate_type}_{file.stem}.py"
|
||||
if py_file.exists():
|
||||
possible_py_candidates.append(py_file)
|
||||
else:
|
||||
missing_files.append((str(file), str(py_file)))
|
||||
|
||||
if missing_files:
|
||||
missing_details = "\n".join(
|
||||
f"{candidate_type.upper()} file: {src} -> Missing Python file: {dst}"
|
||||
for src, dst in missing_files
|
||||
)
|
||||
warnings.warn(
|
||||
f"[DEPLOY] The following {candidate_type} files do not have corresponding "
|
||||
f"Python files:\n {missing_details}",
|
||||
category=RuntimeWarning
|
||||
)
|
||||
|
||||
py_candidates.extend(possible_py_candidates)
|
||||
|
||||
if ui_candidates:
|
||||
add_uic_qrc_candidates(ui_candidates, "ui")
|
||||
|
||||
if qrc_candidates:
|
||||
add_uic_qrc_candidates(qrc_candidates, "rc")
|
||||
|
||||
return py_candidates
|
||||
|
||||
# incase there is not .pyproject file, search all python files in project_dir, except
|
||||
# ignore_dirs
|
||||
if extra_ignore_dirs:
|
||||
ignore_dirs.update(extra_ignore_dirs)
|
||||
|
||||
# find relevant .py files
|
||||
_walk = os.walk(project_dir)
|
||||
for root, dirs, files in _walk:
|
||||
dirs[:] = [d for d in dirs if d not in ignore_dirs and not d.startswith(".")]
|
||||
for py_file in files:
|
||||
if py_file.endswith(".py"):
|
||||
py_candidates.append(Path(root) / py_file)
|
||||
|
||||
return py_candidates
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_ast(py_file: Path):
|
||||
"""Given a Python file returns the abstract syntax tree
|
||||
"""
|
||||
contents = py_file.read_text(encoding="utf-8")
|
||||
try:
|
||||
tree = ast.parse(contents)
|
||||
except SyntaxError:
|
||||
print(f"[DEPLOY] Unable to parse {py_file}")
|
||||
return tree
|
||||
|
||||
|
||||
def find_permission_categories(project_dir: Path, extra_ignore_dirs: list[Path] = None,
|
||||
project_data=None):
|
||||
"""Given the project directory, finds all the permission categories required by the
|
||||
project. eg: Camera, Bluetooth, Contacts etc.
|
||||
|
||||
Note: This function is only relevant for mac0S deployment.
|
||||
"""
|
||||
all_perm_categories = set()
|
||||
mod_pattern = re.compile("Q(?P<mod_name>.*)Permission")
|
||||
|
||||
def pyside_permission_imports(py_file: Path):
|
||||
perm_categories = []
|
||||
try:
|
||||
tree = get_ast(py_file)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
main_mod_name = node.module
|
||||
if main_mod_name == "PySide6.QtCore":
|
||||
# considers 'from PySide6.QtCore import QtMicrophonePermission'
|
||||
for imported_module in node.names:
|
||||
full_mod_name = imported_module.name
|
||||
match = mod_pattern.search(full_mod_name)
|
||||
if match:
|
||||
mod_name = match.group("mod_name")
|
||||
perm_categories.append(mod_name)
|
||||
continue
|
||||
|
||||
if isinstance(node, ast.Import):
|
||||
for imported_module in node.names:
|
||||
full_mod_name = imported_module.name
|
||||
if full_mod_name == "PySide6":
|
||||
logging.warning(IMPORT_WARNING_PYSIDE.format(str(py_file)))
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"[DEPLOY] Finding permission categories failed on file "
|
||||
f"{str(py_file)} with error {e}")
|
||||
|
||||
return set(perm_categories)
|
||||
|
||||
if extra_ignore_dirs is not None:
|
||||
extra_ignore_dirs = tuple(extra_ignore_dirs)
|
||||
py_candidates = get_py_files(project_dir, extra_ignore_dirs, project_data)
|
||||
for py_candidate in py_candidates:
|
||||
all_perm_categories = all_perm_categories.union(pyside_permission_imports(py_candidate))
|
||||
|
||||
if not all_perm_categories:
|
||||
ValueError("[DEPLOY] No permission categories were found for macOS app bundle creation.")
|
||||
|
||||
return all_perm_categories
|
||||
|
||||
|
||||
def find_pyside_modules(project_dir: Path, extra_ignore_dirs: list[Path] = None,
|
||||
project_data=None):
|
||||
"""
|
||||
Searches all the python files in the project to find all the PySide modules used by
|
||||
the application.
|
||||
"""
|
||||
all_modules = set()
|
||||
mod_pattern = re.compile("PySide6.Qt(?P<mod_name>.*)")
|
||||
|
||||
@lru_cache
|
||||
def pyside_module_imports(py_file: Path):
|
||||
modules = []
|
||||
try:
|
||||
tree = get_ast(py_file)
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
main_mod_name = node.module
|
||||
if main_mod_name and main_mod_name.startswith("PySide6"):
|
||||
if main_mod_name == "PySide6":
|
||||
# considers 'from PySide6 import QtCore'
|
||||
for imported_module in node.names:
|
||||
full_mod_name = imported_module.name
|
||||
if full_mod_name.startswith("Qt"):
|
||||
modules.append(full_mod_name[2:])
|
||||
continue
|
||||
|
||||
# considers 'from PySide6.QtCore import Qt'
|
||||
match = mod_pattern.search(main_mod_name)
|
||||
if match:
|
||||
mod_name = match.group("mod_name")
|
||||
modules.append(mod_name)
|
||||
else:
|
||||
logging.warning((
|
||||
f"[DEPLOY] Unable to find module name from {ast.dump(node)}"))
|
||||
|
||||
if isinstance(node, ast.Import):
|
||||
for imported_module in node.names:
|
||||
full_mod_name = imported_module.name
|
||||
if full_mod_name == "PySide6":
|
||||
logging.warning(IMPORT_WARNING_PYSIDE.format(str(py_file)))
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"[DEPLOY] Finding module import failed on file {str(py_file)} with "
|
||||
f"error {e}")
|
||||
|
||||
return set(modules)
|
||||
|
||||
if extra_ignore_dirs is not None:
|
||||
extra_ignore_dirs = tuple(extra_ignore_dirs)
|
||||
py_candidates = get_py_files(project_dir, extra_ignore_dirs, project_data)
|
||||
for py_candidate in py_candidates:
|
||||
all_modules = all_modules.union(pyside_module_imports(py_candidate))
|
||||
|
||||
if not all_modules:
|
||||
ValueError("[DEPLOY] No PySide6 modules were found")
|
||||
|
||||
return list(all_modules)
|
||||
|
||||
|
||||
class QtDependencyReader:
|
||||
def __init__(self, dry_run: bool = False) -> None:
|
||||
self.dry_run = dry_run
|
||||
self.lib_reader_name = None
|
||||
self.qt_module_path_pattern = None
|
||||
self.lib_pattern = None
|
||||
self.command = None
|
||||
self.qt_libs_dir = None
|
||||
|
||||
if sys.platform == "linux":
|
||||
self.lib_reader_name = "readelf"
|
||||
self.qt_module_path_pattern = "libQt6{module}.so.6"
|
||||
self.lib_pattern = re.compile("libQt6(?P<mod_name>.*).so.6")
|
||||
self.command_args = "-d"
|
||||
elif sys.platform == "darwin":
|
||||
self.lib_reader_name = "dyld_info"
|
||||
self.qt_module_path_pattern = "Qt{module}.framework/Versions/A/Qt{module}"
|
||||
self.lib_pattern = re.compile("@rpath/Qt(?P<mod_name>.*).framework/Versions/A/")
|
||||
self.command_args = "-dependents"
|
||||
elif sys.platform == "win32":
|
||||
self.lib_reader_name = "dumpbin"
|
||||
self.qt_module_path_pattern = "Qt6{module}.dll"
|
||||
self.lib_pattern = re.compile("Qt6(?P<mod_name>.*).dll")
|
||||
self.command_args = "/dependents"
|
||||
else:
|
||||
print(f"[DEPLOY] Deployment on unsupported platfrom {sys.platform}")
|
||||
sys.exit(1)
|
||||
|
||||
self.pyside_install_dir = None
|
||||
self.qt_libs_dir = self.get_qt_libs_dir()
|
||||
self._lib_reader = shutil.which(self.lib_reader_name)
|
||||
|
||||
def get_qt_libs_dir(self):
|
||||
"""
|
||||
Finds the path to the Qt libs directory inside PySide6 package installation
|
||||
"""
|
||||
# PYSIDE-2785 consider dist-packages for Debian based systems
|
||||
for possible_site_package in site.getsitepackages():
|
||||
if possible_site_package.endswith(("site-packages", "dist-packages")):
|
||||
self.pyside_install_dir = Path(possible_site_package) / "PySide6"
|
||||
if self.pyside_install_dir.exists():
|
||||
break
|
||||
|
||||
if not self.pyside_install_dir:
|
||||
print("Unable to find where PySide6 is installed. Exiting ...")
|
||||
sys.exit(-1)
|
||||
|
||||
if sys.platform == "win32":
|
||||
return self.pyside_install_dir
|
||||
|
||||
return self.pyside_install_dir / "Qt" / "lib" # for linux and macOS
|
||||
|
||||
@property
|
||||
def lib_reader(self):
|
||||
return self._lib_reader
|
||||
|
||||
def find_dependencies(self, module: str, used_modules: set[str] = None):
|
||||
"""
|
||||
Given a Qt module, find all the other Qt modules it is dependent on and add it to the
|
||||
'used_modules' set
|
||||
"""
|
||||
qt_module_path = self.qt_libs_dir / self.qt_module_path_pattern.format(module=module)
|
||||
if not qt_module_path.exists():
|
||||
warnings.warn(f"[DEPLOY] {qt_module_path.name} not found in {str(qt_module_path)}."
|
||||
"Skipping finding its dependencies.", category=RuntimeWarning)
|
||||
return
|
||||
|
||||
lib_pattern = re.compile(self.lib_pattern)
|
||||
command = [self.lib_reader, self.command_args, str(qt_module_path)]
|
||||
# print the command if dry_run is True.
|
||||
# Normally run_command is going to print the command in dry_run mode. But, this is a
|
||||
# special case where we need to print the command as well as to run it.
|
||||
if self.dry_run:
|
||||
command_str = " ".join(command)
|
||||
print(command_str + "\n")
|
||||
|
||||
# We need to run this even for dry run, to see the full Nuitka command being executed
|
||||
_, output = run_command(command=command, dry_run=False, fetch_output=True)
|
||||
|
||||
dependent_modules = set()
|
||||
for line in output.splitlines():
|
||||
line = line.decode("utf-8").lstrip()
|
||||
if sys.platform == "darwin":
|
||||
if line.endswith(f"Qt{module} [arm64]:"):
|
||||
# macOS Qt frameworks bundles have both x86_64 and arm64 architectures
|
||||
# We only need to consider one as the dependencies are redundant
|
||||
break
|
||||
elif line.endswith(f"Qt{module} [X86_64]:"):
|
||||
# this line needs to be skipped because it matches with the pattern
|
||||
# and is related to the module itself, not the dependencies of the module
|
||||
continue
|
||||
elif sys.platform == "win32" and line.startswith("Summary"):
|
||||
# the dependencies would be found before the `Summary` line
|
||||
break
|
||||
match = lib_pattern.search(line)
|
||||
if match:
|
||||
dep_module = match.group("mod_name")
|
||||
dependent_modules.add(dep_module)
|
||||
if dep_module not in used_modules:
|
||||
used_modules.add(dep_module)
|
||||
self.find_dependencies(module=dep_module, used_modules=used_modules)
|
||||
|
||||
if dependent_modules:
|
||||
logging.info(f"[DEPLOY] Following dependencies found for {module}: {dependent_modules}")
|
||||
else:
|
||||
logging.info(f"[DEPLOY] No Qt dependencies found for {module}")
|
||||
|
||||
def find_plugin_dependencies(self, used_modules: list[str], python_exe: Path) -> list[str]:
|
||||
"""
|
||||
Given the modules used by the application, returns all the required plugins
|
||||
"""
|
||||
plugins = set()
|
||||
pyside_wheels = ["PySide6_Essentials", "PySide6_Addons"]
|
||||
# TODO from 3.12 use list(dist.name for dist in importlib.metadata.distributions())
|
||||
_, installed_packages = run_command(command=[str(python_exe), "-m", "pip", "freeze"],
|
||||
dry_run=False, fetch_output=True)
|
||||
installed_packages = [p.decode().split('==')[0] for p in installed_packages.split()]
|
||||
for pyside_wheel in pyside_wheels:
|
||||
if pyside_wheel not in installed_packages:
|
||||
# the wheel is not installed and hence no plugins are checked for its modules
|
||||
logging.warning((f"[DEPLOY] The package {pyside_wheel} is not installed. "))
|
||||
continue
|
||||
pyside_mod_plugin_json_name = f"{pyside_wheel}.json"
|
||||
pyside_mod_plugin_json_file = self.pyside_install_dir / pyside_mod_plugin_json_name
|
||||
if not pyside_mod_plugin_json_file.exists():
|
||||
warnings.warn(f"[DEPLOY] Unable to find {pyside_mod_plugin_json_file}.",
|
||||
category=RuntimeWarning)
|
||||
continue
|
||||
|
||||
# convert the json to dict
|
||||
pyside_mod_dict = {}
|
||||
with open(pyside_mod_plugin_json_file) as pyside_json:
|
||||
pyside_mod_dict = json.load(pyside_json)
|
||||
|
||||
# find all the plugins in the modules
|
||||
for module in used_modules:
|
||||
plugins.update(pyside_mod_dict.get(module, []))
|
||||
|
||||
return list(plugins)
|
||||
@@ -0,0 +1,106 @@
|
||||
# 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
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from . import EXE_FORMAT
|
||||
from .config import Config, DesktopConfig
|
||||
|
||||
|
||||
def config_option_exists():
|
||||
for argument in sys.argv:
|
||||
if any(item in argument for item in ["--config-file", "-c"]):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def cleanup(config: Config, is_android: bool = False):
|
||||
"""
|
||||
Cleanup the generated build folders/files.
|
||||
|
||||
Parameters:
|
||||
config (Config): The configuration object containing paths and settings.
|
||||
is_android (bool): Flag indicating if the cleanup is for an Android project. Default is False.
|
||||
"""
|
||||
if config.generated_files_path.exists():
|
||||
try:
|
||||
shutil.rmtree(config.generated_files_path)
|
||||
logging.info("[DEPLOY] Deployment directory purged")
|
||||
except PermissionError as e:
|
||||
print(f"{type(e).__name__}: {e}")
|
||||
logging.warning(f"[DEPLOY] Could not delete {config.generated_files_path}")
|
||||
|
||||
if is_android:
|
||||
buildozer_spec: Path = config.project_dir / "buildozer.spec"
|
||||
if buildozer_spec.exists():
|
||||
try:
|
||||
buildozer_spec.unlink()
|
||||
logging.info(f"[DEPLOY] {str(buildozer_spec)} removed")
|
||||
except PermissionError as e:
|
||||
print(f"{type(e).__name__}: {e}")
|
||||
logging.warning(f"[DEPLOY] Could not delete {buildozer_spec}")
|
||||
|
||||
buildozer_build: Path = config.project_dir / ".buildozer"
|
||||
if buildozer_build.exists():
|
||||
try:
|
||||
shutil.rmtree(buildozer_build)
|
||||
logging.info(f"[DEPLOY] {str(buildozer_build)} removed")
|
||||
except PermissionError as e:
|
||||
print(f"{type(e).__name__}: {e}")
|
||||
logging.warning(f"[DEPLOY] Could not delete {buildozer_build}")
|
||||
|
||||
|
||||
def create_config_file(main_file: Path, dry_run: bool = False):
|
||||
"""
|
||||
Creates a new pysidedeploy.spec
|
||||
"""
|
||||
|
||||
config_file = main_file.parent / "pysidedeploy.spec"
|
||||
logging.info(f"[DEPLOY] Creating config file {config_file}")
|
||||
|
||||
default_config_file = Path(__file__).parent / "default.spec"
|
||||
# the config parser needs a reference to parse. So, in the case of --dry-run
|
||||
# use the default.spec file.
|
||||
if dry_run:
|
||||
return default_config_file
|
||||
|
||||
shutil.copy(default_config_file, config_file)
|
||||
return config_file
|
||||
|
||||
|
||||
def finalize(config: DesktopConfig):
|
||||
"""
|
||||
Copy the executable into the final location
|
||||
For Android deployment, this is done through buildozer
|
||||
"""
|
||||
exe_format = EXE_FORMAT
|
||||
if config.mode == DesktopConfig.NuitkaMode.STANDALONE and sys.platform != "darwin":
|
||||
exe_format = ".dist"
|
||||
|
||||
generated_exec_path = config.generated_files_path / (config.source_file.stem + exe_format)
|
||||
if not generated_exec_path.exists():
|
||||
logging.error(f"[DEPLOY] Executable not found at {generated_exec_path.absolute()}")
|
||||
return
|
||||
|
||||
logging.info(f"[DEPLOY] executable generated at {generated_exec_path.absolute()}")
|
||||
if not config.exe_dir:
|
||||
logging.info("[DEPLOY] Not copying output executable because no output directory specified")
|
||||
return
|
||||
|
||||
output_path = config.exe_dir / (config.title + exe_format)
|
||||
|
||||
if sys.platform == "darwin" or config.mode == DesktopConfig.NuitkaMode.STANDALONE:
|
||||
# Copy the folder that contains the executable
|
||||
logging.info(f"[DEPLOY] copying generated folder to {output_path.absolute()}")
|
||||
shutil.copytree(generated_exec_path, output_path, dirs_exist_ok=True)
|
||||
else:
|
||||
# Copy a single file
|
||||
logging.info(f"[DEPLOY] copying generated file to {output_path.absolute()}")
|
||||
shutil.copy(generated_exec_path, output_path)
|
||||
|
||||
print(f"[DEPLOY] Executed file created in {output_path.absolute()}")
|
||||
@@ -0,0 +1,184 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
# enables to use typehints for classes that has not been defined yet or imported
|
||||
# used for resolving circular imports
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
import shlex
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from project_lib import DesignStudioProject
|
||||
from . import MAJOR_VERSION, run_command, DEFAULT_IGNORE_DIRS, PLUGINS_TO_REMOVE
|
||||
from .config import DesktopConfig
|
||||
|
||||
|
||||
class Nuitka:
|
||||
"""
|
||||
Wrapper class around the nuitka executable, enabling its usage through python code
|
||||
"""
|
||||
|
||||
def __init__(self, nuitka):
|
||||
self.nuitka = nuitka
|
||||
# plugins to ignore. The sensible plugins are include by default by Nuitka for PySide6
|
||||
# application deployment
|
||||
self.qt_plugins_to_ignore = ["imageformats", # being Nuitka `sensible`` plugins
|
||||
"iconengines",
|
||||
"mediaservice",
|
||||
"printsupport",
|
||||
"platforms",
|
||||
"platformthemes",
|
||||
"styles",
|
||||
"wayland-shell-integration",
|
||||
"wayland-decoration-client",
|
||||
"wayland-graphics-integration-client",
|
||||
"egldeviceintegrations",
|
||||
"xcbglintegrations",
|
||||
"tls", # end Nuitka `sensible` plugins
|
||||
"generic" # plugins that error with Nuitka
|
||||
]
|
||||
|
||||
self.files_to_ignore = [".cpp.o", ".qsb"]
|
||||
|
||||
@staticmethod
|
||||
def icon_option():
|
||||
if sys.platform == "linux":
|
||||
return "--linux-icon"
|
||||
elif sys.platform == "win32":
|
||||
return "--windows-icon-from-ico"
|
||||
else:
|
||||
return "--macos-app-icon"
|
||||
|
||||
def _create_windows_command(self, source_file: Path, command: list):
|
||||
"""
|
||||
Special case for Windows where the command length is limited to 8191 characters.
|
||||
"""
|
||||
|
||||
# if the platform is windows and the command is more than 8191 characters, the command
|
||||
# will fail with the error message "The command line is too long". To avoid this, we will
|
||||
# we will move the source_file to the intermediate source file called deploy_main.py, and
|
||||
# include the Nuitka options direcly in the main file as mentioned in
|
||||
# https://nuitka.net/user-documentation/user-manual.html#nuitka-project-options
|
||||
|
||||
# convert command into a format recognized by Nuitka when written to the main file
|
||||
# the first item is ignore because it is 'python -m nuitka'
|
||||
nuitka_comment_options = []
|
||||
for command_entry in command[4:]:
|
||||
nuitka_comment_options.append(f"# nuitka-project: {command_entry}")
|
||||
nuitka_comment_options_str = "\n".join(nuitka_comment_options)
|
||||
nuitka_comment_options_str += "\n"
|
||||
|
||||
# read the content of the source file
|
||||
new_source_content = (nuitka_comment_options_str
|
||||
+ Path(source_file).read_text(encoding="utf-8"))
|
||||
|
||||
# create and write back the new source content to deploy_main.py
|
||||
new_source_file = source_file.parent / "deploy_main.py"
|
||||
new_source_file.write_text(new_source_content, encoding="utf-8")
|
||||
|
||||
return new_source_file
|
||||
|
||||
def create_executable(self, source_file: Path, extra_args: str, qml_files: list[Path],
|
||||
qt_plugins: list[str], excluded_qml_plugins: list[str], icon: str,
|
||||
dry_run: bool, permissions: list[str],
|
||||
mode: DesktopConfig.NuitkaMode) -> str:
|
||||
qt_plugins = [plugin for plugin in qt_plugins if plugin not in self.qt_plugins_to_ignore]
|
||||
extra_args = shlex.split(extra_args)
|
||||
|
||||
# macOS uses the --standalone option by default to create an app bundle
|
||||
if sys.platform == "darwin":
|
||||
# create an app bundle
|
||||
extra_args.extend(["--standalone", "--macos-create-app-bundle"])
|
||||
permission_pattern = "--macos-app-protected-resource={permission}"
|
||||
for permission in permissions:
|
||||
extra_args.append(permission_pattern.format(permission=permission))
|
||||
else:
|
||||
extra_args.append(f"--{mode.value}")
|
||||
|
||||
qml_args = []
|
||||
if qml_files:
|
||||
# include all the subdirectories in the project directory as data directories
|
||||
# This includes all the qml modules
|
||||
all_relevant_subdirs = []
|
||||
for subdir in source_file.parent.iterdir():
|
||||
if subdir.is_dir() and subdir.name not in DEFAULT_IGNORE_DIRS:
|
||||
extra_args.append(f"--include-data-dir={subdir}="
|
||||
f"./{subdir.name}")
|
||||
all_relevant_subdirs.append(subdir)
|
||||
|
||||
# find all the qml files that are not included via the data directories
|
||||
extra_qml_files = [file for file in qml_files
|
||||
if file.parent not in all_relevant_subdirs]
|
||||
|
||||
# This will generate options for each file using:
|
||||
# --include-data-files=ABSOLUTE_PATH_TO_FILE=RELATIVE_PATH_TO ROOT
|
||||
# for each file.
|
||||
qml_args.extend(
|
||||
[f"--include-data-files={qml_file.resolve()}="
|
||||
f"./{qml_file.resolve().relative_to(source_file.resolve().parent)}"
|
||||
for qml_file in extra_qml_files]
|
||||
)
|
||||
|
||||
if qml_files or DesignStudioProject.is_ds_project(source_file):
|
||||
# add qml plugin. The `qml`` plugin name is not present in the module json files shipped
|
||||
# with Qt and hence not in `qt_plugins``. However, Nuitka uses the 'qml' plugin name to
|
||||
# include the necessary qml plugins. There we have to add it explicitly for a qml
|
||||
# application
|
||||
qt_plugins.append("qml")
|
||||
|
||||
if excluded_qml_plugins:
|
||||
prefix = "lib" if sys.platform != "win32" else ""
|
||||
for plugin in excluded_qml_plugins:
|
||||
dll_name = plugin.replace("Qt", f"Qt{MAJOR_VERSION}")
|
||||
qml_args.append(f"--noinclude-dlls={prefix}{dll_name}*")
|
||||
|
||||
# Exclude .qen json files from QtQuickEffectMaker
|
||||
# These files are not relevant for PySide6 applications
|
||||
qml_args.append("--noinclude-dlls=*/qml/QtQuickEffectMaker/*")
|
||||
|
||||
# Exclude files that cannot be processed by Nuitka
|
||||
for file in self.files_to_ignore:
|
||||
extra_args.append(f"--noinclude-dlls=*{file}")
|
||||
|
||||
output_dir = source_file.parent / "deployment"
|
||||
if not dry_run:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
logging.info("[DEPLOY] Running Nuitka")
|
||||
command = self.nuitka + [
|
||||
os.fspath(source_file),
|
||||
"--follow-imports",
|
||||
"--enable-plugin=pyside6",
|
||||
f"--output-dir={output_dir}",
|
||||
]
|
||||
|
||||
command.extend(extra_args + qml_args)
|
||||
command.append(f"{self.__class__.icon_option()}={icon}")
|
||||
if qt_plugins:
|
||||
# sort qt_plugins so that the result is definitive when testing
|
||||
qt_plugins.sort()
|
||||
# remove the following plugins from the qt_plugins list as Nuitka only checks
|
||||
# for plugins within PySide6/Qt/plugins folder, and the following plugins
|
||||
# are not present in the PySide6/Qt/plugins folder
|
||||
qt_plugins = [plugin for plugin in qt_plugins if plugin not in PLUGINS_TO_REMOVE]
|
||||
qt_plugins_str = ",".join(qt_plugins)
|
||||
command.append(f"--include-qt-plugins={qt_plugins_str}")
|
||||
|
||||
long_command = False
|
||||
if sys.platform == "win32" and len(" ".join(str(cmd) for cmd in command)) > 7000:
|
||||
logging.info("[DEPLOY] Nuitka command too long for Windows. "
|
||||
"Copying the contents of main Python file to an intermediate "
|
||||
"deploy_main.py file")
|
||||
long_command = True
|
||||
new_source_file = self._create_windows_command(source_file=source_file, command=command)
|
||||
command = self.nuitka + [os.fspath(new_source_file)]
|
||||
|
||||
command_str, _ = run_command(command=command, dry_run=dry_run)
|
||||
|
||||
# if deploy_main.py exists, delete it after the command is run
|
||||
if long_command:
|
||||
os.remove(source_file.parent / "deploy_main.py")
|
||||
|
||||
return command_str
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1,123 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from importlib import util
|
||||
from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
|
||||
from . import Config, run_command
|
||||
|
||||
|
||||
class PythonExecutable:
|
||||
"""
|
||||
Wrapper class around Python executable
|
||||
"""
|
||||
|
||||
def __init__(self, python_path: Path = None, dry_run: bool = False, init: bool = False,
|
||||
force: bool = False):
|
||||
|
||||
self.dry_run = dry_run
|
||||
self.init = init
|
||||
if not python_path:
|
||||
response = "yes"
|
||||
# checking if inside virtual environment
|
||||
if not self.is_venv() and not force and not self.dry_run and not self.init:
|
||||
response = input(("You are not using a virtual environment. pyside6-deploy needs "
|
||||
"to install a few Python packages for deployment to work "
|
||||
"seamlessly. \n Proceed? [Y/n]"))
|
||||
|
||||
if response.lower() in ["no", "n"]:
|
||||
print("[DEPLOY] Exiting ...")
|
||||
sys.exit(0)
|
||||
|
||||
self.exe = Path(sys.executable)
|
||||
else:
|
||||
self.exe = python_path
|
||||
|
||||
logging.info(f"[DEPLOY] Using Python at {str(self.exe)}")
|
||||
|
||||
@property
|
||||
def exe(self):
|
||||
return Path(self._exe)
|
||||
|
||||
@exe.setter
|
||||
def exe(self, exe):
|
||||
self._exe = exe
|
||||
|
||||
@staticmethod
|
||||
def is_venv():
|
||||
venv = os.environ.get("VIRTUAL_ENV")
|
||||
return True if venv else False
|
||||
|
||||
def is_pyenv_python(self):
|
||||
pyenv_root = os.environ.get("PYENV_ROOT")
|
||||
|
||||
if pyenv_root:
|
||||
resolved_exe = self.exe.resolve()
|
||||
if str(resolved_exe).startswith(pyenv_root):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def install(self, packages: list = None):
|
||||
_, installed_packages = run_command(command=[str(self.exe), "-m", "pip", "freeze"],
|
||||
dry_run=False, fetch_output=True)
|
||||
installed_packages = [p.decode().split('==')[0] for p in installed_packages.split()]
|
||||
for package in packages:
|
||||
package_info = package.split('==')
|
||||
package_components_len = len(package_info)
|
||||
package_name, package_version = None, None
|
||||
if package_components_len == 1:
|
||||
package_name = package_info[0]
|
||||
elif package_components_len == 2:
|
||||
package_name = package_info[0]
|
||||
package_version = package_info[1]
|
||||
else:
|
||||
raise ValueError(f"{package} should be of the format 'package_name'=='version'")
|
||||
if (package_name not in installed_packages) and (not self.is_installed(package_name)):
|
||||
logging.info(f"[DEPLOY] Installing package: {package}")
|
||||
run_command(
|
||||
command=[self.exe, "-m", "pip", "install", package],
|
||||
dry_run=self.dry_run,
|
||||
)
|
||||
elif package_version:
|
||||
installed_version = version(package_name)
|
||||
if package_version != installed_version:
|
||||
logging.info(f"[DEPLOY] Installing package: {package_name}"
|
||||
f"version: {package_version}")
|
||||
run_command(
|
||||
command=[self.exe, "-m", "pip", "install", "--force", package],
|
||||
dry_run=self.dry_run,
|
||||
)
|
||||
else:
|
||||
logging.info(f"[DEPLOY] package: {package_name}=={package_version}"
|
||||
" already installed")
|
||||
else:
|
||||
logging.info(f"[DEPLOY] package: {package_name} already installed")
|
||||
|
||||
def is_installed(self, package):
|
||||
return bool(util.find_spec(package))
|
||||
|
||||
def install_dependencies(self, config: Config, packages: str, is_android: bool = False):
|
||||
"""
|
||||
Installs the python package dependencies for the target deployment platform
|
||||
"""
|
||||
packages = config.get_value("python", packages).split(",")
|
||||
if not self.init:
|
||||
# install packages needed for deployment
|
||||
logging.info("[DEPLOY] Installing dependencies")
|
||||
self.install(packages=packages)
|
||||
# nuitka requires patchelf to make patchelf rpath changes for some Qt files
|
||||
if sys.platform.startswith("linux") and not is_android:
|
||||
self.install(packages=["patchelf"])
|
||||
elif is_android:
|
||||
# install only buildozer
|
||||
logging.info("[DEPLOY] Installing buildozer")
|
||||
buildozer_package_with_version = ([package for package in packages
|
||||
if package.startswith("buildozer")])
|
||||
self.install(packages=list(buildozer_package_with_version))
|
||||
461
.venv_nopip/Lib/site-packages/PySide6/scripts/metaobjectdump.py
Normal file
461
.venv_nopip/Lib/site-packages/PySide6/scripts/metaobjectdump.py
Normal file
@@ -0,0 +1,461 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tokenize
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
|
||||
DESCRIPTION = """Parses Python source code to create QObject metatype
|
||||
information in JSON format for qmltyperegistrar."""
|
||||
|
||||
|
||||
REVISION = 68
|
||||
|
||||
|
||||
CPP_TYPE_MAPPING = {"str": "QString"}
|
||||
|
||||
|
||||
QML_IMPORT_NAME = "QML_IMPORT_NAME"
|
||||
QML_IMPORT_MAJOR_VERSION = "QML_IMPORT_MAJOR_VERSION"
|
||||
QML_IMPORT_MINOR_VERSION = "QML_IMPORT_MINOR_VERSION"
|
||||
QT_MODULES = "QT_MODULES"
|
||||
|
||||
|
||||
ITEM_MODELS = ["QAbstractListModel", "QAbstractProxyModel",
|
||||
"QAbstractTableModel", "QConcatenateTablesProxyModel",
|
||||
"QFileSystemModel", "QIdentityProxyModel", "QPdfBookmarkModel",
|
||||
"QPdfSearchModel", "QSortFilterProxyModel", "QSqlQueryModel",
|
||||
"QStandardItemModel", "QStringListModel", "QTransposeProxyModel",
|
||||
"QWebEngineHistoryModel"]
|
||||
|
||||
|
||||
QOBJECT_DERIVED = ["QObject", "QQuickItem", "QQuickPaintedItem"] + ITEM_MODELS
|
||||
|
||||
|
||||
# Python 3.9 does not support this syntax, yet
|
||||
# AstDecorator = ast.Name | ast.Call
|
||||
# AstPySideTypeSpec = ast.Name | ast.Constant
|
||||
AstDecorator = Union[ast.Name, ast.Call]
|
||||
AstPySideTypeSpec = Union[ast.Name, ast.Constant]
|
||||
|
||||
|
||||
ClassList = list[dict]
|
||||
|
||||
|
||||
# PropertyEntry = dict[str, str | int | bool]
|
||||
PropertyEntry = dict[str, Union[str, int, bool]]
|
||||
|
||||
Argument = dict[str, str]
|
||||
Arguments = list[Argument]
|
||||
# Signal = dict[str, str | Arguments]
|
||||
# Slot = dict[str, str | Arguments]
|
||||
Signal = dict[str, Union[str, Arguments]]
|
||||
Slot = dict[str, Union[str, Arguments]]
|
||||
|
||||
|
||||
def _decorator(name: str, value: str) -> dict[str, str]:
|
||||
"""Create a QML decorator JSON entry"""
|
||||
return {"name": name, "value": value}
|
||||
|
||||
|
||||
def _attribute(node: ast.Attribute) -> tuple[str, str]:
|
||||
"""Split an attribute."""
|
||||
return node.value.id, node.attr
|
||||
|
||||
|
||||
def _name(node: ast.Name | ast.Attribute | ast.Constant) -> str:
|
||||
"""Return the name of something that is either an attribute or a name,
|
||||
such as base classes or call.func"""
|
||||
if isinstance(node, ast.Constant):
|
||||
return str(node.value)
|
||||
if isinstance(node, ast.Attribute):
|
||||
qualifier, name = _attribute(node)
|
||||
return f"{qualifier}.{node.attr}"
|
||||
return node.id
|
||||
|
||||
|
||||
def _func_name(node: ast.Call) -> str:
|
||||
return _name(node.func)
|
||||
|
||||
|
||||
def _python_to_cpp_type(type: str) -> str:
|
||||
"""Python to C++ type"""
|
||||
c = CPP_TYPE_MAPPING.get(type)
|
||||
return c if c else type
|
||||
|
||||
|
||||
def _parse_property_kwargs(keywords: list[ast.keyword], prop: PropertyEntry):
|
||||
"""Parse keyword arguments of @Property"""
|
||||
for k in keywords:
|
||||
if k.arg == "notify":
|
||||
prop["notify"] = _name(k.value)
|
||||
|
||||
|
||||
def _parse_assignment(node: ast.Assign) -> tuple[str | None, ast.AST | None]:
|
||||
"""Parse an assignment and return a tuple of name, value."""
|
||||
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
||||
var_name = node.targets[0].id
|
||||
return (var_name, node.value)
|
||||
return (None, None)
|
||||
|
||||
|
||||
def _parse_pyside_type(type_spec: AstPySideTypeSpec) -> str:
|
||||
"""Parse type specification of a Slot/Property decorator. Usually a type,
|
||||
but can also be a string constant with a C++ type name."""
|
||||
if isinstance(type_spec, ast.Constant):
|
||||
return type_spec.value
|
||||
return _python_to_cpp_type(_name(type_spec))
|
||||
|
||||
|
||||
def _parse_call_args(call: ast.Call):
|
||||
"""Parse arguments of a Signal call/Slot decorator (type list)."""
|
||||
result: Arguments = []
|
||||
for n, arg in enumerate(call.args):
|
||||
par_name = f"a{n + 1}"
|
||||
par_type = _parse_pyside_type(arg)
|
||||
result.append({"name": par_name, "type": par_type})
|
||||
return result
|
||||
|
||||
|
||||
def _parse_slot(func_name: str, call: ast.Call) -> Slot:
|
||||
"""Parse a 'Slot' decorator."""
|
||||
return_type = "void"
|
||||
for kwarg in call.keywords:
|
||||
if kwarg.arg == "result":
|
||||
return_type = _python_to_cpp_type(_name(kwarg.value))
|
||||
break
|
||||
return {"access": "public", "name": func_name,
|
||||
"arguments": _parse_call_args(call),
|
||||
"returnType": return_type}
|
||||
|
||||
|
||||
class VisitorContext:
|
||||
"""Stores a list of QObject-derived classes encountered in order to find
|
||||
out which classes inherit QObject."""
|
||||
|
||||
def __init__(self):
|
||||
self.qobject_derived = QOBJECT_DERIVED
|
||||
|
||||
|
||||
class MetaObjectDumpVisitor(ast.NodeVisitor):
|
||||
"""AST visitor for parsing sources and creating the data structure for
|
||||
JSON."""
|
||||
|
||||
def __init__(self, context: VisitorContext):
|
||||
super().__init__()
|
||||
self._context = context
|
||||
self._json_class_list: ClassList = []
|
||||
# Property by name, which will be turned into the JSON List later
|
||||
self._properties: list[PropertyEntry] = []
|
||||
self._signals: list[Signal] = []
|
||||
self._within_class: bool = False
|
||||
self._qt_modules: set[str] = set()
|
||||
self._qml_import_name = ""
|
||||
self._qml_import_major_version = 0
|
||||
self._qml_import_minor_version = 0
|
||||
|
||||
def json_class_list(self) -> ClassList:
|
||||
return self._json_class_list
|
||||
|
||||
def qml_import_name(self) -> str:
|
||||
return self._qml_import_name
|
||||
|
||||
def qml_import_version(self) -> tuple[int, int]:
|
||||
return (self._qml_import_major_version, self._qml_import_minor_version)
|
||||
|
||||
def qt_modules(self):
|
||||
return sorted(self._qt_modules)
|
||||
|
||||
@staticmethod
|
||||
def create_ast(filename: Path) -> ast.Module:
|
||||
"""Create an Abstract Syntax Tree on which a visitor can be run"""
|
||||
node = None
|
||||
with tokenize.open(filename) as file:
|
||||
node = ast.parse(file.read(), mode="exec")
|
||||
return node
|
||||
|
||||
def visit_Assign(self, node: ast.Assign):
|
||||
"""Parse the global constants for QML-relevant values"""
|
||||
var_name, value_node = _parse_assignment(node)
|
||||
if not var_name or not isinstance(value_node, ast.Constant):
|
||||
return
|
||||
value = value_node.value
|
||||
if var_name == QML_IMPORT_NAME:
|
||||
self._qml_import_name = value
|
||||
elif var_name == QML_IMPORT_MAJOR_VERSION:
|
||||
self._qml_import_major_version = value
|
||||
elif var_name == QML_IMPORT_MINOR_VERSION:
|
||||
self._qml_import_minor_version = value
|
||||
|
||||
def visit_ClassDef(self, node: ast.Module):
|
||||
"""Visit a class definition"""
|
||||
self._properties = []
|
||||
self._signals = []
|
||||
self._slots = []
|
||||
self._within_class = True
|
||||
qualified_name = node.name
|
||||
last_dot = qualified_name.rfind('.')
|
||||
name = (qualified_name[last_dot + 1:] if last_dot != -1
|
||||
else qualified_name)
|
||||
|
||||
data = {"className": name,
|
||||
"qualifiedClassName": qualified_name}
|
||||
|
||||
q_object = False
|
||||
bases = []
|
||||
for b in node.bases:
|
||||
# PYSIDE-2202: catch weird constructs like "class C(type(Base)):"
|
||||
if isinstance(b, ast.Name):
|
||||
base_name = _name(b)
|
||||
if base_name in self._context.qobject_derived:
|
||||
q_object = True
|
||||
self._context.qobject_derived.append(name)
|
||||
base_dict = {"access": "public", "name": base_name}
|
||||
bases.append(base_dict)
|
||||
|
||||
data["object"] = q_object
|
||||
if bases:
|
||||
data["superClasses"] = bases
|
||||
|
||||
class_decorators: list[dict] = []
|
||||
for d in node.decorator_list:
|
||||
self._parse_class_decorator(d, class_decorators)
|
||||
|
||||
if class_decorators:
|
||||
data["classInfos"] = class_decorators
|
||||
|
||||
for b in node.body:
|
||||
if isinstance(b, ast.Assign):
|
||||
self._parse_class_variable(b)
|
||||
else:
|
||||
self.visit(b)
|
||||
|
||||
if self._properties:
|
||||
data["properties"] = self._properties
|
||||
|
||||
if self._signals:
|
||||
data["signals"] = self._signals
|
||||
|
||||
if self._slots:
|
||||
data["slots"] = self._slots
|
||||
|
||||
self._json_class_list.append(data)
|
||||
|
||||
self._within_class = False
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
if self._within_class:
|
||||
for d in node.decorator_list:
|
||||
self._parse_function_decorator(node.name, d)
|
||||
|
||||
def _parse_class_decorator(self, node: AstDecorator,
|
||||
class_decorators: list[dict]):
|
||||
"""Parse ClassInfo decorators."""
|
||||
if isinstance(node, ast.Call):
|
||||
name = _func_name(node)
|
||||
if name == "QmlUncreatable":
|
||||
class_decorators.append(_decorator("QML.Creatable", "false"))
|
||||
if node.args:
|
||||
reason = node.args[0].value
|
||||
if isinstance(reason, str):
|
||||
d = _decorator("QML.UncreatableReason", reason)
|
||||
class_decorators.append(d)
|
||||
elif name == "QmlAttached" and len(node.args) == 1:
|
||||
d = _decorator("QML.Attached", node.args[0].id)
|
||||
class_decorators.append(d)
|
||||
elif name == "QmlExtended" and len(node.args) == 1:
|
||||
d = _decorator("QML.Extended", node.args[0].id)
|
||||
class_decorators.append(d)
|
||||
elif name == "ClassInfo" and node.keywords:
|
||||
kw = node.keywords[0]
|
||||
class_decorators.append(_decorator(kw.arg, kw.value.value))
|
||||
elif name == "QmlForeign" and len(node.args) == 1:
|
||||
d = _decorator("QML.Foreign", node.args[0].id)
|
||||
class_decorators.append(d)
|
||||
elif name == "QmlNamedElement" and node.args:
|
||||
name = node.args[0].value
|
||||
class_decorators.append(_decorator("QML.Element", name))
|
||||
elif name.startswith('Q'):
|
||||
print('Unknown decorator with parameters:', name,
|
||||
file=sys.stderr)
|
||||
return
|
||||
|
||||
if isinstance(node, ast.Name):
|
||||
name = node.id
|
||||
if name == "QmlElement":
|
||||
class_decorators.append(_decorator("QML.Element", "auto"))
|
||||
elif name == "QmlSingleton":
|
||||
class_decorators.append(_decorator("QML.Singleton", "true"))
|
||||
elif name == "QmlAnonymous":
|
||||
class_decorators.append(_decorator("QML.Element", "anonymous"))
|
||||
elif name.startswith('Q'):
|
||||
print('Unknown decorator:', name, file=sys.stderr)
|
||||
return
|
||||
|
||||
def _index_of_property(self, name: str) -> int:
|
||||
"""Search a property by name"""
|
||||
for i in range(len(self._properties)):
|
||||
if self._properties[i]["name"] == name:
|
||||
return i
|
||||
return -1
|
||||
|
||||
def _create_property_entry(self, name: str, type: str,
|
||||
getter: str | None = None) -> PropertyEntry:
|
||||
"""Create a property JSON entry."""
|
||||
result: PropertyEntry = {"name": name, "type": type,
|
||||
"index": len(self._properties)}
|
||||
if getter:
|
||||
result["read"] = getter
|
||||
return result
|
||||
|
||||
def _parse_function_decorator(self, func_name: str, node: AstDecorator):
|
||||
"""Parse function decorators."""
|
||||
if isinstance(node, ast.Attribute):
|
||||
name = node.value.id
|
||||
value = node.attr
|
||||
if value == "setter": # Property setter
|
||||
idx = self._index_of_property(name)
|
||||
if idx != -1:
|
||||
self._properties[idx]["write"] = func_name
|
||||
return
|
||||
|
||||
if isinstance(node, ast.Call):
|
||||
name = _name(node.func)
|
||||
if name == "Property": # Property getter
|
||||
if node.args: # 1st is type/type string
|
||||
type = _parse_pyside_type(node.args[0])
|
||||
prop = self._create_property_entry(func_name, type,
|
||||
func_name)
|
||||
_parse_property_kwargs(node.keywords, prop)
|
||||
self._properties.append(prop)
|
||||
elif name == "Slot":
|
||||
self._slots.append(_parse_slot(func_name, node))
|
||||
else:
|
||||
print('Unknown decorator with parameters:', name,
|
||||
file=sys.stderr)
|
||||
|
||||
def _parse_class_variable(self, node: ast.Assign):
|
||||
"""Parse a class variable assignment (Property, Signal, etc.)"""
|
||||
(var_name, call) = _parse_assignment(node)
|
||||
if not var_name or not isinstance(node.value, ast.Call):
|
||||
return
|
||||
func_name = _func_name(call)
|
||||
if func_name == "Signal" or func_name == "QtCore.Signal":
|
||||
signal: Signal = {"access": "public", "name": var_name,
|
||||
"arguments": _parse_call_args(call),
|
||||
"returnType": "void"}
|
||||
self._signals.append(signal)
|
||||
elif func_name == "Property" or func_name == "QtCore.Property":
|
||||
type = _python_to_cpp_type(call.args[0].id)
|
||||
prop = self._create_property_entry(var_name, type, call.args[1].id)
|
||||
if len(call.args) > 2:
|
||||
prop["write"] = call.args[2].id
|
||||
_parse_property_kwargs(call.keywords, prop)
|
||||
self._properties.append(prop)
|
||||
elif func_name == "ListProperty" or func_name == "QtCore.ListProperty":
|
||||
type = _python_to_cpp_type(call.args[0].id)
|
||||
type = f"QQmlListProperty<{type}>"
|
||||
prop = self._create_property_entry(var_name, type)
|
||||
self._properties.append(prop)
|
||||
|
||||
def visit_Import(self, node):
|
||||
for n in node.names: # "import PySide6.QtWidgets"
|
||||
self._handle_import(n.name)
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
if "." in node.module: # "from PySide6.QtWidgets import QWidget"
|
||||
self._handle_import(node.module)
|
||||
elif node.module == "PySide6": # "from PySide6 import QtWidgets"
|
||||
for n in node.names:
|
||||
if n.name.startswith("Qt"):
|
||||
self._qt_modules.add(n.name)
|
||||
|
||||
def _handle_import(self, mod: str):
|
||||
if mod.startswith("PySide6."):
|
||||
self._qt_modules.add(mod[8:])
|
||||
|
||||
|
||||
def create_arg_parser(desc: str) -> ArgumentParser:
|
||||
parser = ArgumentParser(description=desc,
|
||||
formatter_class=RawTextHelpFormatter)
|
||||
parser.add_argument('--compact', '-c', action='store_true',
|
||||
help='Use compact format')
|
||||
parser.add_argument('--suppress-file', '-s', action='store_true',
|
||||
help='Suppress inputFile entry (for testing)')
|
||||
parser.add_argument('--quiet', '-q', action='store_true',
|
||||
help='Suppress warnings')
|
||||
parser.add_argument('files', type=str, nargs="+",
|
||||
help='Python source file')
|
||||
parser.add_argument('--out-file', '-o', type=str,
|
||||
help='Write output to file rather than stdout')
|
||||
return parser
|
||||
|
||||
|
||||
def parse_file(file: Path, context: VisitorContext,
|
||||
suppress_file: bool = False) -> dict | None:
|
||||
"""Parse a file and return its json data"""
|
||||
ast_tree = MetaObjectDumpVisitor.create_ast(file)
|
||||
visitor = MetaObjectDumpVisitor(context)
|
||||
visitor.visit(ast_tree)
|
||||
|
||||
class_list = visitor.json_class_list()
|
||||
if not class_list:
|
||||
return None
|
||||
result = {"classes": class_list,
|
||||
"outputRevision": REVISION}
|
||||
|
||||
# Non-standard QML-related values for pyside6-build usage
|
||||
if visitor.qml_import_name():
|
||||
result[QML_IMPORT_NAME] = visitor.qml_import_name()
|
||||
qml_import_version = visitor.qml_import_version()
|
||||
if qml_import_version[0]:
|
||||
result[QML_IMPORT_MAJOR_VERSION] = qml_import_version[0]
|
||||
result[QML_IMPORT_MINOR_VERSION] = qml_import_version[1]
|
||||
|
||||
qt_modules = visitor.qt_modules()
|
||||
if qt_modules:
|
||||
result[QT_MODULES] = qt_modules
|
||||
|
||||
if not suppress_file:
|
||||
result["inputFile"] = os.fspath(file).replace("\\", "/")
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arg_parser = create_arg_parser(DESCRIPTION)
|
||||
args = arg_parser.parse_args()
|
||||
|
||||
context = VisitorContext()
|
||||
json_list = []
|
||||
|
||||
for file_name in args.files:
|
||||
file = Path(file_name).resolve()
|
||||
if not file.is_file():
|
||||
print(f'{file_name} does not exist or is not a file.',
|
||||
file=sys.stderr)
|
||||
sys.exit(-1)
|
||||
|
||||
try:
|
||||
json_data = parse_file(file, context, args.suppress_file)
|
||||
if json_data:
|
||||
json_list.append(json_data)
|
||||
elif not args.quiet:
|
||||
print(f"No classes found in {file_name}", file=sys.stderr)
|
||||
except (AttributeError, SyntaxError) as e:
|
||||
reason = str(e)
|
||||
print(f"Error parsing {file_name}: {reason}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
indent = None if args.compact else 4
|
||||
if args.out_file:
|
||||
with open(args.out_file, 'w') as f:
|
||||
json.dump(json_list, f, indent=indent)
|
||||
else:
|
||||
json.dump(json_list, sys.stdout, indent=indent)
|
||||
348
.venv_nopip/Lib/site-packages/PySide6/scripts/project.py
Normal file
348
.venv_nopip/Lib/site-packages/PySide6/scripts/project.py
Normal file
@@ -0,0 +1,348 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
|
||||
from project_lib import (QmlProjectData, check_qml_decorators, is_python_file, migrate_pyproject,
|
||||
QMLDIR_FILE, MOD_CMD, METATYPES_JSON_SUFFIX, SHADER_SUFFIXES,
|
||||
TRANSLATION_SUFFIX, requires_rebuild, run_command, remove_path,
|
||||
ProjectData, resolve_valid_project_file, new_project, NewProjectTypes,
|
||||
ClOptions, DesignStudioProject)
|
||||
|
||||
DESCRIPTION = """
|
||||
pyside6-project is a command line tool for creating, building and deploying Qt for Python
|
||||
applications. It operates on project files which are also used by Qt Creator.
|
||||
|
||||
Official documentation:
|
||||
https://doc.qt.io/qtforpython-6/tools/pyside-project.html
|
||||
"""
|
||||
|
||||
OPERATION_HELP = {
|
||||
"build": "Build the project. Compiles resources, UI files, and QML files if existing and "
|
||||
"necessary.",
|
||||
"run": "Build and run the project.",
|
||||
"clean": "Clean build artifacts and generated files from the project directory.",
|
||||
"qmllint": "Run the qmllint tool on QML files in the project.",
|
||||
"deploy": "Create a deployable package of the application including all dependencies.",
|
||||
"lupdate": "Update translation files (.ts) with new strings from source files.",
|
||||
"migrate-pyproject": "Migrate a *.pyproject file to pyproject.toml format."
|
||||
}
|
||||
|
||||
UIC_CMD = "pyside6-uic"
|
||||
RCC_CMD = "pyside6-rcc"
|
||||
LRELEASE_CMD = "pyside6-lrelease"
|
||||
LUPDATE_CMD = "pyside6-lupdate"
|
||||
QMLTYPEREGISTRAR_CMD = "pyside6-qmltyperegistrar"
|
||||
QMLLINT_CMD = "pyside6-qmllint"
|
||||
QSB_CMD = "pyside6-qsb"
|
||||
DEPLOY_CMD = "pyside6-deploy"
|
||||
|
||||
|
||||
def _sort_sources(files: list[Path]) -> list[Path]:
|
||||
"""Sort the sources for building, ensure .qrc is last since it might depend
|
||||
on generated files."""
|
||||
|
||||
def key_func(p: Path):
|
||||
return p.suffix if p.suffix != ".qrc" else ".zzzz"
|
||||
|
||||
return sorted(files, key=key_func)
|
||||
|
||||
|
||||
class Project:
|
||||
"""
|
||||
Class to wrap the various operations on Project
|
||||
"""
|
||||
|
||||
def __init__(self, project_file: Path):
|
||||
self.project = ProjectData(project_file=project_file)
|
||||
self.cl_options = ClOptions()
|
||||
|
||||
# Files for QML modules using the QmlElement decorators
|
||||
self._qml_module_sources: list[Path] = []
|
||||
self._qml_module_dir: Path | None = None
|
||||
self._qml_dir_file: Path | None = None
|
||||
self._qml_project_data = QmlProjectData()
|
||||
self._qml_module_check()
|
||||
|
||||
def _qml_module_check(self):
|
||||
"""Run a pre-check on Python source files and find the ones with QML
|
||||
decorators (representing a QML module)."""
|
||||
# Quick check for any QML files (to avoid running moc for no reason).
|
||||
if not self.cl_options.qml_module and not self.project.qml_files:
|
||||
return
|
||||
for file in self.project.files:
|
||||
if is_python_file(file):
|
||||
has_class, data = check_qml_decorators(file)
|
||||
if has_class:
|
||||
self._qml_module_sources.append(file)
|
||||
if data:
|
||||
self._qml_project_data = data
|
||||
|
||||
if not self._qml_module_sources:
|
||||
return
|
||||
if not self._qml_project_data:
|
||||
print("Detected QML-decorated files, " "but was unable to detect QML_IMPORT_NAME")
|
||||
sys.exit(1)
|
||||
|
||||
self._qml_module_dir = self.project.project_file.parent
|
||||
for uri_dir in self._qml_project_data.import_name.split("."):
|
||||
self._qml_module_dir /= uri_dir
|
||||
print(self._qml_module_dir)
|
||||
self._qml_dir_file = self._qml_module_dir / QMLDIR_FILE
|
||||
|
||||
if not self.cl_options.quiet:
|
||||
count = len(self._qml_module_sources)
|
||||
print(f"{self.project.project_file.name}, {count} QML file(s),"
|
||||
f" {self._qml_project_data}")
|
||||
|
||||
def _get_artifacts(self, file: Path, output_path: Path | None = None) -> \
|
||||
tuple[list[Path], list[str] | None]:
|
||||
"""Return path and command for a file's artifact"""
|
||||
if file.suffix == ".ui": # Qt form files
|
||||
py_file = f"{file.parent}/ui_{file.stem}.py"
|
||||
return [Path(py_file)], [UIC_CMD, os.fspath(file), "--rc-prefix", "-o", py_file]
|
||||
if file.suffix == ".qrc": # Qt resources
|
||||
if not output_path:
|
||||
py_file = f"{file.parent}/rc_{file.stem}.py"
|
||||
else:
|
||||
py_file = str(output_path.resolve())
|
||||
return [Path(py_file)], [RCC_CMD, os.fspath(file), "-o", py_file]
|
||||
# generate .qmltypes from sources with Qml decorators
|
||||
if file.suffix == ".py" and file in self._qml_module_sources:
|
||||
assert self._qml_module_dir
|
||||
qml_module_dir = os.fspath(self._qml_module_dir)
|
||||
json_file = f"{qml_module_dir}/{file.stem}{METATYPES_JSON_SUFFIX}"
|
||||
return [Path(json_file)], [MOD_CMD, "-o", json_file, os.fspath(file)]
|
||||
# Run qmltyperegistrar
|
||||
if file.name.endswith(METATYPES_JSON_SUFFIX):
|
||||
assert self._qml_module_dir
|
||||
stem = file.name[: len(file.name) - len(METATYPES_JSON_SUFFIX)]
|
||||
qmltypes_file = self._qml_module_dir / f"{stem}.qmltypes"
|
||||
cpp_file = self._qml_module_dir / f"{stem}_qmltyperegistrations.cpp"
|
||||
cmd = [QMLTYPEREGISTRAR_CMD, "--generate-qmltypes",
|
||||
os.fspath(qmltypes_file), "-o", os.fspath(cpp_file),
|
||||
os.fspath(file)]
|
||||
cmd.extend(self._qml_project_data.registrar_options())
|
||||
return [qmltypes_file, cpp_file], cmd
|
||||
|
||||
if file.name.endswith(TRANSLATION_SUFFIX):
|
||||
qm_file = f"{file.parent}/{file.stem}.qm"
|
||||
cmd = [LRELEASE_CMD, os.fspath(file), "-qm", qm_file]
|
||||
return [Path(qm_file)], cmd
|
||||
|
||||
if file.suffix in SHADER_SUFFIXES:
|
||||
qsb_file = f"{file.parent}/{file.stem}.qsb"
|
||||
cmd = [QSB_CMD, "-o", qsb_file, os.fspath(file)]
|
||||
return [Path(qsb_file)], cmd
|
||||
|
||||
return [], None
|
||||
|
||||
def _regenerate_qmldir(self):
|
||||
"""Regenerate the 'qmldir' file."""
|
||||
if self.cl_options.dry_run or not self._qml_dir_file:
|
||||
return
|
||||
if self.cl_options.force or requires_rebuild(self._qml_module_sources, self._qml_dir_file):
|
||||
with self._qml_dir_file.open("w") as qf:
|
||||
qf.write(f"module {self._qml_project_data.import_name}\n")
|
||||
for f in self._qml_module_dir.glob("*.qmltypes"):
|
||||
qf.write(f"typeinfo {f.name}\n")
|
||||
|
||||
def _build_file(self, source: Path, output_path: Path | None = None):
|
||||
"""Build an artifact if necessary."""
|
||||
artifacts, command = self._get_artifacts(source, output_path)
|
||||
for artifact in artifacts:
|
||||
if self.cl_options.force or requires_rebuild([source], artifact):
|
||||
run_command(command, cwd=self.project.project_file.parent)
|
||||
self._build_file(artifact) # Recurse for QML (json->qmltypes)
|
||||
|
||||
def build_design_studio_resources(self):
|
||||
"""
|
||||
The resources that need to be compiled are defined in autogen/settings.py
|
||||
"""
|
||||
ds_project = DesignStudioProject(self.project.main_file)
|
||||
if (resources_file_path := ds_project.get_resource_file_path()) is None:
|
||||
return
|
||||
|
||||
compiled_resources_file_path = ds_project.get_compiled_resources_file_path()
|
||||
self._build_file(resources_file_path, compiled_resources_file_path)
|
||||
|
||||
def build(self):
|
||||
"""Build the whole project"""
|
||||
for sub_project_file in self.project.sub_projects_files:
|
||||
Project(project_file=sub_project_file).build()
|
||||
|
||||
if self._qml_module_dir:
|
||||
self._qml_module_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
for file in _sort_sources(self.project.files):
|
||||
self._build_file(file)
|
||||
|
||||
if DesignStudioProject.is_ds_project(self.project.main_file):
|
||||
self.build_design_studio_resources()
|
||||
|
||||
self._regenerate_qmldir()
|
||||
|
||||
def run(self) -> int:
|
||||
"""Runs the project"""
|
||||
self.build()
|
||||
cmd = [sys.executable, str(self.project.main_file)]
|
||||
return run_command(cmd, cwd=self.project.project_file.parent)
|
||||
|
||||
def _clean_file(self, source: Path):
|
||||
"""Clean an artifact."""
|
||||
artifacts, command = self._get_artifacts(source)
|
||||
for artifact in artifacts:
|
||||
remove_path(artifact)
|
||||
self._clean_file(artifact) # Recurse for QML (json->qmltypes)
|
||||
|
||||
def clean(self):
|
||||
"""Clean build artifacts."""
|
||||
for sub_project_file in self.project.sub_projects_files:
|
||||
Project(project_file=sub_project_file).clean()
|
||||
for file in self.project.files:
|
||||
self._clean_file(file)
|
||||
if self._qml_module_dir and self._qml_module_dir.is_dir():
|
||||
remove_path(self._qml_module_dir)
|
||||
# In case of a dir hierarchy ("a.b" -> a/b), determine and delete
|
||||
# the root directory
|
||||
if self._qml_module_dir.parent != self.project.project_file.parent:
|
||||
project_dir_parts = len(self.project.project_file.parent.parts)
|
||||
first_module_dir = self._qml_module_dir.parts[project_dir_parts]
|
||||
remove_path(self.project.project_file.parent / first_module_dir)
|
||||
|
||||
if DesignStudioProject.is_ds_project(self.project.main_file):
|
||||
DesignStudioProject(self.project.main_file).clean()
|
||||
|
||||
def _qmllint(self):
|
||||
"""Helper for running qmllint on .qml files (non-recursive)."""
|
||||
if not self.project.qml_files:
|
||||
print(f"{self.project.project_file.name}: No QML files found", file=sys.stderr)
|
||||
return
|
||||
|
||||
cmd = [QMLLINT_CMD]
|
||||
if self._qml_dir_file:
|
||||
cmd.extend(["-i", os.fspath(self._qml_dir_file)])
|
||||
for f in self.project.qml_files:
|
||||
cmd.append(os.fspath(f))
|
||||
run_command(cmd, cwd=self.project.project_file.parent, ignore_fail=True)
|
||||
|
||||
def qmllint(self):
|
||||
"""Run qmllint on .qml files."""
|
||||
self.build()
|
||||
for sub_project_file in self.project.sub_projects_files:
|
||||
Project(project_file=sub_project_file)._qmllint()
|
||||
self._qmllint()
|
||||
|
||||
def deploy(self):
|
||||
"""Deploys the application"""
|
||||
cmd = [DEPLOY_CMD]
|
||||
cmd.extend([str(self.project.main_file), "-f"])
|
||||
run_command(cmd, cwd=self.project.project_file.parent)
|
||||
|
||||
def lupdate(self):
|
||||
for sub_project_file in self.project.sub_projects_files:
|
||||
Project(project_file=sub_project_file).lupdate()
|
||||
|
||||
if not self.project.ts_files:
|
||||
print(f"{self.project.project_file.name}: No .ts file found.",
|
||||
file=sys.stderr)
|
||||
return
|
||||
|
||||
source_files = self.project.python_files + self.project.ui_files
|
||||
project_dir = self.project.project_file.parent
|
||||
cmd_prefix = [LUPDATE_CMD] + [os.fspath(p.relative_to(project_dir)) for p in source_files]
|
||||
cmd_prefix.append("-ts")
|
||||
for ts_file in self.project.ts_files:
|
||||
ts_dir = ts_file.parent
|
||||
if not ts_dir.exists():
|
||||
ts_dir.mkdir(parents=True, exist_ok=True)
|
||||
if requires_rebuild(source_files, ts_file):
|
||||
cmd = cmd_prefix
|
||||
cmd.append(os.fspath(ts_file))
|
||||
run_command(cmd, cwd=project_dir)
|
||||
|
||||
|
||||
def main(mode: str = None, dry_run: bool = False, quiet: bool = False, force: bool = False,
|
||||
qml_module: bool = None, project_dir: str = None, project_path: str = None,
|
||||
legacy_pyproject: bool = False):
|
||||
cl_options = ClOptions(dry_run=dry_run, quiet=quiet, # noqa: F841
|
||||
force=force, qml_module=qml_module)
|
||||
|
||||
if new_project_type := NewProjectTypes.find_by_command(mode):
|
||||
if not project_dir:
|
||||
print(f"Error creating new project: {mode} requires a directory name or path",
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
project_dir = Path(project_dir)
|
||||
try:
|
||||
project_dir.resolve()
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
print("Invalid project name", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(new_project(project_dir, new_project_type, legacy_pyproject))
|
||||
|
||||
if mode == "migrate-pyproject":
|
||||
sys.exit(migrate_pyproject(project_path))
|
||||
|
||||
try:
|
||||
project_file = resolve_valid_project_file(project_path)
|
||||
except ValueError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
project = Project(project_file)
|
||||
if mode == "build":
|
||||
project.build()
|
||||
elif mode == "run":
|
||||
sys.exit(project.run())
|
||||
elif mode == "clean":
|
||||
project.clean()
|
||||
elif mode == "qmllint":
|
||||
project.qmllint()
|
||||
elif mode == "deploy":
|
||||
project.deploy()
|
||||
elif mode == "lupdate":
|
||||
project.lupdate()
|
||||
else:
|
||||
print(f"Invalid mode {mode}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = ArgumentParser(description=DESCRIPTION, formatter_class=RawTextHelpFormatter)
|
||||
parser.add_argument("--quiet", "-q", action="store_true", help="Quiet")
|
||||
parser.add_argument("--dry-run", "-n", action="store_true", help="Only print commands")
|
||||
parser.add_argument("--force", "-f", action="store_true", help="Force rebuild")
|
||||
parser.add_argument("--qml-module", "-Q", action="store_true",
|
||||
help="Perform check for QML module")
|
||||
|
||||
# Create subparsers for the two different command branches
|
||||
subparsers = parser.add_subparsers(dest='mode', required=True)
|
||||
|
||||
# Add subparser for project creation commands
|
||||
for project_type in NewProjectTypes:
|
||||
new_parser = subparsers.add_parser(project_type.value.command,
|
||||
help=project_type.value.description)
|
||||
new_parser.add_argument(
|
||||
"project_dir", help="Name or location of the new project", nargs="?", type=str)
|
||||
|
||||
new_parser.add_argument(
|
||||
"--legacy-pyproject", action="store_true", help="Create a legacy *.pyproject file")
|
||||
|
||||
# Add subparser for project operation commands
|
||||
for op_mode, op_help in OPERATION_HELP.items():
|
||||
op_parser = subparsers.add_parser(op_mode, help=op_help)
|
||||
op_parser.add_argument("project_path", nargs="?", type=str, help="Path to the project file")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.mode, args.dry_run, args.quiet, args.force, args.qml_module,
|
||||
getattr(args, "project_dir", None), getattr(args, "project_path", None),
|
||||
getattr(args, "legacy_pyproject", None))
|
||||
@@ -0,0 +1,53 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
QTPATHS_CMD = "qtpaths6"
|
||||
MOD_CMD = "pyside6-metaobjectdump"
|
||||
|
||||
PYPROJECT_TOML_PATTERN = "pyproject.toml"
|
||||
PYPROJECT_JSON_PATTERN = "*.pyproject"
|
||||
# Note that the order is important, as the first pattern that matches is used
|
||||
PYPROJECT_FILE_PATTERNS = [PYPROJECT_TOML_PATTERN, PYPROJECT_JSON_PATTERN]
|
||||
QMLDIR_FILE = "qmldir"
|
||||
|
||||
QML_IMPORT_NAME = "QML_IMPORT_NAME"
|
||||
QML_IMPORT_MAJOR_VERSION = "QML_IMPORT_MAJOR_VERSION"
|
||||
QML_IMPORT_MINOR_VERSION = "QML_IMPORT_MINOR_VERSION"
|
||||
QT_MODULES = "QT_MODULES"
|
||||
|
||||
METATYPES_JSON_SUFFIX = "metatypes.json"
|
||||
TRANSLATION_SUFFIX = ".ts"
|
||||
SHADER_SUFFIXES = ".vert", ".frag"
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super().__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClOptions(metaclass=Singleton):
|
||||
"""
|
||||
Dataclass to store the cl options that needs to be passed as arguments.
|
||||
"""
|
||||
dry_run: bool
|
||||
quiet: bool
|
||||
force: bool
|
||||
qml_module: bool
|
||||
|
||||
|
||||
from .utils import (run_command, requires_rebuild, remove_path, package_dir, qtpaths,
|
||||
qt_metatype_json_dir, resolve_valid_project_file)
|
||||
from .project_data import (is_python_file, ProjectData, QmlProjectData,
|
||||
check_qml_decorators)
|
||||
from .newproject import new_project, NewProjectTypes
|
||||
from .design_studio_project import DesignStudioProject
|
||||
from .pyproject_toml import parse_pyproject_toml, write_pyproject_toml, migrate_pyproject
|
||||
from .pyproject_json import parse_pyproject_json
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
# 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
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class DesignStudioProject:
|
||||
"""
|
||||
Class to handle Design Studio projects. The project structure is as follows:
|
||||
- Python folder
|
||||
- autogen folder
|
||||
- settings.py
|
||||
- resources.py (Compiled resources)
|
||||
- main.py
|
||||
<ProjectName>.qrc (Resources collection file)
|
||||
<ProjectName>.qmlproject
|
||||
<ProjectName>.qmlproject.qtds (should be added to .gitignore)
|
||||
... Other files and folders ...
|
||||
"""
|
||||
|
||||
def __init__(self, main_file: Path):
|
||||
self.main_file = main_file
|
||||
self.project_dir = main_file.parent.parent
|
||||
self.compiled_resources_file = self.main_file.parent / "autogen" / "resources.py"
|
||||
|
||||
@staticmethod
|
||||
def is_ds_project(main_file: Path) -> bool:
|
||||
return bool(*main_file.parent.parent.glob("*.qmlproject"))
|
||||
|
||||
def compiled_resources_available(self) -> bool:
|
||||
"""
|
||||
Returns whether the resources of the project have been compiled into a .py file.
|
||||
TODO: Make the resources path configurable. Wait for the pyproject TOML configuration
|
||||
"""
|
||||
return self.compiled_resources_file.exists()
|
||||
|
||||
def get_resource_file_path(self) -> Optional[Path]:
|
||||
"""
|
||||
Return the path to the *.qrc resources file from the project root folder.
|
||||
If not found, log an error message and return None
|
||||
If multiple files are found, log an error message and return None
|
||||
If a single file is found, return its path
|
||||
"""
|
||||
resource_files = list(self.project_dir.glob("*.qrc"))
|
||||
if not resource_files:
|
||||
logging.error("No *.qrc resources file found in the project root folder")
|
||||
return None
|
||||
if len(resource_files) > 1:
|
||||
logging.error("Multiple *.qrc resources files found in the project root folder")
|
||||
return None
|
||||
return resource_files[0]
|
||||
|
||||
def get_compiled_resources_file_path(self) -> Path:
|
||||
"""
|
||||
Return the path of the output file generated by compiling the *.qrc resources file
|
||||
"""
|
||||
# TODO: make this more robust and configurable. Wait for the pyproject TOML configuration
|
||||
return self.main_file.parent / "autogen" / "resources.py"
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Remove the compiled resources file if it exists
|
||||
"""
|
||||
self.compiled_resources_file.unlink(missing_ok=True)
|
||||
@@ -0,0 +1,189 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from .pyproject_toml import write_pyproject_toml
|
||||
from .pyproject_json import write_pyproject_json
|
||||
|
||||
"""New project generation code."""
|
||||
|
||||
_WIDGET_MAIN = """if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
"""
|
||||
|
||||
_WIDGET_IMPORTS = """import sys
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow
|
||||
"""
|
||||
|
||||
_WIDGET_CLASS_DEFINITION = """class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
"""
|
||||
|
||||
_WIDGET_SETUP_UI_CODE = """ self._ui = Ui_MainWindow()
|
||||
self._ui.setupUi(self)
|
||||
"""
|
||||
|
||||
_MAINWINDOW_FORM = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget"/>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QStatusBar" name="statusbar"/>
|
||||
</widget>
|
||||
</ui>
|
||||
"""
|
||||
|
||||
_QUICK_FORM = """import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
ApplicationWindow {
|
||||
id: window
|
||||
width: 1024
|
||||
height: 600
|
||||
visible: true
|
||||
}
|
||||
"""
|
||||
|
||||
_QUICK_MAIN = """import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtGui import QGuiApplication
|
||||
from PySide6.QtCore import QUrl
|
||||
from PySide6.QtQml import QQmlApplicationEngine
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QGuiApplication()
|
||||
engine = QQmlApplicationEngine()
|
||||
qml_file = Path(__file__).parent / 'main.qml'
|
||||
engine.load(QUrl.fromLocalFile(qml_file))
|
||||
if not engine.rootObjects():
|
||||
sys.exit(-1)
|
||||
exit_code = app.exec()
|
||||
del engine
|
||||
sys.exit(exit_code)
|
||||
"""
|
||||
|
||||
NewProjectFiles = list[tuple[str, str]] # tuple of (filename, contents).
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NewProjectType:
|
||||
command: str
|
||||
description: str
|
||||
files: NewProjectFiles
|
||||
|
||||
|
||||
def _write_project(directory: Path, files: NewProjectFiles, legacy_pyproject: bool):
|
||||
"""
|
||||
Create the project files in the specified directory.
|
||||
|
||||
:param directory: The directory to create the project in.
|
||||
:param files: The files that belong to the project to create.
|
||||
"""
|
||||
file_names = []
|
||||
for file_name, contents in files:
|
||||
(directory / file_name).write_text(contents)
|
||||
print(f"Wrote {directory.name}{os.sep}{file_name}.")
|
||||
file_names.append(file_name)
|
||||
|
||||
if legacy_pyproject:
|
||||
pyproject_file = directory / f"{directory.name}.pyproject"
|
||||
write_pyproject_json(pyproject_file, file_names)
|
||||
else:
|
||||
pyproject_file = directory / "pyproject.toml"
|
||||
write_pyproject_toml(pyproject_file, directory.name, file_names)
|
||||
print(f"Wrote {pyproject_file}.")
|
||||
|
||||
|
||||
def _widget_project() -> NewProjectFiles:
|
||||
"""Create a (form-less) widgets project."""
|
||||
main_py = (_WIDGET_IMPORTS + "\n\n" + _WIDGET_CLASS_DEFINITION + "\n\n"
|
||||
+ _WIDGET_MAIN)
|
||||
return [("main.py", main_py)]
|
||||
|
||||
|
||||
def _ui_form_project() -> NewProjectFiles:
|
||||
"""Create a Qt Designer .ui form based widgets project."""
|
||||
main_py = (_WIDGET_IMPORTS
|
||||
+ "\nfrom ui_mainwindow import Ui_MainWindow\n\n\n"
|
||||
+ _WIDGET_CLASS_DEFINITION + _WIDGET_SETUP_UI_CODE
|
||||
+ "\n\n" + _WIDGET_MAIN)
|
||||
return [("main.py", main_py),
|
||||
("mainwindow.ui", _MAINWINDOW_FORM)]
|
||||
|
||||
|
||||
def _qml_project() -> NewProjectFiles:
|
||||
"""Create a QML project."""
|
||||
return [("main.py", _QUICK_MAIN),
|
||||
("main.qml", _QUICK_FORM)]
|
||||
|
||||
|
||||
class NewProjectTypes(Enum):
|
||||
QUICK = NewProjectType("new-quick", "Create a new Qt Quick project", _qml_project())
|
||||
WIDGET_FORM = NewProjectType("new-ui", "Create a new Qt Widgets Form project",
|
||||
_ui_form_project())
|
||||
WIDGET = NewProjectType("new-widget", "Create a new Qt Widgets project", _widget_project())
|
||||
|
||||
@staticmethod
|
||||
def find_by_command(command: str) -> NewProjectType | None:
|
||||
return next((pt.value for pt in NewProjectTypes if pt.value.command == command), None)
|
||||
|
||||
|
||||
def new_project(
|
||||
project_dir: Path, project_type: NewProjectType, legacy_pyproject: bool
|
||||
) -> int:
|
||||
"""
|
||||
Create a new project at the specified project_dir directory.
|
||||
|
||||
:param project_dir: The directory path to create the project. If existing, must be empty.
|
||||
:param project_type: The Qt type of project to create (Qt Widgets, Qt Quick, etc.)
|
||||
|
||||
:return: 0 if the project was created successfully, otherwise 1.
|
||||
"""
|
||||
if any(project_dir.iterdir()):
|
||||
print(f"Can not create project at {project_dir}: directory is not empty.", file=sys.stderr)
|
||||
return 1
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
_write_project(project_dir, project_type.files, legacy_pyproject)
|
||||
except Exception as e:
|
||||
print(f"Error creating project file: {str(e)}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if project_type == NewProjectTypes.WIDGET_FORM:
|
||||
print(f'Run "pyside6-project build {project_dir}" to build the project')
|
||||
print(f'Run "pyside6-project run {project_dir / "main.py"}" to run the project')
|
||||
return 0
|
||||
@@ -0,0 +1,259 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from . import (METATYPES_JSON_SUFFIX, PYPROJECT_JSON_PATTERN, PYPROJECT_TOML_PATTERN,
|
||||
PYPROJECT_FILE_PATTERNS, TRANSLATION_SUFFIX, qt_metatype_json_dir, MOD_CMD,
|
||||
QML_IMPORT_MAJOR_VERSION, QML_IMPORT_MINOR_VERSION, QML_IMPORT_NAME, QT_MODULES)
|
||||
from .pyproject_toml import parse_pyproject_toml
|
||||
from .pyproject_json import parse_pyproject_json
|
||||
|
||||
|
||||
def is_python_file(file: Path) -> bool:
|
||||
return (file.suffix == ".py"
|
||||
or sys.platform == "win32" and file.suffix == ".pyw")
|
||||
|
||||
|
||||
class ProjectData:
|
||||
def __init__(self, project_file: Path) -> None:
|
||||
"""Parse the project file."""
|
||||
self._project_file = project_file.resolve()
|
||||
self._sub_projects_files: list[Path] = []
|
||||
|
||||
# All sources except subprojects
|
||||
self._files: list[Path] = []
|
||||
# QML files
|
||||
self._qml_files: list[Path] = []
|
||||
# Python files
|
||||
self.main_file: Path = None
|
||||
self._python_files: list[Path] = []
|
||||
# ui files
|
||||
self._ui_files: list[Path] = []
|
||||
# qrc files
|
||||
self._qrc_files: list[Path] = []
|
||||
# ts files
|
||||
self._ts_files: list[Path] = []
|
||||
|
||||
if project_file.match(PYPROJECT_JSON_PATTERN):
|
||||
project_file_data = parse_pyproject_json(project_file)
|
||||
elif project_file.match(PYPROJECT_TOML_PATTERN):
|
||||
project_file_data = parse_pyproject_toml(project_file)
|
||||
else:
|
||||
print(f"Unknown project file format: {project_file}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if project_file_data.errors:
|
||||
print(f"Invalid project file: {project_file}. Errors found:", file=sys.stderr)
|
||||
for error in project_file_data.errors:
|
||||
print(f"{error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for f in project_file_data.files:
|
||||
file = Path(project_file.parent / f)
|
||||
if any(file.match(pattern) for pattern in PYPROJECT_FILE_PATTERNS):
|
||||
self._sub_projects_files.append(file)
|
||||
continue
|
||||
|
||||
self._files.append(file)
|
||||
if file.suffix == ".qml":
|
||||
self._qml_files.append(file)
|
||||
elif is_python_file(file):
|
||||
if file.stem == "main":
|
||||
self.main_file = file
|
||||
self._python_files.append(file)
|
||||
elif file.suffix == ".ui":
|
||||
self._ui_files.append(file)
|
||||
elif file.suffix == ".qrc":
|
||||
self._qrc_files.append(file)
|
||||
elif file.suffix == TRANSLATION_SUFFIX:
|
||||
self._ts_files.append(file)
|
||||
|
||||
if not self.main_file:
|
||||
self._find_main_file()
|
||||
|
||||
@property
|
||||
def project_file(self):
|
||||
return self._project_file
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
return self._files
|
||||
|
||||
@property
|
||||
def main_file(self):
|
||||
return self._main_file
|
||||
|
||||
@main_file.setter
|
||||
def main_file(self, main_file):
|
||||
self._main_file = main_file
|
||||
|
||||
@property
|
||||
def python_files(self):
|
||||
return self._python_files
|
||||
|
||||
@property
|
||||
def ui_files(self):
|
||||
return self._ui_files
|
||||
|
||||
@property
|
||||
def qrc_files(self):
|
||||
return self._qrc_files
|
||||
|
||||
@property
|
||||
def qml_files(self):
|
||||
return self._qml_files
|
||||
|
||||
@property
|
||||
def ts_files(self):
|
||||
return self._ts_files
|
||||
|
||||
@property
|
||||
def sub_projects_files(self):
|
||||
return self._sub_projects_files
|
||||
|
||||
def _find_main_file(self) -> str:
|
||||
"""Find the entry point file containing the main function"""
|
||||
|
||||
def is_main(file):
|
||||
return "__main__" in file.read_text(encoding="utf-8")
|
||||
|
||||
if not self.main_file:
|
||||
for python_file in self.python_files:
|
||||
if is_main(python_file):
|
||||
self.main_file = python_file
|
||||
return str(python_file)
|
||||
|
||||
# __main__ not found
|
||||
print(
|
||||
f"Python file with main function not found. Add the file to {self.project_file}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class QmlProjectData:
|
||||
"""QML relevant project data."""
|
||||
|
||||
def __init__(self):
|
||||
self._import_name: str = ""
|
||||
self._import_major_version: int = 0
|
||||
self._import_minor_version: int = 0
|
||||
self._qt_modules: list[str] = []
|
||||
|
||||
def registrar_options(self):
|
||||
result = [
|
||||
"--import-name",
|
||||
self._import_name,
|
||||
"--major-version",
|
||||
str(self._import_major_version),
|
||||
"--minor-version",
|
||||
str(self._import_minor_version),
|
||||
]
|
||||
if self._qt_modules:
|
||||
# Add Qt modules as foreign types
|
||||
foreign_files: list[str] = []
|
||||
meta_dir = qt_metatype_json_dir()
|
||||
for mod in self._qt_modules:
|
||||
mod_id = mod[2:].lower()
|
||||
pattern = f"qt6{mod_id}_*"
|
||||
if sys.platform != "win32":
|
||||
pattern += "_" # qt6core_debug_metatypes.json (Linux)
|
||||
pattern += METATYPES_JSON_SUFFIX
|
||||
for f in meta_dir.glob(pattern):
|
||||
foreign_files.append(os.fspath(f))
|
||||
break
|
||||
if foreign_files:
|
||||
foreign_files_str = ",".join(foreign_files)
|
||||
result.append(f"--foreign-types={foreign_files_str}")
|
||||
return result
|
||||
|
||||
@property
|
||||
def import_name(self):
|
||||
return self._import_name
|
||||
|
||||
@import_name.setter
|
||||
def import_name(self, n):
|
||||
self._import_name = n
|
||||
|
||||
@property
|
||||
def import_major_version(self):
|
||||
return self._import_major_version
|
||||
|
||||
@import_major_version.setter
|
||||
def import_major_version(self, v):
|
||||
self._import_major_version = v
|
||||
|
||||
@property
|
||||
def import_minor_version(self):
|
||||
return self._import_minor_version
|
||||
|
||||
@import_minor_version.setter
|
||||
def import_minor_version(self, v):
|
||||
self._import_minor_version = v
|
||||
|
||||
@property
|
||||
def qt_modules(self):
|
||||
return self._qt_modules
|
||||
|
||||
@qt_modules.setter
|
||||
def qt_modules(self, v):
|
||||
self._qt_modules = v
|
||||
|
||||
def __str__(self) -> str:
|
||||
vmaj = self._import_major_version
|
||||
vmin = self._import_minor_version
|
||||
return f'"{self._import_name}" v{vmaj}.{vmin}'
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return len(self._import_name) > 0 and self._import_major_version > 0
|
||||
|
||||
|
||||
def _has_qml_decorated_class(class_list: list) -> bool:
|
||||
"""Check for QML-decorated classes in the moc json output."""
|
||||
for d in class_list:
|
||||
class_infos = d.get("classInfos")
|
||||
if class_infos:
|
||||
for e in class_infos:
|
||||
if "QML" in e["name"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def check_qml_decorators(py_file: Path) -> tuple[bool, QmlProjectData]:
|
||||
"""Check if a Python file has QML-decorated classes by running a moc check
|
||||
and return whether a class was found and the QML data."""
|
||||
data = None
|
||||
try:
|
||||
cmd = [MOD_CMD, "--quiet", os.fspath(py_file)]
|
||||
with subprocess.Popen(cmd, stdout=subprocess.PIPE) as proc:
|
||||
data = json.load(proc.stdout)
|
||||
proc.wait()
|
||||
except Exception as e:
|
||||
t = type(e).__name__
|
||||
print(f"{t}: running {MOD_CMD} on {py_file}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
qml_project_data = QmlProjectData()
|
||||
if not data:
|
||||
return (False, qml_project_data) # No classes in file
|
||||
|
||||
first = data[0]
|
||||
class_list = first["classes"]
|
||||
has_class = _has_qml_decorated_class(class_list)
|
||||
if has_class:
|
||||
v = first.get(QML_IMPORT_NAME)
|
||||
if v:
|
||||
qml_project_data.import_name = v
|
||||
v = first.get(QML_IMPORT_MAJOR_VERSION)
|
||||
if v:
|
||||
qml_project_data.import_major_version = v
|
||||
qml_project_data.import_minor_version = first.get(QML_IMPORT_MINOR_VERSION)
|
||||
v = first.get(QT_MODULES)
|
||||
if v:
|
||||
qml_project_data.qt_modules = v
|
||||
return (has_class, qml_project_data)
|
||||
@@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2025 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from .pyproject_parse_result import PyProjectParseResult
|
||||
|
||||
|
||||
def write_pyproject_json(pyproject_file: Path, project_files: list[str]):
|
||||
"""
|
||||
Create or update a *.pyproject file with the specified content.
|
||||
|
||||
:param pyproject_file: The *.pyproject file path to create or update.
|
||||
:param project_files: The relative paths of the files to include in the project.
|
||||
"""
|
||||
# The content of the file is fully replaced, so it is not necessary to read and merge any
|
||||
# existing content
|
||||
content = {
|
||||
"files": sorted(project_files),
|
||||
}
|
||||
pyproject_file.write_text(json.dumps(content), encoding="utf-8")
|
||||
|
||||
|
||||
def parse_pyproject_json(pyproject_json_file: Path) -> PyProjectParseResult:
|
||||
"""
|
||||
Parse a pyproject.json file and return a PyProjectParseResult object.
|
||||
"""
|
||||
result = PyProjectParseResult()
|
||||
try:
|
||||
with pyproject_json_file.open("r") as pyf:
|
||||
project_file_data = json.load(pyf)
|
||||
except json.JSONDecodeError as e:
|
||||
result.errors.append(str(e))
|
||||
return result
|
||||
except Exception as e:
|
||||
result.errors.append(str(e))
|
||||
return result
|
||||
|
||||
if not isinstance(project_file_data, dict):
|
||||
result.errors.append("The root element of pyproject.json must be a JSON object")
|
||||
return result
|
||||
|
||||
found_files = project_file_data.get("files")
|
||||
if found_files and not isinstance(found_files, list):
|
||||
result.errors.append("The files element must be a list")
|
||||
return result
|
||||
|
||||
for file in project_file_data.get("files", []):
|
||||
if not isinstance(file, str):
|
||||
result.errors.append(f"Invalid file: {file}")
|
||||
return result
|
||||
|
||||
file_path = Path(file)
|
||||
if not file_path.is_absolute():
|
||||
file_path = (pyproject_json_file.parent / file).resolve()
|
||||
result.files.append(file_path)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,10 @@
|
||||
# Copyright (C) 2025 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class PyProjectParseResult:
|
||||
errors: list[str] = field(default_factory=list)
|
||||
files: list[Path] = field(default_factory=list)
|
||||
@@ -0,0 +1,275 @@
|
||||
# Copyright (C) 2025 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 __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
# TODO: Remove this import when Python 3.11 is the minimum supported version
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
from . import PYPROJECT_JSON_PATTERN
|
||||
from .pyproject_parse_result import PyProjectParseResult
|
||||
from .pyproject_json import parse_pyproject_json
|
||||
|
||||
|
||||
def _parse_toml_content(content: str) -> dict:
|
||||
"""
|
||||
Parse TOML content for project name and files list only.
|
||||
"""
|
||||
result = {"project": {}, "tool": {"pyside6-project": {}}}
|
||||
current_section = None
|
||||
|
||||
for line in content.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
if line == '[project]':
|
||||
current_section = 'project'
|
||||
elif line == '[tool.pyside6-project]':
|
||||
current_section = 'tool.pyside6-project'
|
||||
elif '=' in line and current_section:
|
||||
key, value = [part.strip() for part in line.split('=', 1)]
|
||||
|
||||
# Handle string values - name of the project
|
||||
if value.startswith('"') and value.endswith('"'):
|
||||
value = value[1:-1]
|
||||
# Handle array of strings - files names
|
||||
elif value.startswith('[') and value.endswith(']'):
|
||||
items = value[1:-1].split(',')
|
||||
value = [item.strip().strip('"') for item in items if item.strip()]
|
||||
|
||||
if current_section == 'project':
|
||||
result['project'][key] = value
|
||||
else: # tool.pyside6-project
|
||||
result['tool']['pyside6-project'][key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _write_base_toml_content(data: dict) -> str:
|
||||
"""
|
||||
Write minimal TOML content with project and tool.pyside6-project sections.
|
||||
"""
|
||||
lines = []
|
||||
|
||||
if data.get('project'):
|
||||
lines.append('[project]')
|
||||
for key, value in sorted(data['project'].items()):
|
||||
if isinstance(value, str):
|
||||
lines.append(f'{key} = "{value}"')
|
||||
|
||||
if data.get("tool") and data['tool'].get('pyside6-project'):
|
||||
lines.append('\n[tool.pyside6-project]')
|
||||
for key, value in sorted(data['tool']['pyside6-project'].items()):
|
||||
if isinstance(value, list):
|
||||
items = [f'"{item}"' for item in sorted(value)]
|
||||
lines.append(f'{key} = [{", ".join(items)}]')
|
||||
else:
|
||||
lines.append(f'{key} = "{value}"')
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def parse_pyproject_toml(pyproject_toml_file: Path) -> PyProjectParseResult:
|
||||
"""
|
||||
Parse a pyproject.toml file and return a PyProjectParseResult object.
|
||||
"""
|
||||
result = PyProjectParseResult()
|
||||
|
||||
try:
|
||||
content = pyproject_toml_file.read_text(encoding='utf-8')
|
||||
# TODO: Remove the manual parsing when Python 3.11 is the minimum supported version
|
||||
if sys.version_info >= (3, 11):
|
||||
root_table = tomllib.loads(content) # Use tomllib for Python >= 3.11
|
||||
print("Using tomllib for parsing TOML content")
|
||||
else:
|
||||
root_table = _parse_toml_content(content) # Fallback to manual parsing
|
||||
except Exception as e:
|
||||
result.errors.append(str(e))
|
||||
return result
|
||||
|
||||
pyside_table = root_table.get("tool", {}).get("pyside6-project", {})
|
||||
if not pyside_table:
|
||||
result.errors.append("Missing [tool.pyside6-project] table")
|
||||
return result
|
||||
|
||||
files = pyside_table.get("files", [])
|
||||
if not isinstance(files, list):
|
||||
result.errors.append("Missing or invalid files list")
|
||||
return result
|
||||
|
||||
# Convert paths
|
||||
for file in files:
|
||||
if not isinstance(file, str):
|
||||
result.errors.append(f"Invalid file: {file}")
|
||||
return result
|
||||
file_path = Path(file)
|
||||
if not file_path.is_absolute():
|
||||
file_path = (pyproject_toml_file.parent / file).resolve()
|
||||
result.files.append(file_path)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def write_pyproject_toml(pyproject_file: Path, project_name: str, project_files: list[str]):
|
||||
"""
|
||||
Create or overwrite a pyproject.toml file with the specified content.
|
||||
"""
|
||||
data = {
|
||||
"project": {"name": project_name},
|
||||
"tool": {
|
||||
"pyside6-project": {"files": sorted(project_files)}
|
||||
}
|
||||
}
|
||||
|
||||
content = _write_base_toml_content(data)
|
||||
try:
|
||||
pyproject_file.write_text(content, encoding='utf-8')
|
||||
except Exception as e:
|
||||
raise ValueError(f"Error writing TOML file: {str(e)}")
|
||||
|
||||
|
||||
def robust_relative_to_posix(target_path: Path, base_path: Path) -> str:
|
||||
"""
|
||||
Calculates the relative path from base_path to target_path.
|
||||
Uses Path.relative_to first, falls back to os.path.relpath if it fails.
|
||||
Returns the result as a POSIX path string.
|
||||
"""
|
||||
# Ensure both paths are absolute for reliable calculation, although in this specific code,
|
||||
# project_folder and paths in output_files are expected to be resolved/absolute already.
|
||||
abs_target = target_path.resolve() if not target_path.is_absolute() else target_path
|
||||
abs_base = base_path.resolve() if not base_path.is_absolute() else base_path
|
||||
|
||||
try:
|
||||
return abs_target.relative_to(abs_base).as_posix()
|
||||
except ValueError:
|
||||
# Fallback to os.path.relpath which is more robust for paths that are not direct subpaths.
|
||||
relative_str = os.path.relpath(str(abs_target), str(abs_base))
|
||||
# Convert back to Path temporarily to get POSIX format
|
||||
return Path(relative_str).as_posix()
|
||||
|
||||
|
||||
def migrate_pyproject(pyproject_file: Path | str = None) -> int:
|
||||
"""
|
||||
Migrate a project *.pyproject JSON file to the new pyproject.toml format.
|
||||
|
||||
The containing subprojects are migrated recursively.
|
||||
|
||||
:return: 0 if successful, 1 if an error occurred.
|
||||
"""
|
||||
project_name = None
|
||||
|
||||
# Transform the user input string into a Path object
|
||||
if isinstance(pyproject_file, str):
|
||||
pyproject_file = Path(pyproject_file)
|
||||
|
||||
if pyproject_file:
|
||||
if not pyproject_file.match(PYPROJECT_JSON_PATTERN):
|
||||
print(f"Cannot migrate non \"{PYPROJECT_JSON_PATTERN}\" file:", file=sys.stderr)
|
||||
print(f"\"{pyproject_file}\"", file=sys.stderr)
|
||||
return 1
|
||||
project_files = [pyproject_file]
|
||||
project_name = pyproject_file.stem
|
||||
else:
|
||||
# Get the existing *.pyproject files in the current directory
|
||||
project_files = list(Path().glob(PYPROJECT_JSON_PATTERN))
|
||||
if not project_files:
|
||||
print(f"No project file found in the current directory: {Path()}", file=sys.stderr)
|
||||
return 1
|
||||
if len(project_files) > 1:
|
||||
print("Multiple pyproject files found in the project folder:")
|
||||
print('\n'.join(str(project_file) for project_file in project_files))
|
||||
response = input("Continue? y/n: ")
|
||||
if response.lower().strip() not in {"yes", "y"}:
|
||||
return 0
|
||||
else:
|
||||
# If there is only one *.pyproject file in the current directory,
|
||||
# use its file name as the project name
|
||||
project_name = project_files[0].stem
|
||||
|
||||
# The project files that will be written to the pyproject.toml file
|
||||
output_files: set[Path] = set()
|
||||
for project_file in project_files:
|
||||
project_data = parse_pyproject_json(project_file)
|
||||
if project_data.errors:
|
||||
print(f"Invalid project file: {project_file}. Errors found:", file=sys.stderr)
|
||||
print('\n'.join(project_data.errors), file=sys.stderr)
|
||||
return 1
|
||||
output_files.update(project_data.files)
|
||||
|
||||
project_folder = project_files[0].parent.resolve()
|
||||
if project_name is None:
|
||||
# If a project name has not resolved, use the name of the parent folder
|
||||
project_name = project_folder.name
|
||||
|
||||
pyproject_toml_file = project_folder / "pyproject.toml"
|
||||
|
||||
relative_files = sorted(
|
||||
robust_relative_to_posix(p, project_folder) for p in output_files
|
||||
)
|
||||
|
||||
if not (already_existing_file := pyproject_toml_file.exists()):
|
||||
# Create new pyproject.toml file
|
||||
data = {
|
||||
"project": {"name": project_name},
|
||||
"tool": {
|
||||
"pyside6-project": {"files": relative_files}
|
||||
}
|
||||
}
|
||||
updated_content = _write_base_toml_content(data)
|
||||
else:
|
||||
# For an already existing file, append our tool.pyside6-project section
|
||||
# If the project section is missing, add it
|
||||
try:
|
||||
content = pyproject_toml_file.read_text(encoding='utf-8')
|
||||
except Exception as e:
|
||||
print(f"Error processing existing TOML file: {str(e)}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
append_content = []
|
||||
|
||||
if '[project]' not in content:
|
||||
# Add project section if needed
|
||||
append_content.append('\n[project]')
|
||||
append_content.append(f'name = "{project_name}"')
|
||||
|
||||
if '[tool.pyside6-project]' not in content:
|
||||
# Add tool.pyside6-project section
|
||||
append_content.append('\n[tool.pyside6-project]')
|
||||
items = [f'"{item}"' for item in relative_files]
|
||||
append_content.append(f'files = [{", ".join(items)}]')
|
||||
|
||||
if append_content:
|
||||
updated_content = content.rstrip() + '\n' + '\n'.join(append_content)
|
||||
else:
|
||||
# No changes needed
|
||||
print("pyproject.toml already contains [project] and [tool.pyside6-project] sections")
|
||||
return 0
|
||||
|
||||
print(f"WARNING: A pyproject.toml file already exists at \"{pyproject_toml_file}\"")
|
||||
print("The file will be updated with the following content:")
|
||||
print(updated_content)
|
||||
response = input("Proceed? [Y/n] ")
|
||||
if response.lower().strip() not in {"yes", "y"}:
|
||||
return 0
|
||||
|
||||
try:
|
||||
pyproject_toml_file.write_text(updated_content, encoding='utf-8')
|
||||
except Exception as e:
|
||||
print(f"Error writing to \"{pyproject_toml_file}\": {str(e)}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not already_existing_file:
|
||||
print(f"Created \"{pyproject_toml_file}\"")
|
||||
else:
|
||||
print(f"Updated \"{pyproject_toml_file}\"")
|
||||
|
||||
# Recursively migrate the subprojects
|
||||
for sub_project_file in filter(lambda f: f.match(PYPROJECT_JSON_PATTERN), output_files):
|
||||
result = migrate_pyproject(sub_project_file)
|
||||
if result != 0:
|
||||
return result
|
||||
return 0
|
||||
@@ -0,0 +1,194 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from . import (QTPATHS_CMD, PYPROJECT_JSON_PATTERN, PYPROJECT_TOML_PATTERN, PYPROJECT_FILE_PATTERNS,
|
||||
ClOptions)
|
||||
from .pyproject_toml import parse_pyproject_toml
|
||||
from .pyproject_json import parse_pyproject_json
|
||||
|
||||
|
||||
def run_command(command: list[str], cwd: str = None, ignore_fail: bool = False) -> int:
|
||||
"""
|
||||
Run a command using a subprocess.
|
||||
If dry run is enabled, the command will be printed to stdout instead of being executed.
|
||||
|
||||
:param command: The command to run including the arguments
|
||||
:param cwd: The working directory to run the command in
|
||||
:param ignore_fail: If True, the current process will not exit if the command fails
|
||||
|
||||
:return: The exit code of the command
|
||||
"""
|
||||
cloptions = ClOptions()
|
||||
if not cloptions.quiet or cloptions.dry_run:
|
||||
print(" ".join(command))
|
||||
if cloptions.dry_run:
|
||||
return 0
|
||||
|
||||
ex = subprocess.call(command, cwd=cwd)
|
||||
if ex != 0 and not ignore_fail:
|
||||
sys.exit(ex)
|
||||
return ex
|
||||
|
||||
|
||||
def qrc_file_requires_rebuild(resources_file_path: Path, compiled_resources_path: Path) -> bool:
|
||||
"""Returns whether a compiled qrc file needs to be rebuilt based on the files that references"""
|
||||
root_element = ET.parse(resources_file_path).getroot()
|
||||
project_root = resources_file_path.parent
|
||||
|
||||
files = [project_root / file.text for file in root_element.findall(".//file")]
|
||||
|
||||
compiled_resources_time = compiled_resources_path.stat().st_mtime
|
||||
# If any of the resource files has been modified after the compiled qrc file, the compiled qrc
|
||||
# file needs to be rebuilt
|
||||
if any(file.is_file() and file.stat().st_mtime > compiled_resources_time for file in files):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def requires_rebuild(sources: list[Path], artifact: Path) -> bool:
|
||||
"""Returns whether artifact needs to be rebuilt depending on sources"""
|
||||
if not artifact.is_file():
|
||||
return True
|
||||
|
||||
artifact_mod_time = artifact.stat().st_mtime
|
||||
for source in sources:
|
||||
if source.stat().st_mtime > artifact_mod_time:
|
||||
return True
|
||||
# The .qrc file references other files that might have changed
|
||||
if source.suffix == ".qrc" and qrc_file_requires_rebuild(source, artifact):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _remove_path_recursion(path: Path):
|
||||
"""Recursion to remove a file or directory."""
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
elif path.is_dir():
|
||||
for item in path.iterdir():
|
||||
_remove_path_recursion(item)
|
||||
path.rmdir()
|
||||
|
||||
|
||||
def remove_path(path: Path):
|
||||
"""Remove path (file or directory) observing opt_dry_run."""
|
||||
cloptions = ClOptions()
|
||||
if not path.exists():
|
||||
return
|
||||
if not cloptions.quiet:
|
||||
print(f"Removing {path.name}...")
|
||||
if cloptions.dry_run:
|
||||
return
|
||||
_remove_path_recursion(path)
|
||||
|
||||
|
||||
def package_dir() -> Path:
|
||||
"""Return the PySide6 root."""
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
_qtpaths_info: dict[str, str] = {}
|
||||
|
||||
|
||||
def qtpaths() -> dict[str, str]:
|
||||
"""Run qtpaths and return a dict of values."""
|
||||
global _qtpaths_info
|
||||
if not _qtpaths_info:
|
||||
output = subprocess.check_output([QTPATHS_CMD, "--query"])
|
||||
for line in output.decode("utf-8").split("\n"):
|
||||
tokens = line.strip().split(":", maxsplit=1) # "Path=C:\..."
|
||||
if len(tokens) == 2:
|
||||
_qtpaths_info[tokens[0]] = tokens[1]
|
||||
return _qtpaths_info
|
||||
|
||||
|
||||
_qt_metatype_json_dir: Path | None = None
|
||||
|
||||
|
||||
def qt_metatype_json_dir() -> Path:
|
||||
"""Return the location of the Qt QML metatype files."""
|
||||
global _qt_metatype_json_dir
|
||||
if not _qt_metatype_json_dir:
|
||||
qt_dir = package_dir()
|
||||
if sys.platform != "win32":
|
||||
qt_dir /= "Qt"
|
||||
metatypes_dir = qt_dir / "metatypes"
|
||||
if metatypes_dir.is_dir(): # Fully installed case
|
||||
_qt_metatype_json_dir = metatypes_dir
|
||||
else:
|
||||
# Fallback for distro builds/development.
|
||||
print(
|
||||
f"Falling back to {QTPATHS_CMD} to determine metatypes directory.", file=sys.stderr
|
||||
)
|
||||
_qt_metatype_json_dir = Path(qtpaths()["QT_INSTALL_ARCHDATA"]) / "metatypes"
|
||||
return _qt_metatype_json_dir
|
||||
|
||||
|
||||
def resolve_valid_project_file(
|
||||
project_path_input: str = None, project_file_patterns: list[str] = PYPROJECT_FILE_PATTERNS
|
||||
) -> Path:
|
||||
"""
|
||||
Find a valid project file given a preferred project file name and a list of project file name
|
||||
patterns for a fallback search.
|
||||
|
||||
If the provided file name is a valid project file, return it. Otherwise, search for a known
|
||||
project file in the current working directory with the given patterns.
|
||||
|
||||
Raises a ValueError if no project file is found, multiple project files are found in the same
|
||||
directory or the provided path is not a valid project file or folder.
|
||||
|
||||
:param project_path_input: The command-line argument specifying a project file or folder path.
|
||||
:param project_file_patterns: The list of project file patterns to search for.
|
||||
|
||||
:return: The resolved project file path
|
||||
"""
|
||||
if project_path_input and (project_file := Path(project_path_input).resolve()).is_file():
|
||||
if project_file.match(PYPROJECT_TOML_PATTERN):
|
||||
if bool(parse_pyproject_toml(project_file).errors):
|
||||
raise ValueError(f"Invalid project file: {project_file}")
|
||||
elif project_file.match(PYPROJECT_JSON_PATTERN):
|
||||
pyproject_json_result = parse_pyproject_json(project_file)
|
||||
if errors := '\n'.join(str(e) for e in pyproject_json_result.errors):
|
||||
raise ValueError(f"Invalid project file: {project_file}\n{errors}")
|
||||
else:
|
||||
raise ValueError(f"Unknown project file: {project_file}")
|
||||
return project_file
|
||||
|
||||
project_folder = Path.cwd()
|
||||
if project_path_input:
|
||||
if not Path(project_path_input).resolve().is_dir():
|
||||
raise ValueError(f"Invalid project path: {project_path_input}")
|
||||
project_folder = Path(project_path_input).resolve()
|
||||
|
||||
# Search a project file in the project folder using the provided patterns
|
||||
for pattern in project_file_patterns:
|
||||
if not (matches := list(project_folder.glob(pattern))):
|
||||
# No project files found with the specified pattern
|
||||
continue
|
||||
|
||||
if len(matches) > 1:
|
||||
matched_files = '\n'.join(str(f) for f in matches)
|
||||
raise ValueError(f"Multiple project files found:\n{matched_files}")
|
||||
|
||||
project_file = matches[0]
|
||||
|
||||
if pattern == PYPROJECT_TOML_PATTERN:
|
||||
if parse_pyproject_toml(project_file).errors:
|
||||
# Invalid file, but a .pyproject file may exist
|
||||
# We can not raise an error due to ensuring backward compatibility
|
||||
continue
|
||||
elif pattern == PYPROJECT_JSON_PATTERN:
|
||||
pyproject_json_result = parse_pyproject_json(project_file)
|
||||
if errors := '\n'.join(str(e) for e in pyproject_json_result.errors):
|
||||
raise ValueError(f"Invalid project file: {project_file}\n{errors}")
|
||||
|
||||
# Found a valid project file
|
||||
return project_file
|
||||
|
||||
raise ValueError("No project file found in the current directory")
|
||||
264
.venv_nopip/Lib/site-packages/PySide6/scripts/pyside_tool.py
Normal file
264
.venv_nopip/Lib/site-packages/PySide6/scripts/pyside_tool.py
Normal file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
from pathlib import Path
|
||||
|
||||
import PySide6 as ref_mod
|
||||
|
||||
VIRTUAL_ENV = "VIRTUAL_ENV"
|
||||
|
||||
|
||||
def is_pyenv_python():
|
||||
pyenv_root = os.environ.get("PYENV_ROOT")
|
||||
|
||||
if pyenv_root:
|
||||
resolved_exe = Path(sys.executable).resolve()
|
||||
if str(resolved_exe).startswith(pyenv_root):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_virtual_env():
|
||||
return sys.prefix != sys.base_prefix
|
||||
|
||||
|
||||
def init_virtual_env():
|
||||
"""PYSIDE-2251: Enable running from a non-activated virtual environment
|
||||
as is the case for Visual Studio Code by setting the VIRTUAL_ENV
|
||||
variable which is used by the Qt Designer plugin."""
|
||||
if is_virtual_env() and not os.environ.get(VIRTUAL_ENV):
|
||||
os.environ[VIRTUAL_ENV] = sys.prefix
|
||||
|
||||
|
||||
def main():
|
||||
# This will take care of "pyside6-lupdate" listed as an entrypoint
|
||||
# in setup.py are copied to 'scripts/..'
|
||||
cmd = os.path.join("..", os.path.basename(sys.argv[0]))
|
||||
command = [os.path.join(os.path.dirname(os.path.realpath(__file__)), cmd)]
|
||||
command.extend(sys.argv[1:])
|
||||
sys.exit(subprocess.call(command))
|
||||
|
||||
|
||||
def qt_tool_wrapper(qt_tool, args, libexec=False):
|
||||
# Taking care of pyside6-uic, pyside6-rcc, and pyside6-designer
|
||||
# listed as an entrypoint in setup.py
|
||||
pyside_dir = Path(ref_mod.__file__).resolve().parent
|
||||
if libexec and sys.platform != "win32":
|
||||
exe = pyside_dir / 'Qt' / 'libexec' / qt_tool
|
||||
else:
|
||||
exe = pyside_dir / qt_tool
|
||||
|
||||
cmd = [os.fspath(exe)] + args
|
||||
returncode = subprocess.call(cmd)
|
||||
if returncode != 0:
|
||||
command = ' '.join(cmd)
|
||||
print(f"'{command}' returned {returncode}", file=sys.stderr)
|
||||
sys.exit(returncode)
|
||||
|
||||
|
||||
def pyside_script_wrapper(script_name):
|
||||
"""Launch a script shipped with PySide."""
|
||||
script = Path(__file__).resolve().parent / script_name
|
||||
command = [sys.executable, os.fspath(script)] + sys.argv[1:]
|
||||
sys.exit(subprocess.call(command))
|
||||
|
||||
|
||||
def ui_tool_binary(binary):
|
||||
"""Return the binary of a UI tool (App bundle on macOS)."""
|
||||
if sys.platform != "darwin":
|
||||
return binary
|
||||
name = binary[0:1].upper() + binary[1:]
|
||||
return f"{name}.app/Contents/MacOS/{name}"
|
||||
|
||||
|
||||
def lrelease():
|
||||
qt_tool_wrapper("lrelease", sys.argv[1:])
|
||||
|
||||
|
||||
def lupdate():
|
||||
qt_tool_wrapper("lupdate", sys.argv[1:])
|
||||
|
||||
|
||||
def uic():
|
||||
qt_tool_wrapper("uic", ['-g', 'python'] + sys.argv[1:], True)
|
||||
|
||||
|
||||
def rcc():
|
||||
args = []
|
||||
user_args = sys.argv[1:]
|
||||
if "--binary" not in user_args:
|
||||
args.extend(['-g', 'python'])
|
||||
args.extend(user_args)
|
||||
qt_tool_wrapper("rcc", args, True)
|
||||
|
||||
|
||||
def qmltyperegistrar():
|
||||
qt_tool_wrapper("qmltyperegistrar", sys.argv[1:], True)
|
||||
|
||||
|
||||
def qmlimportscanner():
|
||||
qt_tool_wrapper("qmlimportscanner", sys.argv[1:], True)
|
||||
|
||||
|
||||
def qmlcachegen():
|
||||
qt_tool_wrapper("qmlcachegen", sys.argv[1:], True)
|
||||
|
||||
|
||||
def qmllint():
|
||||
qt_tool_wrapper("qmllint", sys.argv[1:])
|
||||
|
||||
|
||||
def qmlformat():
|
||||
qt_tool_wrapper("qmlformat", sys.argv[1:])
|
||||
|
||||
|
||||
def qmlls():
|
||||
qt_tool_wrapper("qmlls", sys.argv[1:])
|
||||
|
||||
|
||||
def assistant():
|
||||
qt_tool_wrapper(ui_tool_binary("assistant"), sys.argv[1:])
|
||||
|
||||
|
||||
def _extend_path_var(var, value, prepend=False):
|
||||
env_value = os.environ.get(var)
|
||||
if env_value:
|
||||
env_value = (f'{value}{os.pathsep}{env_value}'
|
||||
if prepend else f'{env_value}{os.pathsep}{value}')
|
||||
else:
|
||||
env_value = value
|
||||
os.environ[var] = env_value
|
||||
|
||||
|
||||
def designer():
|
||||
init_virtual_env()
|
||||
|
||||
# https://www.python.org/dev/peps/pep-0384/#linkage :
|
||||
# "On Unix systems, the ABI is typically provided by the python executable
|
||||
# itself", that is, libshiboken does not link against any Python library
|
||||
# and expects to get these symbols from a python executable. Since no
|
||||
# python executable is involved when loading this plugin, pre-load python.so
|
||||
# This should also help to work around a numpy issue, see
|
||||
# https://stackoverflow.com/questions/49784583/numpy-import-fails-on-multiarray-extension-library-when-called-from-embedded-pyt
|
||||
major_version = sys.version_info[0]
|
||||
minor_version = sys.version_info[1]
|
||||
os.environ['PY_MAJOR_VERSION'] = str(major_version)
|
||||
os.environ['PY_MINOR_VERSION'] = str(minor_version)
|
||||
if sys.platform == 'linux':
|
||||
# Determine library name (examples/utils/pyside_config.py)
|
||||
version = f'{major_version}.{minor_version}'
|
||||
library_name = f'libpython{version}{sys.abiflags}.so'
|
||||
if is_pyenv_python():
|
||||
library_name = str(Path(sysconfig.get_config_var('LIBDIR')) / library_name)
|
||||
os.environ['LD_PRELOAD'] = library_name
|
||||
elif sys.platform == 'darwin':
|
||||
library_name = sysconfig.get_config_var("LDLIBRARY")
|
||||
framework_prefix = sysconfig.get_config_var("PYTHONFRAMEWORKPREFIX")
|
||||
lib_path = None
|
||||
if framework_prefix:
|
||||
lib_path = os.fspath(Path(framework_prefix) / library_name)
|
||||
elif is_pyenv_python():
|
||||
lib_path = str(Path(sysconfig.get_config_var('LIBDIR')) / library_name)
|
||||
else:
|
||||
# ideally this should never be reached because the system Python and Python installed
|
||||
# from python.org are all framework builds
|
||||
print("Unable to find Python library directory. Use a framework build of Python.",
|
||||
file=sys.stderr)
|
||||
sys.exit(0)
|
||||
os.environ['DYLD_INSERT_LIBRARIES'] = lib_path
|
||||
elif sys.platform == 'win32':
|
||||
# Find Python DLLs from the base installation
|
||||
if is_virtual_env():
|
||||
_extend_path_var("PATH", os.fspath(Path(sys._base_executable).parent), True)
|
||||
|
||||
qt_tool_wrapper(ui_tool_binary("designer"), sys.argv[1:])
|
||||
|
||||
|
||||
def linguist():
|
||||
qt_tool_wrapper(ui_tool_binary("linguist"), sys.argv[1:])
|
||||
|
||||
|
||||
def genpyi():
|
||||
pyside_dir = Path(__file__).resolve().parents[1]
|
||||
support = pyside_dir / "support"
|
||||
cmd = support / "generate_pyi.py"
|
||||
command = [sys.executable, os.fspath(cmd)] + sys.argv[1:]
|
||||
sys.exit(subprocess.call(command))
|
||||
|
||||
|
||||
def metaobjectdump():
|
||||
pyside_script_wrapper("metaobjectdump.py")
|
||||
|
||||
|
||||
def _check_requirements(requirements_file):
|
||||
"""Check if all required packages are installed."""
|
||||
missing_packages = []
|
||||
with open(requirements_file, 'r', encoding='UTF-8') as file:
|
||||
for line in file:
|
||||
# versions
|
||||
package = line.strip().split('==')[0]
|
||||
if not importlib.util.find_spec(package):
|
||||
missing_packages.append(line.strip())
|
||||
return missing_packages
|
||||
|
||||
|
||||
def project():
|
||||
pyside_script_wrapper("project.py")
|
||||
|
||||
|
||||
def qml():
|
||||
pyside_script_wrapper("qml.py")
|
||||
|
||||
|
||||
def qtpy2cpp():
|
||||
pyside_script_wrapper("qtpy2cpp.py")
|
||||
|
||||
|
||||
def deploy():
|
||||
pyside_script_wrapper("deploy.py")
|
||||
|
||||
|
||||
def android_deploy():
|
||||
if sys.platform == "win32":
|
||||
print("pyside6-android-deploy only works from a Unix host and not a Windows host",
|
||||
file=sys.stderr)
|
||||
else:
|
||||
android_requirements_file = Path(__file__).parent / "requirements-android.txt"
|
||||
if android_requirements_file.exists():
|
||||
missing_packages = _check_requirements(android_requirements_file)
|
||||
if missing_packages:
|
||||
print("The following packages are required but not installed:")
|
||||
for package in missing_packages:
|
||||
print(f" - {package}")
|
||||
print("Please install them using:")
|
||||
print(f" pip install -r {android_requirements_file}")
|
||||
sys.exit(1)
|
||||
pyside_script_wrapper("android_deploy.py")
|
||||
|
||||
|
||||
def qsb():
|
||||
qt_tool_wrapper("qsb", sys.argv[1:])
|
||||
|
||||
|
||||
def balsam():
|
||||
qt_tool_wrapper("balsam", sys.argv[1:])
|
||||
|
||||
|
||||
def balsamui():
|
||||
qt_tool_wrapper("balsamui", sys.argv[1:])
|
||||
|
||||
|
||||
def svgtoqml():
|
||||
qt_tool_wrapper("svgtoqml", sys.argv[1:])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
248
.venv_nopip/Lib/site-packages/PySide6/scripts/qml.py
Normal file
248
.venv_nopip/Lib/site-packages/PySide6/scripts/qml.py
Normal file
@@ -0,0 +1,248 @@
|
||||
# Copyright (C) 2018 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 __future__ import annotations
|
||||
|
||||
"""pyside6-qml tool implementation. This tool mimics the capabilities of qml runtime utility
|
||||
for python and enables quick protyping with python modules"""
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pprint import pprint
|
||||
|
||||
from PySide6.QtCore import QCoreApplication, Qt, QLibraryInfo, QUrl, SignalInstance
|
||||
from PySide6.QtGui import QGuiApplication, QSurfaceFormat
|
||||
from PySide6.QtQml import QQmlApplicationEngine, QQmlComponent
|
||||
from PySide6.QtQuick import QQuickView, QQuickItem
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
|
||||
def import_qml_modules(qml_parent_path: Path, module_paths: list[Path] = []):
|
||||
'''
|
||||
Import all the python modules in the qml_parent_path. This way all the classes
|
||||
containing the @QmlElement/@QmlNamedElement are also imported
|
||||
|
||||
Parameters:
|
||||
qml_parent_path (Path): Parent directory of the qml file
|
||||
module_paths (int): user give import paths obtained through cli
|
||||
'''
|
||||
|
||||
search_dir_paths = []
|
||||
search_file_paths = []
|
||||
|
||||
if not module_paths:
|
||||
search_dir_paths.append(qml_parent_path)
|
||||
else:
|
||||
for module_path in module_paths:
|
||||
if module_path.is_dir():
|
||||
search_dir_paths.append(module_path)
|
||||
elif module_path.exists() and module_path.suffix == ".py":
|
||||
search_file_paths.append(module_path)
|
||||
|
||||
def import_module(import_module_paths: set[Path]):
|
||||
"""Import the modules in 'import_module_paths'"""
|
||||
for module_path in import_module_paths:
|
||||
module_name = module_path.name[:-3]
|
||||
_spec = importlib.util.spec_from_file_location(f"{module_name}", module_path)
|
||||
_module = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(module=_module)
|
||||
|
||||
modules_to_import = set()
|
||||
for search_path in search_dir_paths:
|
||||
possible_modules = list(search_path.glob("**/*.py"))
|
||||
for possible_module in possible_modules:
|
||||
if possible_module.is_file() and possible_module.name != "__init__.py":
|
||||
module_parent = str(possible_module.parent)
|
||||
if module_parent not in sys.path:
|
||||
sys.path.append(module_parent)
|
||||
modules_to_import.add(possible_module)
|
||||
|
||||
for search_path in search_file_paths:
|
||||
sys.path.append(str(search_path.parent))
|
||||
modules_to_import.add(search_path)
|
||||
|
||||
import_module(import_module_paths=modules_to_import)
|
||||
|
||||
|
||||
def print_configurations():
|
||||
return "Built-in configurations \n\t default \n\t resizeToItem"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="This tools mimics the capabilities of qml runtime utility by directly"
|
||||
" invoking QQmlEngine/QQuickView. It enables quick prototyping with qml files.",
|
||||
formatter_class=argparse.RawTextHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"file",
|
||||
type=lambda p: Path(p).absolute(),
|
||||
help="Path to qml file to display",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--module-paths", "-I",
|
||||
type=lambda p: Path(p).absolute(),
|
||||
nargs="+",
|
||||
help="Specify space separated folder/file paths where the Qml classes are defined. By"
|
||||
" default,the parent directory of the qml_path is searched recursively for all .py"
|
||||
" files and they are imported. Otherwise only the paths give in module paths are"
|
||||
" searched",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-conf",
|
||||
action="version",
|
||||
help="List the built-in configurations.",
|
||||
version=print_configurations()
|
||||
)
|
||||
parser.add_argument(
|
||||
"--apptype", "-a",
|
||||
choices=["core", "gui", "widget"],
|
||||
default="gui",
|
||||
help="Select which application class to use. Default is gui",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config", "-c",
|
||||
choices=["default", "resizeToItem"],
|
||||
default="default",
|
||||
help="Select the built-in configurations.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rhi", "-r",
|
||||
choices=["vulkan", "metal", "d3dll", "gl"],
|
||||
help="Set the backend for the Qt graphics abstraction (RHI).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--core-profile",
|
||||
action="store_true",
|
||||
help="Force use of OpenGL Core Profile.",
|
||||
)
|
||||
parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
help="Print information about what qml is doing, like specific file URLs being loaded.",
|
||||
action="store_const", dest="loglevel", const=logging.INFO,
|
||||
)
|
||||
|
||||
gl_group = parser.add_mutually_exclusive_group(required=False)
|
||||
gl_group.add_argument(
|
||||
"--gles",
|
||||
action="store_true",
|
||||
help="Force use of GLES (AA_UseOpenGLES)",
|
||||
)
|
||||
gl_group.add_argument(
|
||||
"--desktop",
|
||||
action="store_true",
|
||||
help="Force use of desktop OpenGL (AA_UseDesktopOpenGL)",
|
||||
)
|
||||
gl_group.add_argument(
|
||||
"--software",
|
||||
action="store_true",
|
||||
help="Force use of software rendering(AA_UseSoftwareOpenGL)",
|
||||
)
|
||||
gl_group.add_argument(
|
||||
"--disable-context-sharing",
|
||||
action="store_true",
|
||||
help=" Disable the use of a shared GL context for QtQuick Windows",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
apptype = args.apptype
|
||||
|
||||
qquick_present = False
|
||||
|
||||
with open(args.file) as myfile:
|
||||
if 'import QtQuick' in myfile.read():
|
||||
qquick_present = True
|
||||
|
||||
# no import QtQuick => QQCoreApplication
|
||||
if not qquick_present:
|
||||
apptype = "core"
|
||||
|
||||
import_qml_modules(args.file.parent, args.module_paths)
|
||||
|
||||
logging.basicConfig(level=args.loglevel)
|
||||
logging.info(f"qml: {QLibraryInfo.build()}")
|
||||
logging.info(f"qml: Using built-in configuration: {args.config}")
|
||||
|
||||
if args.rhi:
|
||||
os.environ['QSG_RHI_BACKEND'] = args.rhi
|
||||
|
||||
logging.info(f"qml: loading {args.file}")
|
||||
qml_file = QUrl.fromLocalFile(str(args.file))
|
||||
|
||||
if apptype == "gui":
|
||||
if args.gles:
|
||||
logging.info("qml: Using attribute AA_UseOpenGLES")
|
||||
QCoreApplication.setAttribute(Qt.AA_UseOpenGLES)
|
||||
elif args.desktop:
|
||||
logging.info("qml: Using attribute AA_UseDesktopOpenGL")
|
||||
QCoreApplication.setAttribute(Qt.AA_UseDesktopOpenGL)
|
||||
elif args.software:
|
||||
logging.info("qml: Using attribute AA_UseSoftwareOpenGL")
|
||||
QCoreApplication.setAttribute(Qt.AA_UseSoftwareOpenGL)
|
||||
|
||||
# context-sharing is enabled by default
|
||||
if not args.disable_context_sharing:
|
||||
logging.info("qml: Using attribute AA_ShareOpenGLContexts")
|
||||
QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts)
|
||||
|
||||
if apptype == "core":
|
||||
logging.info("qml: Core application")
|
||||
app = QCoreApplication(sys.argv)
|
||||
elif apptype == "widgets":
|
||||
logging.info("qml: Widget application")
|
||||
app = QApplication(sys.argv)
|
||||
else:
|
||||
logging.info("qml: Gui application")
|
||||
app = QGuiApplication(sys.argv)
|
||||
|
||||
engine = QQmlApplicationEngine()
|
||||
|
||||
# set OpenGLContextProfile
|
||||
if apptype == "gui" and args.core_profile:
|
||||
logging.info("qml: Set profile for QSurfaceFormat as CoreProfile")
|
||||
surfaceFormat = QSurfaceFormat()
|
||||
surfaceFormat.setStencilBufferSize(8)
|
||||
surfaceFormat.setDepthBufferSize(24)
|
||||
surfaceFormat.setVersion(4, 1)
|
||||
surfaceFormat.setProfile(QSurfaceFormat.CoreProfile)
|
||||
QSurfaceFormat.setDefaultFormat(surfaceFormat)
|
||||
|
||||
# in the case of QCoreApplication we print the attributes of the object created via
|
||||
# QQmlComponent and exit
|
||||
if apptype == "core":
|
||||
component = QQmlComponent(engine, qml_file)
|
||||
obj = component.create()
|
||||
filtered_attributes = {k: v for k, v in vars(obj).items() if type(v) is not SignalInstance}
|
||||
logging.info("qml: component object attributes are")
|
||||
pprint(filtered_attributes)
|
||||
del engine
|
||||
sys.exit(0)
|
||||
|
||||
engine.load(qml_file)
|
||||
rootObjects = engine.rootObjects()
|
||||
if not rootObjects:
|
||||
sys.exit(-1)
|
||||
|
||||
qquick_view = False
|
||||
if isinstance(rootObjects[0], QQuickItem) and qquick_present:
|
||||
logging.info("qml: loading with QQuickView")
|
||||
viewer = QQuickView()
|
||||
viewer.setSource(qml_file)
|
||||
if args.config != "resizeToItem":
|
||||
viewer.setResizeMode(QQuickView.SizeRootObjectToView)
|
||||
else:
|
||||
viewer.setResizeMode(QQuickView.SizeViewToRootObject)
|
||||
viewer.show()
|
||||
qquick_view = True
|
||||
|
||||
if not qquick_view:
|
||||
logging.info("qml: loading with QQmlApplicationEngine")
|
||||
if args.config == "resizeToItem":
|
||||
logging.info("qml: Not a QQuickview item. resizeToItem is done by default")
|
||||
|
||||
exit_code = app.exec()
|
||||
del engine
|
||||
sys.exit(exit_code)
|
||||
63
.venv_nopip/Lib/site-packages/PySide6/scripts/qtpy2cpp.py
Normal file
63
.venv_nopip/Lib/site-packages/PySide6/scripts/qtpy2cpp.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from pathlib import Path
|
||||
|
||||
from qtpy2cpp_lib.visitor import ConvertVisitor
|
||||
|
||||
DESCRIPTION = "Tool to convert Python to C++"
|
||||
|
||||
|
||||
def create_arg_parser(desc):
|
||||
parser = ArgumentParser(description=desc,
|
||||
formatter_class=RawTextHelpFormatter)
|
||||
parser.add_argument("--debug", "-d", action="store_true",
|
||||
help="Debug")
|
||||
parser.add_argument("--stdout", "-s", action="store_true",
|
||||
help="Write to stdout")
|
||||
parser.add_argument("--force", "-f", action="store_true",
|
||||
help="Force overwrite of existing files")
|
||||
parser.add_argument("files", type=str, nargs="+", help="Python source file(s)")
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
arg_parser = create_arg_parser(DESCRIPTION)
|
||||
args = arg_parser.parse_args()
|
||||
ConvertVisitor.debug = args.debug
|
||||
|
||||
for input_file_str in args.files:
|
||||
input_file = Path(input_file_str)
|
||||
if not input_file.is_file():
|
||||
logger.error(f"{input_file_str} does not exist or is not a file.")
|
||||
sys.exit(-1)
|
||||
file_root, ext = os.path.splitext(input_file)
|
||||
if input_file.suffix != ".py":
|
||||
logger.error(f"{input_file_str} does not appear to be a Python file.")
|
||||
sys.exit(-1)
|
||||
|
||||
ast_tree = ConvertVisitor.create_ast(input_file_str)
|
||||
if args.stdout:
|
||||
sys.stdout.write(f"// Converted from {input_file.name}\n")
|
||||
ConvertVisitor(input_file, sys.stdout).visit(ast_tree)
|
||||
else:
|
||||
target_file = input_file.parent / (input_file.stem + ".cpp")
|
||||
if target_file.exists():
|
||||
if not target_file.is_file():
|
||||
logger.error(f"{target_file} exists and is not a file.")
|
||||
sys.exit(-1)
|
||||
if not args.force:
|
||||
logger.error(f"{target_file} exists. Use -f to overwrite.")
|
||||
sys.exit(-1)
|
||||
|
||||
with target_file.open("w") as file:
|
||||
file.write(f"// Converted from {input_file.name}\n")
|
||||
ConvertVisitor(input_file, file).visit(ast_tree)
|
||||
logger.info(f"Wrote {target_file}.")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,112 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
"""Tool to dump a Python AST"""
|
||||
|
||||
|
||||
import ast
|
||||
import tokenize
|
||||
from argparse import ArgumentParser, RawTextHelpFormatter
|
||||
from enum import Enum
|
||||
|
||||
from nodedump import debug_format_node
|
||||
|
||||
DESCRIPTION = "Tool to dump a Python AST"
|
||||
|
||||
|
||||
_source_lines = []
|
||||
_opt_verbose = False
|
||||
|
||||
|
||||
def first_non_space(s):
|
||||
for i, c in enumerate(s):
|
||||
if c != ' ':
|
||||
return i
|
||||
return 0
|
||||
|
||||
|
||||
class NodeType(Enum):
|
||||
IGNORE = 1
|
||||
PRINT_ONE_LINE = 2 # Print as a one liner, do not visit children
|
||||
PRINT = 3 # Print with opening closing tag, visit children
|
||||
PRINT_WITH_SOURCE = 4 # Like PRINT, but print source line above
|
||||
|
||||
|
||||
def get_node_type(node):
|
||||
if isinstance(node, (ast.Load, ast.Store, ast.Delete)):
|
||||
return NodeType.IGNORE
|
||||
if isinstance(node, (ast.Add, ast.alias, ast.arg, ast.Eq, ast.Gt, ast.Lt,
|
||||
ast.Mult, ast.Name, ast.NotEq, ast.NameConstant, ast.Not,
|
||||
ast.Num, ast.Str)):
|
||||
return NodeType.PRINT_ONE_LINE
|
||||
if not hasattr(node, 'lineno'):
|
||||
return NodeType.PRINT
|
||||
if isinstance(node, (ast.Attribute)):
|
||||
return NodeType.PRINT_ONE_LINE if isinstance(node.value, ast.Name) else NodeType.PRINT
|
||||
return NodeType.PRINT_WITH_SOURCE
|
||||
|
||||
|
||||
class DumpVisitor(ast.NodeVisitor):
|
||||
def __init__(self):
|
||||
ast.NodeVisitor.__init__(self)
|
||||
self._indent = 0
|
||||
self._printed_source_lines = {-1}
|
||||
|
||||
def generic_visit(self, node):
|
||||
node_type = get_node_type(node)
|
||||
if _opt_verbose and node_type in (NodeType.IGNORE, NodeType.PRINT_ONE_LINE):
|
||||
node_type = NodeType.PRINT
|
||||
if node_type == NodeType.IGNORE:
|
||||
return
|
||||
self._indent = self._indent + 1
|
||||
indent = ' ' * self._indent
|
||||
|
||||
if node_type == NodeType.PRINT_WITH_SOURCE:
|
||||
line_number = node.lineno - 1
|
||||
if line_number not in self._printed_source_lines:
|
||||
self._printed_source_lines.add(line_number)
|
||||
line = _source_lines[line_number]
|
||||
non_space = first_non_space(line)
|
||||
print('{:04d} {}{}'.format(line_number, '_' * non_space,
|
||||
line[non_space:]))
|
||||
|
||||
if node_type == NodeType.PRINT_ONE_LINE:
|
||||
print(indent, debug_format_node(node))
|
||||
else:
|
||||
print(indent, '>', debug_format_node(node))
|
||||
ast.NodeVisitor.generic_visit(self, node)
|
||||
print(indent, '<', type(node).__name__)
|
||||
|
||||
self._indent = self._indent - 1
|
||||
|
||||
|
||||
def parse_ast(filename):
|
||||
node = None
|
||||
with tokenize.open(filename) as f:
|
||||
global _source_lines
|
||||
source = f.read()
|
||||
_source_lines = source.split('\n')
|
||||
node = ast.parse(source, mode="exec")
|
||||
return node
|
||||
|
||||
|
||||
def create_arg_parser(desc):
|
||||
parser = ArgumentParser(description=desc,
|
||||
formatter_class=RawTextHelpFormatter)
|
||||
parser.add_argument('--verbose', '-v', action='store_true',
|
||||
help='Verbose')
|
||||
parser.add_argument('source', type=str, help='Python source')
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arg_parser = create_arg_parser(DESCRIPTION)
|
||||
options = arg_parser.parse_args()
|
||||
_opt_verbose = options.verbose
|
||||
title = f'AST tree for {options.source}'
|
||||
print('=' * len(title))
|
||||
print(title)
|
||||
print('=' * len(title))
|
||||
tree = parse_ast(options.source)
|
||||
DumpVisitor().visit(tree)
|
||||
@@ -0,0 +1,266 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
"""C++ formatting helper functions and formatter class"""
|
||||
|
||||
|
||||
import ast
|
||||
|
||||
from .qt import ClassFlag, qt_class_flags
|
||||
|
||||
CLOSING = {"{": "}", "(": ")", "[": "]"} # Closing parenthesis for C++
|
||||
|
||||
|
||||
def _fix_function_argument_type(type, for_return):
|
||||
"""Fix function argument/return qualifiers using some heuristics for Qt."""
|
||||
if type == "float":
|
||||
return "double"
|
||||
if type == "str":
|
||||
type = "QString"
|
||||
if not type.startswith("Q"):
|
||||
return type
|
||||
flags = qt_class_flags(type)
|
||||
if flags & ClassFlag.PASS_BY_VALUE:
|
||||
return type
|
||||
if flags & ClassFlag.PASS_BY_CONSTREF:
|
||||
return type if for_return else f"const {type} &"
|
||||
if flags & ClassFlag.PASS_BY_REF:
|
||||
return type if for_return else f"{type} &"
|
||||
return type + " *" # Assume pointer by default
|
||||
|
||||
|
||||
def to_string(node):
|
||||
"""Helper to retrieve a string from the (Lists of)Name/Attribute
|
||||
aggregated into some nodes"""
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
if isinstance(node, ast.Attribute):
|
||||
return node.attr
|
||||
return ''
|
||||
|
||||
|
||||
def format_inheritance(class_def_node):
|
||||
"""Returns inheritance specification of a class"""
|
||||
result = ''
|
||||
for base in class_def_node.bases:
|
||||
name = to_string(base)
|
||||
if name != 'object':
|
||||
result += ', public ' if result else ' : public '
|
||||
result += name
|
||||
return result
|
||||
|
||||
|
||||
def format_for_target(target_node):
|
||||
if isinstance(target_node, ast.Tuple): # for i,e in enumerate()
|
||||
result = ''
|
||||
for i, el in enumerate(target_node.elts):
|
||||
if i > 0:
|
||||
result += ', '
|
||||
result += format_reference(el)
|
||||
return result
|
||||
return format_reference(target_node)
|
||||
|
||||
|
||||
def format_for_loop(f_node):
|
||||
"""Format a for loop
|
||||
This applies some heuristics to detect:
|
||||
1) "for a in [1,2])" -> "for (f: {1, 2}) {"
|
||||
2) "for i in range(5)" -> "for (i = 0; i < 5; ++i) {"
|
||||
3) "for i in range(2,5)" -> "for (i = 2; i < 5; ++i) {"
|
||||
|
||||
TODO: Detect other cases, maybe including enumerate().
|
||||
"""
|
||||
loop_vars = format_for_target(f_node.target)
|
||||
result = 'for (' + loop_vars
|
||||
if isinstance(f_node.iter, ast.Call):
|
||||
f = format_reference(f_node.iter.func)
|
||||
if f == 'range':
|
||||
start = 0
|
||||
end = -1
|
||||
if len(f_node.iter.args) == 2:
|
||||
start = format_literal(f_node.iter.args[0])
|
||||
end = format_literal(f_node.iter.args[1])
|
||||
elif len(f_node.iter.args) == 1:
|
||||
end = format_literal(f_node.iter.args[0])
|
||||
result += f' = {start}; {loop_vars} < {end}; ++{loop_vars}'
|
||||
elif isinstance(f_node.iter, ast.List):
|
||||
# Range based for over list
|
||||
result += ': ' + format_literal_list(f_node.iter)
|
||||
elif isinstance(f_node.iter, ast.Name):
|
||||
# Range based for over variable
|
||||
result += ': ' + f_node.iter.id
|
||||
result += ') {'
|
||||
return result
|
||||
|
||||
|
||||
def format_name_constant(node):
|
||||
"""Format a ast.NameConstant."""
|
||||
if node.value is None:
|
||||
return "nullptr"
|
||||
return "true" if node.value else "false"
|
||||
|
||||
|
||||
def format_literal(node):
|
||||
"""Returns the value of number/string literals"""
|
||||
if isinstance(node, ast.NameConstant):
|
||||
return format_name_constant(node)
|
||||
if isinstance(node, ast.Num):
|
||||
return str(node.n)
|
||||
if isinstance(node, ast.Str):
|
||||
# Fixme: escaping
|
||||
return f'"{node.s}"'
|
||||
return ''
|
||||
|
||||
|
||||
def format_literal_list(l_node, enclosing='{'):
|
||||
"""Formats a list/tuple of number/string literals as C++ initializer list"""
|
||||
result = enclosing
|
||||
for i, el in enumerate(l_node.elts):
|
||||
if i > 0:
|
||||
result += ', '
|
||||
result += format_literal(el)
|
||||
result += CLOSING[enclosing]
|
||||
return result
|
||||
|
||||
|
||||
def format_member(attrib_node, qualifier_in='auto'):
|
||||
"""Member access foo->member() is expressed as an attribute with
|
||||
further nested Attributes/Names as value"""
|
||||
n = attrib_node
|
||||
result = ''
|
||||
# Black magic: Guess '::' if name appears to be a class name
|
||||
qualifier = qualifier_in
|
||||
if qualifier_in == 'auto':
|
||||
qualifier = '::' if n.attr[0:1].isupper() else '->'
|
||||
while isinstance(n, ast.Attribute):
|
||||
result = n.attr if not result else n.attr + qualifier + result
|
||||
n = n.value
|
||||
if isinstance(n, ast.Name) and n.id != 'self':
|
||||
if qualifier_in == 'auto' and n.id == "Qt": # Qt namespace
|
||||
qualifier = "::"
|
||||
result = n.id + qualifier + result
|
||||
return result
|
||||
|
||||
|
||||
def format_reference(node, qualifier='auto'):
|
||||
"""Format member reference or free item"""
|
||||
return node.id if isinstance(node, ast.Name) else format_member(node, qualifier)
|
||||
|
||||
|
||||
def format_function_def_arguments(function_def_node):
|
||||
"""Formats arguments of a function definition"""
|
||||
# Default values is a list of the last default values, expand
|
||||
# so that indexes match
|
||||
argument_count = len(function_def_node.args.args)
|
||||
default_values = function_def_node.args.defaults
|
||||
while len(default_values) < argument_count:
|
||||
default_values.insert(0, None)
|
||||
result = ''
|
||||
for i, a in enumerate(function_def_node.args.args):
|
||||
if result:
|
||||
result += ', '
|
||||
if a.arg != 'self':
|
||||
if a.annotation and isinstance(a.annotation, ast.Name):
|
||||
result += _fix_function_argument_type(a.annotation.id, False) + ' '
|
||||
result += a.arg
|
||||
if default_values[i]:
|
||||
result += ' = '
|
||||
default_value = default_values[i]
|
||||
if isinstance(default_value, ast.Attribute):
|
||||
result += format_reference(default_value)
|
||||
else:
|
||||
result += format_literal(default_value)
|
||||
return result
|
||||
|
||||
|
||||
def format_start_function_call(call_node):
|
||||
"""Format a call of a free or member function"""
|
||||
return format_reference(call_node.func) + '('
|
||||
|
||||
|
||||
def write_import(file, i_node):
|
||||
"""Print an import of a Qt class as #include"""
|
||||
for alias in i_node.names:
|
||||
if alias.name.startswith('Q'):
|
||||
file.write(f'#include <{alias.name}>\n')
|
||||
|
||||
|
||||
def write_import_from(file, i_node):
|
||||
"""Print an import from Qt classes as #include sequence"""
|
||||
# "from PySide6.QtGui import QGuiApplication" or
|
||||
# "from PySide6 import QtGui"
|
||||
mod = i_node.module
|
||||
if not mod.startswith('PySide') and not mod.startswith('PyQt'):
|
||||
return
|
||||
dot = mod.find('.')
|
||||
qt_module = mod[dot + 1:] + '/' if dot >= 0 else ''
|
||||
for i in i_node.names:
|
||||
if i.name.startswith('Q'):
|
||||
file.write(f'#include <{qt_module}{i.name}>\n')
|
||||
|
||||
|
||||
class Indenter:
|
||||
"""Helper for Indentation"""
|
||||
|
||||
def __init__(self, output_file):
|
||||
self._indent_level = 0
|
||||
self._indentation = ''
|
||||
self._output_file = output_file
|
||||
|
||||
def indent_string(self, string):
|
||||
"""Start a new line by a string"""
|
||||
self._output_file.write(self._indentation)
|
||||
self._output_file.write(string)
|
||||
|
||||
def indent_line(self, line):
|
||||
"""Write an indented line"""
|
||||
self._output_file.write(self._indentation)
|
||||
self._output_file.write(line)
|
||||
self._output_file.write('\n')
|
||||
|
||||
def INDENT(self):
|
||||
"""Write indentation"""
|
||||
self._output_file.write(self._indentation)
|
||||
|
||||
def indent(self):
|
||||
"""Increase indentation level"""
|
||||
self._indent_level = self._indent_level + 1
|
||||
self._indentation = ' ' * self._indent_level
|
||||
|
||||
def dedent(self):
|
||||
"""Decrease indentation level"""
|
||||
self._indent_level = self._indent_level - 1
|
||||
self._indentation = ' ' * self._indent_level
|
||||
|
||||
|
||||
class CppFormatter(Indenter):
|
||||
"""Provides helpers for formatting multi-line C++ constructs"""
|
||||
|
||||
def __init__(self, output_file):
|
||||
Indenter.__init__(self, output_file)
|
||||
|
||||
def write_class_def(self, class_node):
|
||||
"""Print a class definition with inheritance"""
|
||||
self._output_file.write('\n')
|
||||
inherits = format_inheritance(class_node)
|
||||
self.indent_line(f'class {class_node.name}{inherits}')
|
||||
self.indent_line('{')
|
||||
self.indent_line('public:')
|
||||
|
||||
def write_function_def(self, f_node, class_context):
|
||||
"""Print a function definition with arguments"""
|
||||
self._output_file.write('\n')
|
||||
arguments = format_function_def_arguments(f_node)
|
||||
if f_node.name == '__init__' and class_context: # Constructor
|
||||
name = class_context
|
||||
elif f_node.name == '__del__' and class_context: # Destructor
|
||||
name = '~' + class_context
|
||||
else:
|
||||
return_type = "void"
|
||||
if f_node.returns and isinstance(f_node.returns, ast.Name):
|
||||
return_type = _fix_function_argument_type(f_node.returns.id, True)
|
||||
name = return_type + " " + f_node.name
|
||||
self.indent_string(f'{name}({arguments})')
|
||||
self._output_file.write('\n')
|
||||
self.indent_line('{')
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
"""Helper to dump AST nodes for debugging"""
|
||||
|
||||
|
||||
import ast
|
||||
|
||||
|
||||
def to_string(node):
|
||||
"""Helper to retrieve a string from the (Lists of )Name/Attribute
|
||||
aggregated into some nodes"""
|
||||
if isinstance(node, ast.Name):
|
||||
return node.id
|
||||
if isinstance(node, ast.Attribute):
|
||||
return node.attr
|
||||
return ''
|
||||
|
||||
|
||||
def debug_format_node(node):
|
||||
"""Format AST node for debugging"""
|
||||
if isinstance(node, ast.alias):
|
||||
return f'alias("{node.name}")'
|
||||
if isinstance(node, ast.arg):
|
||||
return f'arg({node.arg})'
|
||||
if isinstance(node, ast.Attribute):
|
||||
if isinstance(node.value, ast.Name):
|
||||
nested_name = debug_format_node(node.value)
|
||||
return f'Attribute("{node.attr}", {nested_name})'
|
||||
return f'Attribute("{node.attr}")'
|
||||
if isinstance(node, ast.Call):
|
||||
return 'Call({}({}))'.format(to_string(node.func), len(node.args))
|
||||
if isinstance(node, ast.ClassDef):
|
||||
base_names = [to_string(base) for base in node.bases]
|
||||
bases = ': ' + ','.join(base_names) if base_names else ''
|
||||
return f'ClassDef({node.name}{bases})'
|
||||
if isinstance(node, ast.ImportFrom):
|
||||
return f'ImportFrom("{node.module}")'
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
arg_names = [a.arg for a in node.args.args]
|
||||
return 'FunctionDef({}({}))'.format(node.name, ', '.join(arg_names))
|
||||
if isinstance(node, ast.Name):
|
||||
return 'Name("{}", Ctx={})'.format(node.id, type(node.ctx).__name__)
|
||||
if isinstance(node, ast.NameConstant):
|
||||
return f'NameConstant({node.value})'
|
||||
if isinstance(node, ast.Num):
|
||||
return f'Num({node.n})'
|
||||
if isinstance(node, ast.Str):
|
||||
return f'Str("{node.s}")'
|
||||
return type(node).__name__
|
||||
@@ -0,0 +1,63 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
"""Provides some type information on Qt classes"""
|
||||
|
||||
|
||||
from enum import Flag
|
||||
|
||||
|
||||
class ClassFlag(Flag):
|
||||
PASS_BY_CONSTREF = 1
|
||||
PASS_BY_REF = 2
|
||||
PASS_BY_VALUE = 4
|
||||
PASS_ON_STACK_MASK = PASS_BY_CONSTREF | PASS_BY_REF | PASS_BY_VALUE
|
||||
INSTANTIATE_ON_STACK = 8
|
||||
|
||||
|
||||
_QT_CLASS_FLAGS = {
|
||||
# QtCore
|
||||
"QCoreApplication": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QFile": ClassFlag.PASS_BY_REF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QFileInfo": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QLine": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QLineF": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QModelIndex": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QPoint": ClassFlag.PASS_BY_VALUE | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QPointF": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QRect": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QRectF": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QSaveFile": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QSettings": ClassFlag.PASS_BY_REF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QSize": ClassFlag.PASS_BY_VALUE | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QSizeF": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QString": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QTextStream": ClassFlag.PASS_BY_REF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
# QtGui
|
||||
"QBrush": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QColor": ClassFlag.PASS_BY_VALUE | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QGradient": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QGuiApplication": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QIcon": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QPainter": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QPen": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QPixmap": ClassFlag.PASS_BY_CONSTREF | ClassFlag.INSTANTIATE_ON_STACK,
|
||||
# QtWidgets
|
||||
"QApplication": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QColorDialog": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QFileDialog": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QFontDialog": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QMessageBox": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
# QtQml
|
||||
"QQmlApplicationEngine": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QQmlComponent": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
"QQmlEngine": ClassFlag.INSTANTIATE_ON_STACK,
|
||||
# QtQuick
|
||||
"QQuickView": ClassFlag.INSTANTIATE_ON_STACK
|
||||
}
|
||||
|
||||
|
||||
def qt_class_flags(type):
|
||||
f = _QT_CLASS_FLAGS.get(type)
|
||||
return f if f else ClassFlag(0)
|
||||
@@ -0,0 +1,56 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
"""Tool to dump Python Tokens"""
|
||||
|
||||
|
||||
import sys
|
||||
import tokenize
|
||||
|
||||
|
||||
def format_token(t):
|
||||
r = repr(t)
|
||||
if r.startswith('TokenInfo('):
|
||||
r = r[10:]
|
||||
pos = r.find("), line='")
|
||||
if pos < 0:
|
||||
pos = r.find('), line="')
|
||||
if pos > 0:
|
||||
r = r[:pos + 1]
|
||||
return r
|
||||
|
||||
|
||||
def first_non_space(s):
|
||||
for i, c in enumerate(s):
|
||||
if c != ' ':
|
||||
return i
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) < 2:
|
||||
print("Specify file Name")
|
||||
sys.exit(1)
|
||||
filename = sys.argv[1]
|
||||
indent_level = 0
|
||||
indent = ''
|
||||
last_line_number = -1
|
||||
with tokenize.open(filename) as f:
|
||||
generator = tokenize.generate_tokens(f.readline)
|
||||
for t in generator:
|
||||
line_number = t.start[0]
|
||||
if line_number != last_line_number:
|
||||
code_line = t.line.rstrip()
|
||||
non_space = first_non_space(code_line)
|
||||
print('{:04d} {}{}'.format(line_number, '_' * non_space,
|
||||
code_line[non_space:]))
|
||||
last_line_number = line_number
|
||||
if t.type == tokenize.INDENT:
|
||||
indent_level = indent_level + 1
|
||||
indent = ' ' * indent_level
|
||||
elif t.type == tokenize.DEDENT:
|
||||
indent_level = indent_level - 1
|
||||
indent = ' ' * indent_level
|
||||
else:
|
||||
print(' ', indent, format_token(t))
|
||||
@@ -0,0 +1,443 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||
from __future__ import annotations
|
||||
|
||||
"""AST visitor printing out C++"""
|
||||
|
||||
import ast
|
||||
import sys
|
||||
import tokenize
|
||||
import warnings
|
||||
|
||||
from .formatter import (CppFormatter, format_for_loop, format_literal,
|
||||
format_name_constant,
|
||||
format_reference, write_import, write_import_from)
|
||||
from .nodedump import debug_format_node
|
||||
from .qt import ClassFlag, qt_class_flags
|
||||
|
||||
|
||||
def _is_qt_constructor(assign_node):
|
||||
"""Is this assignment node a plain construction of a Qt class?
|
||||
'f = QFile(name)'. Returns the class_name."""
|
||||
call = assign_node.value
|
||||
if (isinstance(call, ast.Call) and isinstance(call.func, ast.Name)):
|
||||
func = call.func.id
|
||||
if func.startswith("Q"):
|
||||
return func
|
||||
return None
|
||||
|
||||
|
||||
def _is_if_main(if_node):
|
||||
"""Return whether an if statement is: if __name__ == '__main__' """
|
||||
test = if_node.test
|
||||
return (isinstance(test, ast.Compare)
|
||||
and len(test.ops) == 1
|
||||
and isinstance(test.ops[0], ast.Eq)
|
||||
and isinstance(test.left, ast.Name)
|
||||
and test.left.id == "__name__"
|
||||
and len(test.comparators) == 1
|
||||
and isinstance(test.comparators[0], ast.Constant)
|
||||
and test.comparators[0].value == "__main__")
|
||||
|
||||
|
||||
class ConvertVisitor(ast.NodeVisitor, CppFormatter):
|
||||
"""AST visitor printing out C++
|
||||
Note on implementation:
|
||||
- Any visit_XXX() overridden function should call self.generic_visit(node)
|
||||
to continue visiting
|
||||
- When controlling the visiting manually (cf visit_Call()),
|
||||
self.visit(child) needs to be called since that dispatches to
|
||||
visit_XXX(). This is usually done to prevent undesired output
|
||||
for example from references of calls, etc.
|
||||
"""
|
||||
|
||||
debug = False
|
||||
|
||||
def __init__(self, file_name, output_file):
|
||||
ast.NodeVisitor.__init__(self)
|
||||
CppFormatter.__init__(self, output_file)
|
||||
self._file_name = file_name
|
||||
self._class_scope = [] # List of class names
|
||||
self._stack = [] # nodes
|
||||
self._stack_variables = [] # variables instantiated on stack
|
||||
self._debug_indent = 0
|
||||
|
||||
@staticmethod
|
||||
def create_ast(filename):
|
||||
"""Create an Abstract Syntax Tree on which a visitor can be run"""
|
||||
node = None
|
||||
with tokenize.open(filename) as file:
|
||||
node = ast.parse(file.read(), mode="exec")
|
||||
return node
|
||||
|
||||
def generic_visit(self, node):
|
||||
parent = self._stack[-1] if self._stack else None
|
||||
if self.debug:
|
||||
self._debug_enter(node, parent)
|
||||
self._stack.append(node)
|
||||
try:
|
||||
super().generic_visit(node)
|
||||
except Exception as e:
|
||||
line_no = node.lineno if hasattr(node, 'lineno') else -1
|
||||
error_message = str(e)
|
||||
message = f'{self._file_name}:{line_no}: Error "{error_message}"'
|
||||
warnings.warn(message)
|
||||
self._output_file.write(f'\n// {error_message}\n')
|
||||
del self._stack[-1]
|
||||
if self.debug:
|
||||
self._debug_leave(node)
|
||||
|
||||
def visit_Add(self, node):
|
||||
self._handle_bin_op(node, "+")
|
||||
|
||||
def _is_augmented_assign(self):
|
||||
"""Is it 'Augmented_assign' (operators +=/-=, etc)?"""
|
||||
return self._stack and isinstance(self._stack[-1], ast.AugAssign)
|
||||
|
||||
def visit_AugAssign(self, node):
|
||||
"""'Augmented_assign', Operators +=/-=, etc."""
|
||||
self.INDENT()
|
||||
self.generic_visit(node)
|
||||
self._output_file.write("\n")
|
||||
|
||||
def visit_Assign(self, node):
|
||||
self.INDENT()
|
||||
|
||||
qt_class = _is_qt_constructor(node)
|
||||
on_stack = qt_class and qt_class_flags(qt_class) & ClassFlag.INSTANTIATE_ON_STACK
|
||||
|
||||
# Is this a free variable and not a member assignment? Instantiate
|
||||
# on stack or give a type
|
||||
if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
||||
if qt_class:
|
||||
if on_stack:
|
||||
# "QFile f(args)"
|
||||
var = node.targets[0].id
|
||||
self._stack_variables.append(var)
|
||||
self._output_file.write(f"{qt_class} {var}(")
|
||||
self._write_function_args(node.value.args)
|
||||
self._output_file.write(");\n")
|
||||
return
|
||||
self._output_file.write("auto *")
|
||||
|
||||
line_no = node.lineno if hasattr(node, 'lineno') else -1
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Tuple):
|
||||
w = f"{self._file_name}:{line_no}: List assignment not handled."
|
||||
warnings.warn(w)
|
||||
elif isinstance(target, ast.Subscript):
|
||||
w = f"{self._file_name}:{line_no}: Subscript assignment not handled."
|
||||
warnings.warn(w)
|
||||
else:
|
||||
self._output_file.write(format_reference(target))
|
||||
self._output_file.write(' = ')
|
||||
if qt_class and not on_stack:
|
||||
self._output_file.write("new ")
|
||||
self.visit(node.value)
|
||||
self._output_file.write(';\n')
|
||||
|
||||
def visit_Attribute(self, node):
|
||||
"""Format a variable reference (cf visit_Name)"""
|
||||
# Default parameter (like Qt::black)?
|
||||
if self._ignore_function_def_node(node):
|
||||
return
|
||||
self._output_file.write(format_reference(node))
|
||||
|
||||
def visit_BinOp(self, node):
|
||||
# Parentheses are not exposed, so, every binary operation needs to
|
||||
# be enclosed by ().
|
||||
self._output_file.write('(')
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(')')
|
||||
|
||||
def _handle_bin_op(self, node, op):
|
||||
"""Handle a binary operator which can appear as 'Augmented Assign'."""
|
||||
self.generic_visit(node)
|
||||
full_op = f" {op}= " if self._is_augmented_assign() else f" {op} "
|
||||
self._output_file.write(full_op)
|
||||
|
||||
def visit_BitAnd(self, node):
|
||||
self._handle_bin_op(node, "&")
|
||||
|
||||
def visit_BitOr(self, node):
|
||||
self._handle_bin_op(node, "|")
|
||||
|
||||
def _format_call(self, node):
|
||||
# Decorator list?
|
||||
if self._ignore_function_def_node(node):
|
||||
return
|
||||
f = node.func
|
||||
if isinstance(f, ast.Name):
|
||||
self._output_file.write(f.id)
|
||||
else:
|
||||
# Attributes denoting chained calls "a->b()->c()". Walk along in
|
||||
# reverse order, recursing for other calls.
|
||||
names = []
|
||||
n = f
|
||||
while isinstance(n, ast.Attribute):
|
||||
names.insert(0, n.attr)
|
||||
n = n.value
|
||||
|
||||
if isinstance(n, ast.Name): # Member or variable reference
|
||||
if n.id != "self":
|
||||
sep = "->"
|
||||
if n.id in self._stack_variables:
|
||||
sep = "."
|
||||
elif n.id[0:1].isupper(): # Heuristics for static
|
||||
sep = "::"
|
||||
self._output_file.write(n.id)
|
||||
self._output_file.write(sep)
|
||||
elif isinstance(n, ast.Call): # A preceding call
|
||||
self._format_call(n)
|
||||
self._output_file.write("->")
|
||||
|
||||
self._output_file.write("->".join(names))
|
||||
|
||||
self._output_file.write('(')
|
||||
self._write_function_args(node.args)
|
||||
self._output_file.write(')')
|
||||
|
||||
def visit_Call(self, node):
|
||||
self._format_call(node)
|
||||
# Context manager expression?
|
||||
if self._within_context_manager():
|
||||
self._output_file.write(";\n")
|
||||
|
||||
def _write_function_args(self, args_node):
|
||||
# Manually do visit(), skip the children of func
|
||||
for i, arg in enumerate(args_node):
|
||||
if i > 0:
|
||||
self._output_file.write(', ')
|
||||
self.visit(arg)
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
# Manually do visit() to skip over base classes
|
||||
# and annotations
|
||||
self._class_scope.append(node.name)
|
||||
self.write_class_def(node)
|
||||
self.indent()
|
||||
for b in node.body:
|
||||
self.visit(b)
|
||||
self.dedent()
|
||||
self.indent_line('};')
|
||||
del self._class_scope[-1]
|
||||
|
||||
def visit_Div(self, node):
|
||||
self._handle_bin_op(node, "/")
|
||||
|
||||
def visit_Eq(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(" == ")
|
||||
|
||||
def visit_Expr(self, node):
|
||||
self.INDENT()
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(';\n')
|
||||
|
||||
def visit_Gt(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(" > ")
|
||||
|
||||
def visit_GtE(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(" >= ")
|
||||
|
||||
def visit_For(self, node):
|
||||
# Manually do visit() to get the indentation right.
|
||||
# TODO: what about orelse?
|
||||
self.indent_line(format_for_loop(node))
|
||||
self.indent()
|
||||
for b in node.body:
|
||||
self.visit(b)
|
||||
self.dedent()
|
||||
self.indent_line('}')
|
||||
|
||||
def visit_FunctionDef(self, node):
|
||||
class_context = self._class_scope[-1] if self._class_scope else None
|
||||
for decorator in node.decorator_list:
|
||||
func = decorator.func # (Call)
|
||||
if isinstance(func, ast.Name) and func.id == "Slot":
|
||||
self._output_file.write("\npublic slots:")
|
||||
self.write_function_def(node, class_context)
|
||||
# Find stack variables
|
||||
for arg in node.args.args:
|
||||
if arg.annotation and isinstance(arg.annotation, ast.Name):
|
||||
type_name = arg.annotation.id
|
||||
flags = qt_class_flags(type_name)
|
||||
if flags & ClassFlag.PASS_ON_STACK_MASK:
|
||||
self._stack_variables.append(arg.arg)
|
||||
self.indent()
|
||||
self.generic_visit(node)
|
||||
self.dedent()
|
||||
self.indent_line('}')
|
||||
self._stack_variables.clear()
|
||||
|
||||
def visit_If(self, node):
|
||||
# Manually do visit() to get the indentation right. Note:
|
||||
# elsif() is modelled as nested if.
|
||||
|
||||
# Check for the main function
|
||||
if _is_if_main(node):
|
||||
self._output_file.write("\nint main(int argc, char *argv[])\n{\n")
|
||||
self.indent()
|
||||
for b in node.body:
|
||||
self.visit(b)
|
||||
self.indent_string("return 0;\n")
|
||||
self.dedent()
|
||||
self._output_file.write("}\n")
|
||||
return
|
||||
|
||||
self.indent_string('if (')
|
||||
self.visit(node.test)
|
||||
self._output_file.write(') {\n')
|
||||
self.indent()
|
||||
for b in node.body:
|
||||
self.visit(b)
|
||||
self.dedent()
|
||||
self.indent_string('}')
|
||||
if node.orelse:
|
||||
self._output_file.write(' else {\n')
|
||||
self.indent()
|
||||
for b in node.orelse:
|
||||
self.visit(b)
|
||||
self.dedent()
|
||||
self.indent_string('}')
|
||||
self._output_file.write('\n')
|
||||
|
||||
def visit_Import(self, node):
|
||||
write_import(self._output_file, node)
|
||||
|
||||
def visit_ImportFrom(self, node):
|
||||
write_import_from(self._output_file, node)
|
||||
|
||||
def visit_List(self, node):
|
||||
# Manually do visit() to get separators right
|
||||
self._output_file.write('{')
|
||||
for i, el in enumerate(node.elts):
|
||||
if i > 0:
|
||||
self._output_file.write(', ')
|
||||
self.visit(el)
|
||||
self._output_file.write('}')
|
||||
|
||||
def visit_LShift(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(" << ")
|
||||
|
||||
def visit_Lt(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(" < ")
|
||||
|
||||
def visit_LtE(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(" <= ")
|
||||
|
||||
def visit_Mult(self, node):
|
||||
self._handle_bin_op(node, "*")
|
||||
|
||||
def _within_context_manager(self):
|
||||
"""Return whether we are within a context manager (with)."""
|
||||
parent = self._stack[-1] if self._stack else None
|
||||
return parent and isinstance(parent, ast.withitem)
|
||||
|
||||
def _ignore_function_def_node(self, node):
|
||||
"""Should this node be ignored within a FunctionDef."""
|
||||
if not self._stack:
|
||||
return False
|
||||
parent = self._stack[-1]
|
||||
# A type annotation or default value of an argument?
|
||||
if isinstance(parent, (ast.arguments, ast.arg)):
|
||||
return True
|
||||
if not isinstance(parent, ast.FunctionDef):
|
||||
return False
|
||||
# Return type annotation or decorator call
|
||||
return node == parent.returns or node in parent.decorator_list
|
||||
|
||||
def visit_Index(self, node):
|
||||
self._output_file.write("[")
|
||||
self.generic_visit(node)
|
||||
self._output_file.write("]")
|
||||
|
||||
def visit_Name(self, node):
|
||||
"""Format a variable reference (cf visit_Attribute)"""
|
||||
# Skip Context manager variables, return or argument type annotation
|
||||
if self._within_context_manager() or self._ignore_function_def_node(node):
|
||||
return
|
||||
self._output_file.write(format_reference(node))
|
||||
|
||||
def visit_NameConstant(self, node):
|
||||
# Default parameter?
|
||||
if self._ignore_function_def_node(node):
|
||||
return
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(format_name_constant(node))
|
||||
|
||||
def visit_Not(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write("!")
|
||||
|
||||
def visit_NotEq(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(" != ")
|
||||
|
||||
def visit_Num(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(format_literal(node))
|
||||
|
||||
def visit_RShift(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(" >> ")
|
||||
|
||||
def visit_Return(self, node):
|
||||
self.indent_string("return")
|
||||
if node.value:
|
||||
self._output_file.write(" ")
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(";\n")
|
||||
|
||||
def visit_Slice(self, node):
|
||||
self._output_file.write("[")
|
||||
if node.lower:
|
||||
self.visit(node.lower)
|
||||
self._output_file.write(":")
|
||||
if node.upper:
|
||||
self.visit(node.upper)
|
||||
self._output_file.write("]")
|
||||
|
||||
def visit_Str(self, node):
|
||||
self.generic_visit(node)
|
||||
self._output_file.write(format_literal(node))
|
||||
|
||||
def visit_Sub(self, node):
|
||||
self._handle_bin_op(node, "-")
|
||||
|
||||
def visit_UnOp(self, node):
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_With(self, node):
|
||||
self.INDENT()
|
||||
self._output_file.write("{ // Converted from context manager\n")
|
||||
self.indent()
|
||||
for item in node.items:
|
||||
self.INDENT()
|
||||
if item.optional_vars:
|
||||
self._output_file.write(format_reference(item.optional_vars))
|
||||
self._output_file.write(" = ")
|
||||
self.generic_visit(node)
|
||||
self.dedent()
|
||||
self.INDENT()
|
||||
self._output_file.write("}\n")
|
||||
|
||||
def _debug_enter(self, node, parent=None):
|
||||
message = '{}>generic_visit({})'.format(' ' * self ._debug_indent,
|
||||
debug_format_node(node))
|
||||
if parent:
|
||||
message += ', parent={}'.format(debug_format_node(parent))
|
||||
message += '\n'
|
||||
sys.stderr.write(message)
|
||||
self._debug_indent += 1
|
||||
|
||||
def _debug_leave(self, node):
|
||||
self._debug_indent -= 1
|
||||
message = '{}<generic_visit({})\n'.format(' ' * self ._debug_indent,
|
||||
type(node).__name__)
|
||||
sys.stderr.write(message)
|
||||
Reference in New Issue
Block a user