Skip to content

Commit

Permalink
feat: use qtpy and add support for pyside6 (#202)
Browse files Browse the repository at this point in the history
Signed-off-by: Justin Sawatzky <[email protected]>
Co-authored-by: Morgan Epp <[email protected]>
  • Loading branch information
justinsaws and epmog authored Mar 22, 2024
1 parent cf9c2d2 commit deb2cca
Show file tree
Hide file tree
Showing 28 changed files with 246 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
25 changes: 19 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 @@ -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]
Expand Down Expand Up @@ -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]
Expand All @@ -84,21 +89,29 @@ mypy_path = "src"

[[tool.mypy.overrides]]
module = [
"PySide2.*",
"qtpy.*",
"boto3.*",
"botocore.*",
"moto.*",
"xxhash",
"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"
]
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.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.*
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
71 changes: 64 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,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)

Expand All @@ -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
Expand All @@ -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()}"
)

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
Loading

0 comments on commit deb2cca

Please sign in to comment.