diff --git a/README.md b/README.md index 511653b4..b409fe1d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ configuration, specifically settings stored in `~/.deadline/*` ### ui -This submodule contains Qt GUIs, based on PySide2, for common controls +This submodule contains Qt GUIs, based on PySide(2/6), for common controls and widgets used in interactive submitters, and to display the status of various AWS Deadline Cloud resoruces. @@ -118,3 +118,8 @@ hatch run all:test ``` ./publish.sh ``` + +# Optional Third Party Dependencies - GUI + +N.B.: Although this repository is released under the Apache-2.0 license, its optional GUI feature +uses the third party Qt && PySide projects. The Qt and PySide projects' licensing includes the LGPL-3.0 license. diff --git a/hatch.toml b/hatch.toml index a87f70d4..497e7de4 100644 --- a/hatch.toml +++ b/hatch.toml @@ -7,9 +7,9 @@ pre-install-commands = [ sync = "pip install -r requirements-testing.txt" test = "pytest --cov-config pyproject.toml {args:test/unit}" test_docker = "./scripts/run_sudo_tests.sh --build" -typing = "mypy {args:src test}" +typing = "mypy {args:src test} --always-false=PYQT5 --always-false=PYSIDE2 --always-false=PYQT6 --always-true=PYSIDE6" style = [ - "ruff {args:.}", + "ruff check {args:.}", "black --check --diff {args:.}", ] fmt = [ diff --git a/pyproject.toml b/pyproject.toml index 756c7f11..2c8a946d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" name = "deadline" dynamic = ["version"] readme = "README.md" -license = "" +license = "Apache-2.0" requires-python = ">=3.7" # Note: All deps should be using >= since this is a *library* as well as an application. @@ -24,6 +24,13 @@ dependencies = [ # Pinning due to new 4.18 dependencies breaking pyinstaller implementation "jsonschema == 4.17.*", "pywin32 == 306; sys_platform == 'win32'", + "QtPy == 2.4.*", +] + +[project.optional-dependencies] +gui = [ + # If the version changes, update the version in deadline/client/ui/__init__.py + "PySide6-essentials == 6.6.*", ] [project.scripts] @@ -63,8 +70,6 @@ include = [ [tool.hatch.build.targets.wheel] packages = [ "src/deadline", - # TODO: Remove this once consumers update to deadline.job_attachments - "src/deadline_job_attachments", ] [tool.mypy] @@ -84,7 +89,7 @@ mypy_path = "src" [[tool.mypy.overrides]] module = [ - "PySide2.*", + "qtpy.*", "boto3.*", "botocore.*", "moto.*", @@ -92,13 +97,21 @@ module = [ "jsonschema", ] +[[tool.mypy.overrides]] +module = "deadline.client.ui.*" +# 1. [attr-defined] - It thinks Qt, etc. are types and can't see their attributes +# 2. [assignment] - we have a lot of self.layout assignments in QWidgets +disable_error_code = ["attr-defined", "assignment"] + [tool.ruff] +line-length = 100 + +[tool.ruff.lint] ignore = [ "E501", ] -line-length = 100 -[tool.ruff.isort] +[tool.ruff.lint.isort] known-first-party = [ "deadline" ] diff --git a/requirements-testing.txt b/requirements-testing.txt index 6442eb19..f5fc2679 100644 --- a/requirements-testing.txt +++ b/requirements-testing.txt @@ -1,16 +1,17 @@ coverage[toml] ~= 7.2; python_version == '3.7' -coverage[toml] ~= 7.4; python_version > '3.7' -pytest ~= 7.4 -pytest-cov ~= 4.1 -pytest-timeout ~= 2.2 -pytest-xdist ~= 3.5 -freezegun ~= 1.4 -types-pyyaml ~= 6.0 -twine ~= 4.0 +coverage[toml] == 7.*; python_version > '3.7' +pytest == 7.* +pytest-cov == 4.* +pytest-timeout == 2.* +pytest-xdist == 3.* +freezegun == 1.* +types-pyyaml == 6.* +twine == 4.*; python_version == '3.7' +twine == 5.*; python_version > '3.7' black == 23.3.*; python_version == '3.7' black == 23.*; python_version > '3.7' -mypy ~= 1.4; python_version == '3.7' -mypy ~= 1.8; python_version > '3.7' -ruff ~= 0.2.1 -moto ~= 4.2 -jsondiff ~= 2.0 +mypy == 1.4.*; python_version == '3.7' +mypy == 1.*; python_version > '3.7' +ruff == 0.3.* +moto == 4.* +jsondiff == 2.* diff --git a/src/deadline/client/cli/_groups/bundle_group.py b/src/deadline/client/cli/_groups/bundle_group.py index c729486e..a729e8b7 100644 --- a/src/deadline/client/cli/_groups/bundle_group.py +++ b/src/deadline/client/cli/_groups/bundle_group.py @@ -226,9 +226,9 @@ def bundle_gui_submit(job_bundle_dir, browse, **args): if not submitter: return - response = submitter.show() + submitter.show() - app.exec_() + app.exec() response = None if submitter: diff --git a/src/deadline/client/ui/__init__.py b/src/deadline/client/ui/__init__.py index 4542df0f..be9b74a6 100644 --- a/src/deadline/client/ui/__init__.py +++ b/src/deadline/client/ui/__init__.py @@ -33,7 +33,7 @@ def gui_error_handler(message_title: str, parent: Any = None): """ try: - from PySide2.QtWidgets import QMessageBox + from qtpy.QtWidgets import QMessageBox yield except DeadlineOperationError as e: @@ -57,22 +57,79 @@ def gui_context_for_cli(): show_cli_job_submitter() - app.exec_() + app.exec() """ + import importlib + from os.path import basename, dirname, join, normpath + import shlex + import shutil + import subprocess import sys from pathlib import Path import click + has_pyside = importlib.util.find_spec("PySide6") or importlib.util.find_spec("PySide2") + if not has_pyside: + message = "Optional GUI components for deadline are unavailable. Would you like to install PySide?" + will_install_gui = click.confirm(message, default=False) + if not will_install_gui: + click.echo("Unable to continue without GUI, exiting") + sys.exit(1) + + # this should match what's in the pyproject.toml + pyside6_pypi = "PySide6-essentials==6.6.*" + if "deadline" in basename(sys.executable).lower(): + # running with a deadline executable, not standard python. + # So exit the deadline folder into the main deps dir + deps_folder = normpath( + join( + dirname(__file__), + "..", + "..", + "..", + ) + ) + runtime_version = f"{sys.version_info.major}.{sys.version_info.minor}" + pip_command = [ + "-m", + "pip", + "install", + pyside6_pypi, + "--python-version", + runtime_version, + "--only-binary=:all:", + "-t", + deps_folder, + ] + python_executable = shutil.which("python3") or shutil.which("python") + if python_executable: + command = " ".join(shlex.quote(v) for v in [python_executable] + pip_command) + subprocess.run([python_executable] + pip_command) + else: + click.echo( + "Unable to install GUI dependencies, if you have python available you can install it by running:" + ) + click.echo() + click.echo(f"\t{' '.join(shlex.quote(v) for v in ['python'] + pip_command)}") + click.echo() + sys.exit(1) + else: + # standard python sys.executable + # TODO: swap to deadline[gui]==version once published and at the same + # time consider local editables `pip install .[gui]` + subprocess.run([sys.executable, "-m", "pip", "install", pyside6_pypi]) + try: - from PySide2.QtGui import QIcon - from PySide2.QtWidgets import QApplication, QMessageBox + from qtpy.QtGui import QIcon + from qtpy.QtWidgets import QApplication, QMessageBox except ImportError as e: - click.echo(f"Failed to import PySide2, which is required to show the GUI:\n{e}") + click.echo(f"Failed to import qtpy/PySide/Qt, which is required to show the GUI:\n{e}") sys.exit(1) try: app = QApplication(sys.argv) + app.setApplicationName("AWS Deadline Cloud") icon = QIcon(str(Path(__file__).parent.parent / "ui" / "resources" / "deadline_logo.svg")) app.setWindowIcon(icon) @@ -84,7 +141,7 @@ def gui_context_for_cli(): command = f"{os.path.basename(sys.argv[0])} " + " ".join( shlex.quote(v) for v in sys.argv[1:] ) - QMessageBox.warning(None, f'Error running "{command}"', str(e)) + QMessageBox.warning(None, f'Error running "{command}"', str(e)) # type: ignore[call-overload] except Exception: import os import shlex @@ -93,7 +150,7 @@ def gui_context_for_cli(): command = f"{os.path.basename(sys.argv[0])} " + " ".join( shlex.quote(v) for v in sys.argv[1:] ) - QMessageBox.warning( + QMessageBox.warning( # type: ignore[call-overload] None, f'Error running "{command}"', f"Exception caught:\n{traceback.format_exc()}" ) diff --git a/src/deadline/client/ui/cli_job_submitter.py b/src/deadline/client/ui/cli_job_submitter.py index ca057a0e..8ad4031d 100644 --- a/src/deadline/client/ui/cli_job_submitter.py +++ b/src/deadline/client/ui/cli_job_submitter.py @@ -8,8 +8,8 @@ from typing import Any, Dict, Optional import copy -from PySide2.QtCore import Qt # pylint: disable=import-error -from PySide2.QtWidgets import ( # pylint: disable=import-error; type: ignore +from qtpy.QtCore import Qt # pylint: disable=import-error +from qtpy.QtWidgets import ( # pylint: disable=import-error; type: ignore QApplication, QMainWindow, ) @@ -43,7 +43,7 @@ def show_cli_job_submitter(parent=None, f=Qt.WindowFlags()) -> None: if parent is None: # Get the main application window so we can parent ours to it app = QApplication.instance() - parent = [widget for widget in app.topLevelWidgets() if isinstance(widget, QMainWindow)][0] + parent = [widget for widget in app.topLevelWidgets() if isinstance(widget, QMainWindow)][0] # type: ignore[union-attr] def on_create_job_bundle_callback( widget: SubmitJobToDeadlineDialog, diff --git a/src/deadline/client/ui/deadline_authentication_status.py b/src/deadline/client/ui/deadline_authentication_status.py index d02f8119..5b54f019 100644 --- a/src/deadline/client/ui/deadline_authentication_status.py +++ b/src/deadline/client/ui/deadline_authentication_status.py @@ -26,7 +26,7 @@ from logging import getLogger from typing import Optional -from PySide2.QtCore import QObject, QFileSystemWatcher, Signal +from qtpy.QtCore import QObject, QFileSystemWatcher, Signal from .. import api from ..config import config_file diff --git a/src/deadline/client/ui/dev_application.py b/src/deadline/client/ui/dev_application.py index 5bb7fdae..9cd4a710 100644 --- a/src/deadline/client/ui/dev_application.py +++ b/src/deadline/client/ui/dev_application.py @@ -7,9 +7,9 @@ from logging import getLogger from pathlib import Path -from PySide2.QtCore import Qt -from PySide2.QtGui import QColor, QIcon, QPalette -from PySide2.QtWidgets import QApplication, QFileDialog, QMainWindow, QStyleFactory +from qtpy.QtCore import Qt +from qtpy.QtGui import QColor, QIcon, QPalette +from qtpy.QtWidgets import QApplication, QFileDialog, QMainWindow, QStyleFactory from .. import api from .cli_job_submitter import show_cli_job_submitter @@ -35,7 +35,7 @@ def __init__(self, parent=None): # Remove the central widget. This leaves us with just dockable widgets, which provides # the most flexibility, since we don't really have a "main" widget. - self.setCentralWidget(None) + self.setCentralWidget(None) # type: ignore[arg-type] self.setDockOptions( QMainWindow.AllowNestedDocks | QMainWindow.AllowTabbedDocks | QMainWindow.AnimatedDocks @@ -142,9 +142,9 @@ def app() -> None: + """);}""" ) - window = DevMainWindow() - window.show() + main_window = DevMainWindow() + main_window.show() - window.submit_job_bundle() + main_window.submit_job_bundle() app.exec_() diff --git a/src/deadline/client/ui/dialogs/deadline_config_dialog.py b/src/deadline/client/ui/dialogs/deadline_config_dialog.py index 5b9c9e67..c55a7e32 100644 --- a/src/deadline/client/ui/dialogs/deadline_config_dialog.py +++ b/src/deadline/client/ui/dialogs/deadline_config_dialog.py @@ -19,8 +19,8 @@ import boto3 # type: ignore[import] from botocore.exceptions import ProfileNotFound # type: ignore[import] from deadline.job_attachments.models import FileConflictResolution, JobAttachmentsFileSystem -from PySide2.QtCore import QSize, Qt, Signal -from PySide2.QtWidgets import ( # pylint: disable=import-error; type: ignore +from qtpy.QtCore import QSize, Qt, Signal +from qtpy.QtWidgets import ( # pylint: disable=import-error; type: ignore QApplication, QCheckBox, QComboBox, @@ -401,7 +401,7 @@ def _init_combobox_setting_with_tooltips( """ Creates and adds a combo box setting to the given group and layout, similar to `_init_combobox_setting` method. This method differentiates itself by adding tooltips for label and combo box items. Also, - appends an (PySide2's built-in) Information icon at the label end to indicate tooltip availability. + appends an (PySide6's built-in) Information icon at the label end to indicate tooltip availability. Args: group (QWidget): The parent of the combobox diff --git a/src/deadline/client/ui/dialogs/deadline_login_dialog.py b/src/deadline/client/ui/dialogs/deadline_login_dialog.py index 6bac6d93..7f0b93e8 100644 --- a/src/deadline/client/ui/dialogs/deadline_login_dialog.py +++ b/src/deadline/client/ui/dialogs/deadline_login_dialog.py @@ -17,8 +17,8 @@ from configparser import ConfigParser from typing import Optional -from PySide2.QtCore import Signal -from PySide2.QtWidgets import ( # pylint: disable=import-error; type: ignore +from qtpy.QtCore import Signal +from qtpy.QtWidgets import ( # pylint: disable=import-error; type: ignore QApplication, QMessageBox, ) @@ -171,14 +171,11 @@ def on_button_clicked(self, button): self.canceled = True if self.__login_thread: while self.__login_thread.is_alive(): - QApplication.instance().processEvents() + QApplication.instance().processEvents() # type: ignore[union-attr] def exec(self) -> bool: """ Runs the modal login dialog, returning True if the login was successful, False otherwise. """ - return super().exec_() == QMessageBox.Ok - - def exec_(self) -> bool: - return self.exec() + return super().exec() == QMessageBox.Ok diff --git a/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py b/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py index 544978f1..3cc55a87 100644 --- a/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_progress_dialog.py @@ -13,9 +13,9 @@ from typing import Any, Dict, List, Optional, cast from botocore.client import BaseClient # type: ignore[import] -from PySide2.QtCore import Qt, Signal -from PySide2.QtGui import QCloseEvent -from PySide2.QtWidgets import ( # pylint: disable=import-error; type: ignore +from qtpy.QtCore import Qt, Signal +from qtpy.QtGui import QCloseEvent +from qtpy.QtWidgets import ( # pylint: disable=import-error; type: ignore QApplication, QDialog, QDialogButtonBox, @@ -75,9 +75,9 @@ class SubmitJobProgressDialog(QDialog): create_job_thread_exception = Signal(BaseException) # These signals are sent when the background threads succeed. - hashing_thread_succeeded = Signal([SummaryStatistics, list]) - upload_thread_succeeded = Signal([SummaryStatistics, dict]) - create_job_thread_succeeded = Signal([bool, str]) + hashing_thread_succeeded = Signal(SummaryStatistics, list) + upload_thread_succeeded = Signal(SummaryStatistics, dict) + create_job_thread_succeeded = Signal(bool, str) # These signals are sent when the progress reporting callbacks are called # from job attachments during hashing/uploading. @@ -581,7 +581,7 @@ def _confirm_asset_references_outside_storage_profile( message_box.addButton(dont_ask_button, QMessageBox.ActionRole) message_box.setWindowTitle("Job Attachments Valid Files Confirmation") - selection = message_box.exec_() + selection = message_box.exec() return selection != QMessageBox.Cancel @@ -611,20 +611,17 @@ def _shutdown_threads(self) -> None: for thread in threads: if thread: while thread.is_alive(): - QApplication.instance().processEvents() + QApplication.instance().processEvents() # type: ignore[union-attr] - def exec(self) -> Optional[Dict[str, Any]]: + def exec(self) -> Optional[Dict[str, Any]]: # type: ignore[override] """ Runs the modal job progress dialog, returns the response from calling create job if the dialog was accepted. Otherwise returns None """ - if super().exec_() == QDialog.Accepted: + if super().exec() == QDialog.Accepted: return self._create_job_response return None - def exec_(self) -> Optional[Dict[str, Any]]: - return self.exec() - class JobAttachmentsProgressWidget(QGroupBox): """ diff --git a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py index ac2a8205..6ac2074d 100644 --- a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py @@ -8,9 +8,9 @@ import sys from typing import Any, Dict, Optional -from PySide2.QtCore import QSize, Qt # pylint: disable=import-error -from PySide2.QtGui import QKeyEvent # pylint: disable=import-error -from PySide2.QtWidgets import ( # pylint: disable=import-error; type: ignore +from qtpy.QtCore import QSize, Qt # pylint: disable=import-error +from qtpy.QtGui import QKeyEvent # pylint: disable=import-error +from qtpy.QtWidgets import ( # pylint: disable=import-error; type: ignore QApplication, QDialog, QDialogButtonBox, @@ -76,7 +76,7 @@ class SubmitJobToDeadlineDialog(QDialog): def __init__( self, *, - job_setup_widget_type: QWidget, + job_setup_widget_type: type[QWidget], initial_job_settings, initial_shared_parameter_values: dict[str, Any], auto_detected_attachments: AssetReferences, @@ -396,7 +396,7 @@ def on_submit(self): job_progress_dialog = SubmitJobProgressDialog(parent=self) job_progress_dialog.show() - QApplication.instance().processEvents() + QApplication.instance().processEvents() # type: ignore[union-attr] # Submit the job try: diff --git a/src/deadline/client/ui/job_bundle_submitter.py b/src/deadline/client/ui/job_bundle_submitter.py index b55aa83d..385871d6 100644 --- a/src/deadline/client/ui/job_bundle_submitter.py +++ b/src/deadline/client/ui/job_bundle_submitter.py @@ -6,8 +6,8 @@ from logging import getLogger from typing import Any, Optional, Dict -from PySide2.QtCore import Qt # pylint: disable=import-error -from PySide2.QtWidgets import ( # pylint: disable=import-error; type: ignore +from qtpy.QtCore import Qt # pylint: disable=import-error +from qtpy.QtWidgets import ( # pylint: disable=import-error; type: ignore QApplication, QFileDialog, QMainWindow, @@ -52,7 +52,7 @@ def show_job_bundle_submitter( # Get the main application window so we can parent ours to it app = QApplication.instance() main_windows = [ - widget for widget in app.topLevelWidgets() if isinstance(widget, QMainWindow) + widget for widget in app.topLevelWidgets() if isinstance(widget, QMainWindow) # type: ignore[union-attr] ] if main_windows: parent = main_windows[0] diff --git a/src/deadline/client/ui/widgets/cli_job_settings_tab.py b/src/deadline/client/ui/widgets/cli_job_settings_tab.py index 9581b2e4..a4b82be0 100644 --- a/src/deadline/client/ui/widgets/cli_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/cli_job_settings_tab.py @@ -5,8 +5,8 @@ """ import os -from PySide2.QtCore import Qt # type: ignore -from PySide2.QtWidgets import ( # type: ignore +from qtpy.QtCore import Qt # type: ignore +from qtpy.QtWidgets import ( # type: ignore QCheckBox, QComboBox, QGridLayout, diff --git a/src/deadline/client/ui/widgets/deadline_authentication_status_widget.py b/src/deadline/client/ui/widgets/deadline_authentication_status_widget.py index 808d6ec7..c342b177 100644 --- a/src/deadline/client/ui/widgets/deadline_authentication_status_widget.py +++ b/src/deadline/client/ui/widgets/deadline_authentication_status_widget.py @@ -8,8 +8,8 @@ from logging import getLogger from typing import Optional -from PySide2.QtCore import Qt -from PySide2.QtWidgets import ( # pylint: disable=import-error; type: ignore +from qtpy.QtCore import Qt +from qtpy.QtWidgets import ( # pylint: disable=import-error; type: ignore QFormLayout, QGroupBox, QHBoxLayout, diff --git a/src/deadline/client/ui/widgets/host_requirements_tab.py b/src/deadline/client/ui/widgets/host_requirements_tab.py index beb84171..aff14e0e 100644 --- a/src/deadline/client/ui/widgets/host_requirements_tab.py +++ b/src/deadline/client/ui/widgets/host_requirements_tab.py @@ -5,9 +5,10 @@ """ from typing import Any, Dict, List, Optional, Union from pathlib import Path -from PySide2.QtCore import Qt # type: ignore -from PySide2.QtGui import QFont, QValidator, QIntValidator, QBrush, QIcon, QRegExpValidator -from PySide2.QtWidgets import ( # type: ignore + +from qtpy.QtCore import Qt # type: ignore +from qtpy.QtGui import QFont, QValidator, QIntValidator, QBrush, QIcon, QRegularExpressionValidator # type: ignore +from qtpy.QtWidgets import ( # type: ignore QComboBox, QGroupBox, QHBoxLayout, @@ -485,7 +486,9 @@ def _build_ui(self): self.name_label.setFixedWidth(LABEL_FIXED_WIDTH) self.name_line_edit = QLineEdit() self.name_line_edit.setFixedWidth(LABEL_FIXED_WIDTH) - self.name_line_edit.setValidator(QRegExpValidator(ATTRIBUTE_CAPABILITY_VALUE_REGEX)) + self.name_line_edit.setValidator( + QRegularExpressionValidator(ATTRIBUTE_CAPABILITY_VALUE_REGEX) + ) assert (100 - len(AMOUNT_CAPABILITY_PREFIX)) > 0 self.name_line_edit.setMaxLength(100 - len(AMOUNT_CAPABILITY_PREFIX)) @@ -583,7 +586,9 @@ def _build_ui(self): self.name_line_edit.setFixedWidth(LABEL_FIXED_WIDTH) assert (100 - len(ATTRIBUTE_CAPABILITY_PREFIX)) > 0 self.name_line_edit.setMaxLength(100 - len(ATTRIBUTE_CAPABILITY_PREFIX)) - self.name_line_edit.setValidator(QRegExpValidator(ATTRIBUTE_CAPABILITY_VALUE_REGEX)) + self.name_line_edit.setValidator( + QRegularExpressionValidator(ATTRIBUTE_CAPABILITY_VALUE_REGEX) + ) self.add_value_button = None self.top_row = QHBoxLayout() @@ -735,7 +740,7 @@ def __init__(self, value_list_item: QListWidgetItem, parent: CustomAttributeWidg self.line_edit = QLineEdit() self.line_edit.setFixedWidth(LABEL_FIXED_WIDTH + 20) self.line_edit.setMaxLength(100) - self.line_edit.setValidator(QRegExpValidator(ATTRIBUTE_CAPABILITY_VALUE_REGEX)) + self.line_edit.setValidator(QRegularExpressionValidator(ATTRIBUTE_CAPABILITY_VALUE_REGEX)) self.remove_button = QPushButton("Remove") self.remove_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) @@ -879,7 +884,7 @@ def validate(self, input: str, pos: int) -> QValidator.State: validator = QIntValidator() validator.setBottom(self.min) validator.setTop(self.max) - return validator.validate(input, pos) + return validator.validate(input, pos) # type: ignore[return-value] def valueFromText(self, text: str) -> int: """ diff --git a/src/deadline/client/ui/widgets/job_attachments_tab.py b/src/deadline/client/ui/widgets/job_attachments_tab.py index 56552169..7026c83d 100644 --- a/src/deadline/client/ui/widgets/job_attachments_tab.py +++ b/src/deadline/client/ui/widgets/job_attachments_tab.py @@ -8,7 +8,7 @@ from logging import getLogger from typing import Optional -from PySide2.QtWidgets import ( # type: ignore +from qtpy.QtWidgets import ( # type: ignore QAbstractItemView, QFileDialog, QHBoxLayout, diff --git a/src/deadline/client/ui/widgets/job_bundle_settings_tab.py b/src/deadline/client/ui/widgets/job_bundle_settings_tab.py index 61b514af..92edd88e 100644 --- a/src/deadline/client/ui/widgets/job_bundle_settings_tab.py +++ b/src/deadline/client/ui/widgets/job_bundle_settings_tab.py @@ -9,8 +9,8 @@ from logging import getLogger from typing import Any -from PySide2.QtCore import Signal # type: ignore -from PySide2.QtWidgets import ( # type: ignore +from qtpy.QtCore import Signal # type: ignore +from qtpy.QtWidgets import ( # type: ignore QVBoxLayout, QHBoxLayout, QWidget, diff --git a/src/deadline/client/ui/widgets/openjd_parameters_widget.py b/src/deadline/client/ui/widgets/openjd_parameters_widget.py index 41103b78..f350bae1 100644 --- a/src/deadline/client/ui/widgets/openjd_parameters_widget.py +++ b/src/deadline/client/ui/widgets/openjd_parameters_widget.py @@ -8,9 +8,9 @@ from typing import Any, Dict, List from copy import deepcopy -from PySide2.QtCore import QRegularExpression, Qt, Signal # type: ignore -from PySide2.QtGui import QValidator -from PySide2.QtWidgets import ( # type: ignore +from qtpy.QtCore import QRegularExpression, Qt, Signal # type: ignore +from qtpy.QtGui import QValidator +from qtpy.QtWidgets import ( # type: ignore QCheckBox, QComboBox, QDoubleSpinBox, diff --git a/src/deadline/client/ui/widgets/path_widgets.py b/src/deadline/client/ui/widgets/path_widgets.py index a6d82e1e..0341f37c 100644 --- a/src/deadline/client/ui/widgets/path_widgets.py +++ b/src/deadline/client/ui/widgets/path_widgets.py @@ -4,8 +4,8 @@ import os -from PySide2.QtCore import Signal -from PySide2.QtWidgets import ( # pylint: disable=import-error; type: ignore +from qtpy.QtCore import Signal +from qtpy.QtWidgets import ( # pylint: disable=import-error; type: ignore QFileDialog, QHBoxLayout, QLineEdit, diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index 4e6350f7..9549fa41 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -9,8 +9,8 @@ import threading from typing import Any, Dict, Optional -from PySide2.QtCore import Signal # type: ignore -from PySide2.QtWidgets import ( # type: ignore +from qtpy.QtCore import Signal # type: ignore +from qtpy.QtWidgets import ( # type: ignore QComboBox, QFormLayout, QGroupBox, diff --git a/src/deadline/client/ui/widgets/spinbox_widgets.py b/src/deadline/client/ui/widgets/spinbox_widgets.py index 8f094ba8..b4d76581 100644 --- a/src/deadline/client/ui/widgets/spinbox_widgets.py +++ b/src/deadline/client/ui/widgets/spinbox_widgets.py @@ -5,9 +5,9 @@ import enum from math import ceil, floor, log10 -from PySide2.QtCore import QEvent, QObject, Qt -from PySide2.QtGui import QCursor, QKeyEvent, QMouseEvent -from PySide2.QtWidgets import QAbstractSpinBox, QDoubleSpinBox, QSpinBox, QWidget +from qtpy.QtCore import QEvent, QObject, Qt +from qtpy.QtGui import QCursor, QKeyEvent, QMouseEvent +from qtpy.QtWidgets import QAbstractSpinBox, QDoubleSpinBox, QSpinBox, QWidget class DecimalMode(enum.Enum): @@ -32,7 +32,7 @@ class FloatDragSpinBox(QDoubleSpinBox): MAX_FLOAT_VALUE = 1e308 MIN_FLOAT_VALUE = -1e308 - def __init__(self, parent: QObject = None): + def __init__(self, parent: QWidget = None): super().__init__(parent) self.setKeyboardTracking(False) @@ -89,7 +89,7 @@ def mouseReleaseEvent(self, event: QMouseEvent) -> None: self.setCursor(Qt.SizeVerCursor) self._dragging = False - def stepEnabled(self) -> QAbstractSpinBox.StepEnabled: + def stepEnabled(self) -> QAbstractSpinBox.StepEnabled: # type: ignore[name-defined] """ Disables the arrow buttons if the user is using the hold and drag functionaltiy to change the value. @@ -220,7 +220,7 @@ def mouseReleaseEvent(self, event: QMouseEvent) -> None: self.setCursor(Qt.SizeVerCursor) self._is_dragging = False - def stepEnabled(self) -> QAbstractSpinBox.StepEnabled: + def stepEnabled(self) -> QAbstractSpinBox.StepEnabled: # type: ignore[name-defined] """ Disables the arrow buttons if the user is using the hold and drag functionaltiy to change the value. diff --git a/src/deadline/job_attachments/_aws/deadline.py b/src/deadline/job_attachments/_aws/deadline.py index 6c4db5c4..4c5beb36 100644 --- a/src/deadline/job_attachments/_aws/deadline.py +++ b/src/deadline/job_attachments/_aws/deadline.py @@ -86,25 +86,29 @@ def get_job( raise JobAttachmentsError(f'Failed to get job "{job_id}" from Deadline') from exc return Job( jobId=response["jobId"], - attachments=Attachments( - manifests=[ - ManifestProperties( - fileSystemLocationName=manifest_properties.get("fileSystemLocationName", None), - rootPath=manifest_properties["rootPath"], - rootPathFormat=PathFormat(manifest_properties["rootPathFormat"]), - outputRelativeDirectories=manifest_properties.get( - "outputRelativeDirectories", None - ), - inputManifestPath=manifest_properties.get("inputManifestPath", None), - ) - for manifest_properties in response["attachments"]["manifests"] - ], - fileSystem=JobAttachmentsFileSystem( - response["attachments"].get("fileSystem", JobAttachmentsFileSystem.COPIED.value) - ), - ) - if "attachments" in response and response["attachments"] - else None, + attachments=( + Attachments( + manifests=[ + ManifestProperties( + fileSystemLocationName=manifest_properties.get( + "fileSystemLocationName", None + ), + rootPath=manifest_properties["rootPath"], + rootPathFormat=PathFormat(manifest_properties["rootPathFormat"]), + outputRelativeDirectories=manifest_properties.get( + "outputRelativeDirectories", None + ), + inputManifestPath=manifest_properties.get("inputManifestPath", None), + ) + for manifest_properties in response["attachments"]["manifests"] + ], + fileSystem=JobAttachmentsFileSystem( + response["attachments"].get("fileSystem", JobAttachmentsFileSystem.COPIED.value) + ), + ) + if "attachments" in response and response["attachments"] + else None + ), ) diff --git a/src/deadline/job_attachments/download.py b/src/deadline/job_attachments/download.py index f05f6542..bfbbbd14 100644 --- a/src/deadline/job_attachments/download.py +++ b/src/deadline/job_attachments/download.py @@ -89,14 +89,16 @@ def get_manifest_from_s3( status_code_guidance = { **COMMON_ERROR_GUIDANCE_FOR_S3, 403: ( - "Forbidden or Access denied. Please check your AWS credentials, and ensure that " - "your AWS IAM Role or User has the 's3:GetObject' permission for this bucket. " - ) - if "kms:" not in str(exc) - else ( - "Forbidden or Access denied. Please check your AWS credentials and Job Attachments S3 bucket " - "encryption settings. If a customer-managed KMS key is set, confirm that your AWS IAM Role or " - "User has the 'kms:Decrypt' and 'kms:DescribeKey' permissions for the key used to encrypt the bucket." + ( + "Forbidden or Access denied. Please check your AWS credentials, and ensure that " + "your AWS IAM Role or User has the 's3:GetObject' permission for this bucket. " + ) + if "kms:" not in str(exc) + else ( + "Forbidden or Access denied. Please check your AWS credentials and Job Attachments S3 bucket " + "encryption settings. If a customer-managed KMS key is set, confirm that your AWS IAM Role or " + "User has the 'kms:Decrypt' and 'kms:DescribeKey' permissions for the key used to encrypt the bucket." + ) ), 404: "Not found. Please check your bucket name and object key, and ensure that they exist in the AWS account.", } @@ -453,14 +455,16 @@ def process_client_error(exc: ClientError, status_code: int): status_code_guidance = { **COMMON_ERROR_GUIDANCE_FOR_S3, 403: ( - "Forbidden or Access denied. Please check your AWS credentials, and ensure that " - "your AWS IAM Role or User has the 's3:GetObject' permission for this bucket. " - ) - if "kms:" not in str(exc) - else ( - "Forbidden or Access denied. Please check your AWS credentials and Job Attachments S3 bucket " - "encryption settings. If a customer-managed KMS key is set, confirm that your AWS IAM Role or " - "User has the 'kms:Decrypt' and 'kms:DescribeKey' permissions for the key used to encrypt the bucket." + ( + "Forbidden or Access denied. Please check your AWS credentials, and ensure that " + "your AWS IAM Role or User has the 's3:GetObject' permission for this bucket. " + ) + if "kms:" not in str(exc) + else ( + "Forbidden or Access denied. Please check your AWS credentials and Job Attachments S3 bucket " + "encryption settings. If a customer-managed KMS key is set, confirm that your AWS IAM Role or " + "User has the 'kms:Decrypt' and 'kms:DescribeKey' permissions for the key used to encrypt the bucket." + ) ), 404: ( "Not found. Please check your bucket name and object key, and ensure that they exist in the AWS account." diff --git a/src/deadline/job_attachments/upload.py b/src/deadline/job_attachments/upload.py index c3559c2f..a69b8374 100644 --- a/src/deadline/job_attachments/upload.py +++ b/src/deadline/job_attachments/upload.py @@ -384,14 +384,16 @@ def handler(bytes_uploaded): status_code_guidance = { **COMMON_ERROR_GUIDANCE_FOR_S3, 403: ( - "Forbidden or Access denied. Please check your AWS credentials, and ensure that " - "your AWS IAM Role or User has the 's3:PutObject' permission for this bucket. " - ) - if "kms:" not in str(exc) - else ( - "Forbidden or Access denied. Please check your AWS credentials and Job Attachments S3 bucket " - "encryption settings. If a customer-managed KMS key is set, confirm that your AWS IAM Role or " - "User has the 'kms:GenerateDataKey' and 'kms:DescribeKey' permissions for the key used to encrypt the bucket." + ( + "Forbidden or Access denied. Please check your AWS credentials, and ensure that " + "your AWS IAM Role or User has the 's3:PutObject' permission for this bucket. " + ) + if "kms:" not in str(exc) + else ( + "Forbidden or Access denied. Please check your AWS credentials and Job Attachments S3 bucket " + "encryption settings. If a customer-managed KMS key is set, confirm that your AWS IAM Role or " + "User has the 'kms:GenerateDataKey' and 'kms:DescribeKey' permissions for the key used to encrypt the bucket." + ) ), 404: "Not found. Please check your bucket name and object key, and ensure that they exist in the AWS account.", } @@ -465,14 +467,16 @@ def upload_bytes_to_s3( status_code_guidance = { **COMMON_ERROR_GUIDANCE_FOR_S3, 403: ( - "Forbidden or Access denied. Please check your AWS credentials, and ensure that " - "your AWS IAM Role or User has the 's3:PutObject' permission for this bucket. " - ) - if "kms:" not in str(exc) - else ( - "Forbidden or Access denied. Please check your AWS credentials and Job Attachments S3 bucket " - "encryption settings. If a customer-managed KMS key is set, confirm that your AWS IAM Role or " - "User has the 'kms:GenerateDataKey' and 'kms:DescribeKey' permissions for the key used to encrypt the bucket." + ( + "Forbidden or Access denied. Please check your AWS credentials, and ensure that " + "your AWS IAM Role or User has the 's3:PutObject' permission for this bucket. " + ) + if "kms:" not in str(exc) + else ( + "Forbidden or Access denied. Please check your AWS credentials and Job Attachments S3 bucket " + "encryption settings. If a customer-managed KMS key is set, confirm that your AWS IAM Role or " + "User has the 'kms:GenerateDataKey' and 'kms:DescribeKey' permissions for the key used to encrypt the bucket." + ) ), 404: "Not found. Please check your bucket name, and ensure that it exists in the AWS account.", } diff --git a/test/unit/deadline_client/api/test_api_session.py b/test/unit/deadline_client/api/test_api_session.py index a6cc8f08..955fc004 100644 --- a/test/unit/deadline_client/api/test_api_session.py +++ b/test/unit/deadline_client/api/test_api_session.py @@ -103,8 +103,8 @@ def test_get_queue_user_boto3_session_cache(fresh_deadline_config): session_mock.region_name = "us-west-2" deadline_mock = MagicMock() mock_botocore_session = MagicMock() - mock_botocore_session.get_config_variable = ( - lambda name: "test_profile" if name == "profile" else None + mock_botocore_session.get_config_variable = lambda name: ( + "test_profile" if name == "profile" else None ) with patch.object(api._session, "get_boto3_session", return_value=session_mock), patch( @@ -138,8 +138,8 @@ def test_get_queue_user_boto3_session_no_profile(fresh_deadline_config): session_mock.region_name = "us-west-2" deadline_mock = MagicMock() mock_botocore_session = MagicMock() - mock_botocore_session.get_config_variable = ( - lambda name: "default" if name == "profile" else None + mock_botocore_session.get_config_variable = lambda name: ( + "default" if name == "profile" else None ) with patch.object(api._session, "get_boto3_session", return_value=session_mock), patch( diff --git a/test/unit/deadline_client/ui/__init__.py b/test/unit/deadline_client/ui/__init__.py index 2dda7942..6f05e789 100644 --- a/test/unit/deadline_client/ui/__init__.py +++ b/test/unit/deadline_client/ui/__init__.py @@ -5,10 +5,10 @@ # we must mock UI code mock_modules = [ - "PySide2", - "PySide2.QtCore", - "PySide2.QtGui", - "PySide2.QtWidgets", + "qtpy", + "qtpy.QtCore", + "qtpy.QtGui", + "qtpy.QtWidgets", ] for module in mock_modules: