exe 依赖添加
This commit is contained in:
0
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/__init__.py
vendored
Normal file
0
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/__init__.py
vendored
Normal file
218
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy.py
vendored
Normal file
218
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy.py
vendored
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)
|
||||
67
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/__init__.py
vendored
Normal file
67
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/__init__.py
vendored
Normal file
@@ -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
|
||||
63
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/commands.py
vendored
Normal file
63
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/commands.py
vendored
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 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
|
||||
532
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/config.py
vendored
Normal file
532
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/config.py
vendored
Normal file
@@ -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
|
||||
98
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/default.spec
vendored
Normal file
98
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/default.spec
vendored
Normal file
@@ -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 =
|
||||
337
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/dependency_util.py
vendored
Normal file
337
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/dependency_util.py
vendored
Normal file
@@ -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)
|
||||
106
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/deploy_util.py
vendored
Normal file
106
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/deploy_util.py
vendored
Normal file
@@ -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()}")
|
||||
184
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/nuitka_helper.py
vendored
Normal file
184
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/nuitka_helper.py
vendored
Normal file
@@ -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
|
||||
BIN
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/pyside_icon.icns
vendored
Normal file
BIN
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/pyside_icon.icns
vendored
Normal file
Binary file not shown.
BIN
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/pyside_icon.ico
vendored
Normal file
BIN
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/pyside_icon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/pyside_icon.jpg
vendored
Normal file
BIN
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/pyside_icon.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.0 KiB |
123
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/python_helper.py
vendored
Normal file
123
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/deploy_lib/python_helper.py
vendored
Normal file
@@ -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
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/metaobjectdump.py
vendored
Normal file
461
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/metaobjectdump.py
vendored
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
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project.py
vendored
Normal file
348
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project.py
vendored
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))
|
||||
53
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/__init__.py
vendored
Normal file
53
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/__init__.py
vendored
Normal file
@@ -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
|
||||
65
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/design_studio_project.py
vendored
Normal file
65
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/design_studio_project.py
vendored
Normal file
@@ -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)
|
||||
189
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/newproject.py
vendored
Normal file
189
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/newproject.py
vendored
Normal file
@@ -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
|
||||
259
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/project_data.py
vendored
Normal file
259
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/project_data.py
vendored
Normal file
@@ -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)
|
||||
58
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/pyproject_json.py
vendored
Normal file
58
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/pyproject_json.py
vendored
Normal file
@@ -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
|
||||
10
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/pyproject_parse_result.py
vendored
Normal file
10
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/pyproject_parse_result.py
vendored
Normal file
@@ -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)
|
||||
275
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/pyproject_toml.py
vendored
Normal file
275
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/pyproject_toml.py
vendored
Normal file
@@ -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
|
||||
194
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/utils.py
vendored
Normal file
194
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/project_lib/utils.py
vendored
Normal file
@@ -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
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/pyside_tool.py
vendored
Normal file
264
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/pyside_tool.py
vendored
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
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qml.py
vendored
Normal file
248
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qml.py
vendored
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
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp.py
vendored
Normal file
63
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp.py
vendored
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}.")
|
||||
112
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/astdump.py
vendored
Normal file
112
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/astdump.py
vendored
Normal file
@@ -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)
|
||||
266
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/formatter.py
vendored
Normal file
266
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/formatter.py
vendored
Normal file
@@ -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('{')
|
||||
51
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/nodedump.py
vendored
Normal file
51
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/nodedump.py
vendored
Normal file
@@ -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__
|
||||
63
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/qt.py
vendored
Normal file
63
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/qt.py
vendored
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
|
||||
|
||||
"""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)
|
||||
56
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/tokenizer.py
vendored
Normal file
56
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/tokenizer.py
vendored
Normal file
@@ -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))
|
||||
443
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/visitor.py
vendored
Normal file
443
etl_billiards/dist/ETL_Manager/_internal/PySide6/scripts/qtpy2cpp_lib/visitor.py
vendored
Normal file
@@ -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