|
3 | 3 | This documentation provides guidance on developer workflows for working with the code in this repository. |
4 | 4 |
|
5 | 5 | Table of Contents: |
6 | | -* [Development Environment Setup](#development-environment-setup) |
7 | | -* [The Development Loop](#the-development-loop) |
8 | | -* [Documentation](#documentation) |
9 | | - * [Code Organization](#code-organization) |
10 | | -* [Testing](#testing) |
11 | | - * [Writing tests](#writing-tests) |
12 | | - * [Unit tests](#unit-tests) |
13 | | - * [Integration tests](#integration-tests) |
14 | | - * [Squish GUI Submitter tests](#squish-tests) |
15 | | -* [Changelog Guidelines](#changelog-guidelines) |
16 | | -* [Things to Know](#things-to-know) |
17 | | - * [Public contracts](#public-contracts) |
18 | | - * [Library Dependencies](#dependencies) |
19 | | - * [Qt and Calling AWS APIs](#qt-and-calling-aws-including-aws-deadline-cloud-apis) |
| 6 | +- [Development documentation](#development-documentation) |
| 7 | + - [Development Environment Setup](#development-environment-setup) |
| 8 | + - [The Development Loop](#the-development-loop) |
| 9 | + - [Documentation](#documentation) |
| 10 | + - [Code Organization](#code-organization) |
| 11 | + - [Testing](#testing) |
| 12 | + - [Writing Tests](#writing-tests) |
| 13 | + - [Unit Tests](#unit-tests) |
| 14 | + - [Running Unit Tests](#running-unit-tests) |
| 15 | + - [Running Docker-based Unit Tests](#running-docker-based-unit-tests) |
| 16 | + - [Integration Tests](#integration-tests) |
| 17 | + - [Running Integration Tests](#running-integration-tests) |
| 18 | + - [Squish GUI Submitter Tests](#squish-gui-submitter-tests) |
| 19 | + - [Running Squish GUI Submitter Tests](#running-squish-gui-submitter-tests) |
| 20 | + - [Changelog Guidelines](#changelog-guidelines) |
| 21 | + - [Things to Know](#things-to-know) |
| 22 | + - [Public Contracts](#public-contracts) |
| 23 | + - [Private Modules](#private-modules) |
| 24 | + - [Public Modules](#public-modules) |
| 25 | + - [On `import os as _os`](#on-import-os-as-_os) |
| 26 | + - [Library Dependencies](#library-dependencies) |
| 27 | + - [Why is a new dependency needed?](#why-is-a-new-dependency-needed) |
| 28 | + - [Quality of the dependency](#quality-of-the-dependency) |
| 29 | + - [Version Pinning](#version-pinning) |
| 30 | + - [Licensing](#licensing) |
| 31 | + - [Qt and Calling AWS (including AWS Deadline Cloud) APIs](#qt-and-calling-aws-including-aws-deadline-cloud-apis) |
| 32 | + - [Pattern 1: Simple Async Operations (Recommended)](#pattern-1-simple-async-operations-recommended) |
| 33 | + - [Pattern 2: Long-Running Operations with Progress](#pattern-2-long-running-operations-with-progress) |
| 34 | +- [Profiling in Deadline Cloud](#profiling-in-deadline-cloud) |
20 | 35 |
|
21 | 36 | ## Development Environment Setup |
22 | 37 |
|
@@ -378,66 +393,89 @@ for a signal from the application. |
378 | 393 | If interacting with the GUI can start multiple background threads, you should also track which |
379 | 394 | is the latest, so the code only applies the result of the newest operation. |
380 | 395 |
|
381 | | -See `deadline_config_dialog.py` for some examples that do all of the above. Here's some |
382 | | -code that was edited to show how it fits together: |
| 396 | +See `deadline_config_dialog.py` for some examples that do all of the above. |
| 397 | + |
| 398 | +### Pattern 1: Simple Async Operations (Recommended) |
| 399 | + |
| 400 | +For simple fetch-and-display operations, use `AsyncTaskRunner`: |
383 | 401 |
|
384 | 402 | ```python |
| 403 | +from deadline.client.ui.controllers import AsyncTaskRunner |
| 404 | + |
385 | 405 | class MyCustomWidget(QWidget): |
386 | | - # Signals for the widget to receive from the thread |
387 | | - background_exception = Signal(str, BaseException) |
388 | | - update = Signal(int, BackgroundResult) |
389 | | - |
390 | | - def __init__(self, ...): |
391 | | - # Save information about the thread |
392 | | - self.__refresh_thread = None |
393 | | - self.__refresh_id = 0 |
394 | | - |
395 | | - # Use the CancelationFlag object to decouple the cancelation value |
396 | | - # from the window lifetime. |
397 | | - self.canceled = CancelationFlag() |
398 | | - self.destroyed.connect(self.canceled.set_canceled) |
399 | | - |
400 | | - # Connect the Signals to handler functions that run on the main thread |
401 | | - self.update.connect(self.handle_update) |
402 | | - self.background_exception.connect(self.handle_background_exception) |
403 | | - |
404 | | - def handle_background_exception(self, e: BaseException): |
405 | | - # Handle the error |
406 | | - QMessageBox.warning(...) |
407 | | - |
408 | | - def handle_update(self, refresh_id: int, result: BackgroundResult): |
409 | | - # Apply the refresh if it's still for the latest call |
410 | | - if refresh_id == self.__refresh_id: |
411 | | - # Do something with result |
412 | | - self.result_widget.set_message(result) |
| 406 | + def __init__(self, ...): |
| 407 | + self._runner = AsyncTaskRunner(self) |
| 408 | + self._runner.task_error.connect(self._on_error, Qt.QueuedConnection) |
413 | 409 |
|
414 | 410 | def start_the_refresh(self): |
415 | | - # This function starts the thread to run in the background |
416 | | - |
417 | | - # Update the GUI state to reflect the update |
418 | 411 | self.result_widget.set_refreshing_status(True) |
419 | | - |
420 | | - self.__refresh_id += 1 |
421 | | - self.__refresh_thread = threading.Thread( |
422 | | - target=self._refresh_thread_function, |
423 | | - name=f"AWS Deadline Cloud Refresh Thread", |
424 | | - args=(self.__refresh_id,), |
| 412 | + self._runner.run( |
| 413 | + operation_key="my_refresh", |
| 414 | + fn=self._fetch_data, |
| 415 | + on_success=self._handle_result, |
| 416 | + on_error=self._handle_error, |
425 | 417 | ) |
426 | | - self.__refresh_thread.start() |
427 | | - |
428 | | - def _refresh_thread_function(self, refresh_id: int): |
429 | | - # This function is for the background thread |
430 | | - try: |
431 | | - # Call the slow operations |
432 | | - result = boto3_client.potentially_expensive_api(...) |
433 | | - # Only emit the result if it isn't canceled |
434 | | - if not self.canceled: |
435 | | - self.update.emit(refresh_id, result) |
436 | | - except BaseException as e: |
437 | | - # Use multiple signals for different meanings, such as handling errors. |
438 | | - if not self.canceled: |
439 | | - self.background_exception.emit(f"Background thread error", e) |
440 | 418 |
|
| 419 | + def _fetch_data(self): |
| 420 | + # This runs in background thread |
| 421 | + return boto3_client.potentially_expensive_api(...) |
| 422 | + |
| 423 | + def _handle_result(self, result): |
| 424 | + self.result_widget.set_refreshing_status(False) |
| 425 | + self.result_widget.set_message(result) |
| 426 | + |
| 427 | + def _handle_error(self, error): |
| 428 | + self.result_widget.set_refreshing_status(False) |
| 429 | + QMessageBox.warning(self, "Error", str(error)) |
| 430 | +``` |
| 431 | + |
| 432 | +### Pattern 2: Long-Running Operations with Progress |
| 433 | + |
| 434 | +For complex operations with progress callbacks, use a `QThread` subclass: |
| 435 | + |
| 436 | +```python |
| 437 | +from qtpy.QtCore import QThread, Signal, Qt |
| 438 | + |
| 439 | +class MyWorker(QThread): |
| 440 | + progress = Signal(int, str) # percent, message |
| 441 | + succeeded = Signal(object) |
| 442 | + failed = Signal(BaseException) |
| 443 | + |
| 444 | + def __init__(self, parent=None): |
| 445 | + super().__init__(parent) |
| 446 | + self._canceled = False |
| 447 | + |
| 448 | + def cancel(self): |
| 449 | + self._canceled = True |
| 450 | + |
| 451 | + def run(self): |
| 452 | + try: |
| 453 | + for i, item in enumerate(items): |
| 454 | + if self._canceled: |
| 455 | + return |
| 456 | + self.progress.emit(i * 100 // len(items), f"Processing {item}") |
| 457 | + process(item) |
| 458 | + self.succeeded.emit(result) |
| 459 | + except Exception as e: |
| 460 | + if not self._canceled: |
| 461 | + self.failed.emit(e) |
| 462 | + |
| 463 | + |
| 464 | +class MyCustomWidget(QWidget): |
| 465 | + def __init__(self, ...): |
| 466 | + self._worker = MyWorker(self) |
| 467 | + self._worker.progress.connect(self._on_progress, Qt.QueuedConnection) |
| 468 | + self._worker.succeeded.connect(self._on_success, Qt.QueuedConnection) |
| 469 | + self._worker.failed.connect(self._on_error, Qt.QueuedConnection) |
| 470 | + |
| 471 | + def start_the_operation(self): |
| 472 | + self._worker.start() |
| 473 | + |
| 474 | + def closeEvent(self, event): |
| 475 | + if self._worker.isRunning(): |
| 476 | + self._worker.cancel() |
| 477 | + self._worker.wait() |
| 478 | + super().closeEvent(event) |
441 | 479 | ``` |
442 | 480 |
|
443 | 481 | # Profiling in Deadline Cloud |
|
0 commit comments