Skip to content

Commit

Permalink
fix: removed exec recursion explosion and initialized Signals properly
Browse files Browse the repository at this point in the history
Signed-off-by: Morgan Epp <[email protected]>
  • Loading branch information
epmog committed Mar 20, 2024
1 parent e936938 commit 3ff462c
Show file tree
Hide file tree
Showing 28 changed files with 204 additions and 159 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
4 changes: 2 additions & 2 deletions hatch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
23 changes: 17 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -26,6 +26,12 @@ dependencies = [
"pywin32 == 306; sys_platform == 'win32'",
]

[project.optional-dependencies]
gui = [
"QtPy == 2.4.*",
"PySide6 == 6.6.*",
]

[project.scripts]
deadline-dev-gui = "deadline.client.cli.deadline_dev_gui_main:main"
deadline = "deadline.client.cli:main"
Expand Down Expand Up @@ -63,8 +69,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]
Expand All @@ -84,21 +88,28 @@ mypy_path = "src"

[[tool.mypy.overrides]]
module = [
"PySide2.*",
"qtpy.*",
"boto3.*",
"botocore.*",
"moto.*",
"xxhash",
"jsonschema",
]

[[tool.mypy.overrides]]
module = "deadline.client.ui.*"
# qtpy is not currently playing nicely with mypy
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"
]
Expand Down
27 changes: 14 additions & 13 deletions requirements-testing.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
coverage[toml] ~= 7.2; python_version == '3.7'
coverage[toml] ~= 7.3; 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.*
4 changes: 2 additions & 2 deletions src/deadline/client/cli/_groups/bundle_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 24 additions & 7 deletions src/deadline/client/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -57,22 +57,39 @@ def gui_context_for_cli():
show_cli_job_submitter()
app.exec_()
app.exec()
"""
import importlib
import subprocess
import sys
from pathlib import Path

import click

has_qtpy = importlib.util.find_spec("qtpy")
has_pyside = importlib.util.find_spec("PySide6") or importlib.util.find_spec("PySide2")

if not (has_qtpy or 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)

# 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", "qtpy", "pyside6"])

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)

Expand All @@ -84,7 +101,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
Expand All @@ -93,7 +110,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()}"
)

Expand Down
6 changes: 3 additions & 3 deletions src/deadline/client/ui/cli_job_submitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src/deadline/client/ui/deadline_authentication_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions src/deadline/client/ui/dev_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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_()
6 changes: 3 additions & 3 deletions src/deadline/client/ui/dialogs/deadline_config_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
11 changes: 4 additions & 7 deletions src/deadline/client/ui/dialogs/deadline_login_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
23 changes: 10 additions & 13 deletions src/deadline/client/ui/dialogs/submit_job_progress_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
"""
Expand Down
Loading

0 comments on commit 3ff462c

Please sign in to comment.