Name | PySink JSON |
Version |
0.0.10
JSON |
| download |
home_page | |
Summary | PySide6 Helpers for Powerful Async Desktop Apps |
upload_time | 2023-04-07 18:57:11 |
maintainer | |
docs_url | None |
author | |
requires_python | >=3.9 |
license | MIT License Copyright (c) 2023 Zack Johnson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
keywords |
python
app
gui
async
application
desktop
ui
|
VCS |
|
bugtrack_url |
|
requirements |
No requirements were recorded.
|
Travis-CI |
No Travis.
|
coveralls test coverage |
No coveralls.
|
# PySink
Created by Zack Johnson
## Under construction, not yet ready for use!!
Full documentation can be found on ReadTheDocs
PySink is an extension of the PySide6 Qt Framework that simplifies the implementation
of Asynchronous tasks in Desktop Applications. It contains several
helper Widgets and Classes that enable you to build powerful and professional
desktop applications without worrying about managing threads or freezing
UI with long-running tasks. PySink's implementation suggests an MVC
architecture for your application, but should perform well in other architectures
such as MVVM.
## Basic Overview
PySink is based on the concept of Workers and Managers. Workers are custom objects that
perform long-running tasks. They inherit from the provided *AsyncWorker* class and
override the *AsyncWorker.run()* method to perform the tasks, emitting progress values and
optional status messages along the way. These workers are managed by a generalized object called the
*AsyncManager*.
The Manager is an object that manages all the workers/threading and handles the termination/cancellation
of said threads and workers when necessary. The Manager can also pass along the signals emitted by
the worker (you can also connect to the worker's signals directly, see example 5 below).
The two signals that
## Getting Started
Let's look at a couple examples to help you get started. Full examples and all source code can
be found at https://github.com/zackjohnson298/PySink
### Example 1: Defining and Using a Custom Async Worker
In this first example, we will create a custom AsyncWorker that performs *time.sleep()*
for a specified duration and number of cycles. To create a new worker, define a class
that inherits from *PySink.AsyncWorker*. Any values needed by the worker should be passed
in via it's *\_\_init\_\_* method:
```python
from PySink import AsyncWorker
class DemoAsyncWorker1(AsyncWorker):
def __init__(self, delay_seconds: int, cycles=4):
super(DemoAsyncWorker1, self).__init__()
# Store the values passed in during initialization
self.delay_seconds = delay_seconds
self.cycles = cycles
```
To implement the long-running task, simply override AsyncWorker's *run* method.
This method takes no parameters and returns nothing, it' only job is to perform the long-running task.
Progress is emitted by calling the *self.update_progress(progress, message)*method, and when the task is
done you can emit any results via the *self.complete(\*\*kwargs)* method:
```python
from PySink import AsyncWorker
import time
class DemoAsyncWorker1(AsyncWorker):
def __init__(self, delay_seconds: int, cycles=4):
super(DemoAsyncWorker1, self).__init__()
# Store the values passed in during initialization
self.delay_seconds = delay_seconds
self.cycles = cycles
def run(self):
# Update discrete progress by providing a progress value from 0-100 with an optional message
progress = 5
self.update_progress(progress, 'Starting Task')
for ii in range(self.cycles):
time.sleep(self.delay_seconds)
progress += 90 / self.cycles
self.update_progress(progress, f'Progress message #{ii + 1}')
# Call the self.complete method to end your task, passing any results as keyword arguments
demo_result = 12
self.complete(demo_result=demo_result)
```
Starting your custom worker is as simple as creating an AsyncManager, tying
its signals to your callback methods, and passing your custom Worker to its *start_worker(worker)* method.
*(We also need a QApplication running for the event loop
to start, which you will already have in your PySide6 Application).* Let's see
what this looks like in code:
```python
from PySide6.QtWidgets import QApplication
from PySink import AsyncManager
# Function to be called whenever progress is updated
def progress_callback(progress_value: int, message: str):
print(f'Progress Received, value: {progress_value}, message: {message}')
# Function to be called when the worker is finished
def completion_callback(results: dict):
print(f'\nWorker Complete!')
print(f'\tErrors: {results.get("errors")}')
print(f'\tWarnings: {results.get("warnings")}')
print(f'\tResult: {results.get("demo_result")}')
def run_main():
app = QApplication()
manager = AsyncManager()
# Connect the Manager's signals to your callbacks
manager.worker_progress_signal.connect(progress_callback)
manager.worker_finished_signal.connect(completion_callback)
# Create your Worker, and pass in the necessary values
demo_worker = DemoAsyncWorker(1, cycles=3)
# Start the Worker
manager.start_worker(demo_worker)
app.exec()
run_main()
```
Let's first take a look at the progress callback. Progress is emitted by the manager's
*worker_progress_signal* and contains the progress value as well as the optional message.
It should be tied to the callback function that handles progress events. In this example,
the progress callback simply prints out the progress value and the message.
In the next example, we will look at how to tie this to the *ProgressBarWidget* provided
in the *PySink.Widgets* module.
The completion callback is tied to the manager's *worker_finished_signal*. This signal
emits the results of the worker's task as a dictionary. It is keyed by the keyword arguments
defined when the *self.complete(\*\*kwargs)* method is called within the worker. This
dictionary also contains the worker's *warnings* and *errors*.
Running the code above results in the following being printed to the console:
```commandline
Progress Received, value: 0, message: Starting Task
Progress Received, value: 33, message: Progress message #1
Progress Received, value: 66, message: Progress message #2
Progress Received, value: 100, message: Progress message #3
Worker Complete!
Errors: []
Warnings: []
Result: 12
```
Congratulations! You've just implemented an AsyncWorker that runs a task in a background thread.
Running the task like this has freed up the UI thread, allowing your users to still interact
with your application without freezing the UI. Full example code can be found at
https://github.com/zackjohnson298/PySink
in the examples/example1 folder.
In the next example, we will see how to use PySink to create a basic asynchronous App.
We'll also see how to use the provided ProgressBarWidget to display the progress of your
asynchronous task to the user.
### Example 2: Create a Basic Asynchronous App
Now let's create a desktop app that allows the user to start a long-running task and monitor
its progress within the UI. This example will follow a basic MVC architecture.
PySink includes a helper Widget called the ProgressBarWidget that packages up some helpful
progress bar functionality into a single class. This widget allows you to easily display
discrete progress values from 0-100, show an indeterminate progress state by passing in a negative
value, and (on Windows) set text that gets overlaid on the progress bar (refer to the docs for
more information).
Let's get started building the App. Since this is a demo of PySink and not a PyQt tutorial, I
will not dive too deep into PySide6 Windows, Widgets, or layout management. Start by
setting up a basic View that inherits from QMainWindow, and populate the view with some
widgets:
```python
from PySide6.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, QWidget, QGridLayout, QLabel
from PySide6.QtCore import Signal
from PySink.Widgets import ProgressBarWidget
class MainView(QMainWindow):
button_pushed_signal = Signal()
def __init__(self):
super(MainView, self).__init__()
# Widgets
self.button = QPushButton('Start')
self.progress_bar = ProgressBarWidget()
self.result_label = QLabel()
self.warnings_label = QLabel()
self.errors_label = QLabel()
# Connect Signals
self.button.clicked.connect(self.button_pushed_signal.emit)
# Layout
grid_layout = QGridLayout()
grid_layout.addWidget(QLabel('Result:'), 0, 0)
grid_layout.addWidget(QLabel('Warnings:'), 1, 0)
grid_layout.addWidget(QLabel('Error:'), 2, 0)
grid_layout.addWidget(self.result_label, 0, 1)
grid_layout.addWidget(self.warnings_label, 1, 1)
grid_layout.addWidget(self.errors_label, 2, 1)
central_layout = QVBoxLayout()
central_layout.addLayout(grid_layout)
central_layout.addWidget(self.button)
central_layout.addWidget(self.progress_bar)
central_widget = QWidget()
central_widget.setLayout(central_layout)
self.setCentralWidget(central_widget)
def set_result(self, result):
self.result_label.setText(str(result))
def set_warnings(self, warnings):
self.warnings_label.setText(str(warnings))
def set_errors(self, errors):
self.errors_label.setText(str(errors))
def set_progress(self, progress_value, message=None):
self.progress_bar.set_value(progress_value)
if message:
self.progress_bar.set_text(message)
def clear(self):
self.warnings_label.setText('')
self.errors_label.setText('')
self.result_label.setText('')
def show_progress(self):
self.button.setVisible(False)
self.progress_bar.setVisible(True)
def show_button(self):
self.progress_bar.setVisible(False)
self.button.setVisible(True)
if __name__ == '__main__':
from PySide6.QtWidgets import QApplication
app = QApplication()
window = MainView()
window.show()
app.exec()
```
Including the snippet within the *\_\_name__ == \_\_main__* block allows you to run this
as a script on its own and see the window you've just created. Doing this allows you to make
sure the UI looks correct before you connect any actions to the View.
Run the script to see the app window:
![alt text](docs/img/example2_main_view.png "Title")
This very simple app has a start button, progress bar, and some labels to display the data.
The View also has a signal that gets emitted on the button press, as well as a
few helper methods to reset the UI and set the progress and output values. Exposing the
signals and providing methods like this are not required to make an app work. However,
doing so decouples the UI from the application logic allowing for much more flexibility in
the future.
Now let's make the app actually do something. In the MVC architecture, it is the Controller
that 'controls' the state of the UI, reacts to the signals it emits, and provides data to the
View to be displayed. Again, let's look at the code for the Controller before diving in to how it
works:
```python
from PySink import AsyncManager
from DemoAsyncWorker import DemoAsyncWorker
from MainView import MainView
class MainController:
def __init__(self, view: MainView):
# Initialize/Store Attributes
self.view = view
self.async_manager = AsyncManager()
# Connect UI Signals
self.view.start_signal.connect(self.start_task)
# Connect Async Signals
self.async_manager.worker_progress_signal.connect(self.view.set_progress)
self.async_manager.worker_finished_signal.connect(self.task_complete_callback)
# Initialize UI State
self.view.hide_progress()
def start_task(self):
# Update UI
self.view.clear()
self.view.show_progress()
# Initialize/Start Worker
worker = DemoAsyncWorker(2, cycles=5)
self.async_manager.start_worker(worker)
def task_complete_callback(self, results):
# Update UI
self.view.clear()
self.view.hide_progress()
# Handle results
self.view.set_result(results.get('demo_result'))
self.view.set_warnings(results.get('warnings'))
self.view.set_errors(results.get('errors'))
```
In this example, the Controller gets initialized with the view it is controlling. Injecting
the dependency like this allows you to create layers in your App Architecture, and moves the
responsibility of 'showing' the window up a level. It also makes the Controller testable.
By mocking the attributes and methods of our view in a different class (that
doesn't need UI at all) tests can be automated much faster. Again, this is not required to
get a PySink app to work, but this practice will be beneficial in the long run.
Let's take a closer look at the Controller's *\_\_init\_\_()* method:
```python
class MainController:
def __init__(self, view: MainView):
# Initialize/Store Attributes
self.view = view
self.async_manager = AsyncManager()
# Connect UI Signals
self.view.start_signal.connect(self.start_task)
# Connect Async Signals
self.async_manager.worker_progress_signal.connect(self.view.set_progress)
self.async_manager.worker_finished_signal.connect(self.task_complete_callback)
# Initialize UI State
self.view.hide_progress()
```
Within the *\_\_init__()* method, any attributes needed by the controller are initialized and
stored. In this case, those are the View and an AsyncManager (storing the manager as an
attribute ensures that the worker and thread stay alive after calling the Manager's
*start_worker* method).
The View's signals are then connected to the internal methods that respond to them. In this
case, it is a single signal called *button_pressed_signal* which gets connected to the
Controller's *start_task* method. We also connect the Manager's signals to their respective
callback functions: *self.view.update_progress* for the progress signal and *self.task_complete_callback* for the
completion signal.
Lastly, the UI is placed into the intended initial state that will be displayed upon
application launch. In this example, that just means displaying the start button.
Let's take a closer look at the Controller's *start_task* method. This is the method that gets
called when the user presses the start button:
```python
def start_task(self):
# Update UI
self.view.clear()
self.view.show_progress()
# Initialize/Start Worker
worker = DemoAsyncWorker(2, cycles=5)
self.async_manager.start_worker(worker)
```
Here, the UI is updated so that it is in the correct state for the long-running task to take
place. This usually means clearing out old data, showing the progress widgets, and
disabling/hiding anything that shouldn't be shown while the task is running.
After the UI is dealt with, the task is started by simply creating a new instance of the Worker
(passing in any values it needs), and passing it to the *start_worker* method of the
AsyncManager. And that's it! The worker is now running in the background and the progress
is getting updated within the UI.
Now let's look at what happens when the worker is done running its task. Since the
AsyncManager's *worker_finished_signal* was connected to the Controller's
*task_complete_callback*, that callback will be executed upon the worker's completion.
Here's what the callback looks like:
```python
def task_complete_callback(self, results):
# Update UI
self.view.clear()
self.view.hide_progress()
# Handle results
self.view.set_result(results.get('demo_result'))
self.view.set_warnings(results.get('warnings'))
self.view.set_errors(results.get('errors'))
```
As stated in the previous example, the results of the worker's task are provided as a
dictionary that gets passed in to the completion callback. This dictionary contains the
values defined as keyword arguments within the worker's *run* method, as well as any
warnings/errors encountered during the task. In this callback, the UI is again updated to
reflect the task's completion and the results are displayed to the user by passing them into
the View.
And that's it! You now have a fully functional asynchronous application. Since Dependency
Injection was implemented in the Controller, you will need to instantiate the Controller
and pass it an existing View. Create a new file, import the Controller and View, and start
a QApplication:
```python
from PySide6.QtWidgets import QApplication
from MainController import MainController
from MainView import MainView
app = QApplication()
view = MainView()
controller = MainController(view)
view.show()
app.exec()
```
Run the script and the application will start. Pushing the start button within the app will
trigger the long-running task, and at its completion data from the worker will be displayed:
![alt text](docs/img/example2_while_running.png "Title")
![alt text](docs/img/example2_complete.png "Title")
Congratulations! You've just created an asynchronous app with PySink!
Full example code can be found at https://github.com/zackjohnson298/PySink
in the examples/example2 folder.
Raw data
{
"_id": null,
"home_page": "",
"name": "PySink",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.9",
"maintainer_email": "",
"keywords": "python,app,gui,async,application,desktop,ui",
"author": "",
"author_email": "Zack Johnson <zackjohnson298@gmail.com>",
"download_url": "https://files.pythonhosted.org/packages/e9/7d/275b458733734e30f98837022e18ceee6615eafbe73542e6d90f8801c0c3/PySink-0.0.10.tar.gz",
"platform": null,
"description": "# PySink\n\nCreated by Zack Johnson\n\n## Under construction, not yet ready for use!!\n\nFull documentation can be found on ReadTheDocs\n\nPySink is an extension of the PySide6 Qt Framework that simplifies the implementation\nof Asynchronous tasks in Desktop Applications. It contains several\nhelper Widgets and Classes that enable you to build powerful and professional\ndesktop applications without worrying about managing threads or freezing \nUI with long-running tasks. PySink's implementation suggests an MVC \narchitecture for your application, but should perform well in other architectures\nsuch as MVVM.\n\n## Basic Overview\nPySink is based on the concept of Workers and Managers. Workers are custom objects that \nperform long-running tasks. They inherit from the provided *AsyncWorker* class and \noverride the *AsyncWorker.run()* method to perform the tasks, emitting progress values and \noptional status messages along the way. These workers are managed by a generalized object called the\n*AsyncManager*. \n\nThe Manager is an object that manages all the workers/threading and handles the termination/cancellation \nof said threads and workers when necessary. The Manager can also pass along the signals emitted by \nthe worker (you can also connect to the worker's signals directly, see example 5 below).\n\nThe two signals that\n\n## Getting Started\n\n\nLet's look at a couple examples to help you get started. Full examples and all source code can \nbe found at https://github.com/zackjohnson298/PySink\n\n### Example 1: Defining and Using a Custom Async Worker\nIn this first example, we will create a custom AsyncWorker that performs *time.sleep()* \nfor a specified duration and number of cycles. To create a new worker, define a class\nthat inherits from *PySink.AsyncWorker*. Any values needed by the worker should be passed\nin via it's *\\_\\_init\\_\\_* method: \n\n```python\nfrom PySink import AsyncWorker\n\n\nclass DemoAsyncWorker1(AsyncWorker):\n def __init__(self, delay_seconds: int, cycles=4):\n super(DemoAsyncWorker1, self).__init__()\n # Store the values passed in during initialization\n self.delay_seconds = delay_seconds\n self.cycles = cycles\n```\nTo implement the long-running task, simply override AsyncWorker's *run* method.\nThis method takes no parameters and returns nothing, it' only job is to perform the long-running task. \nProgress is emitted by calling the *self.update_progress(progress, message)*method, and when the task is\ndone you can emit any results via the *self.complete(\\*\\*kwargs)* method:\n\n```python\nfrom PySink import AsyncWorker\nimport time\n\n\nclass DemoAsyncWorker1(AsyncWorker):\n def __init__(self, delay_seconds: int, cycles=4):\n super(DemoAsyncWorker1, self).__init__()\n # Store the values passed in during initialization\n self.delay_seconds = delay_seconds\n self.cycles = cycles\n\n def run(self):\n # Update discrete progress by providing a progress value from 0-100 with an optional message\n progress = 5\n self.update_progress(progress, 'Starting Task')\n for ii in range(self.cycles):\n time.sleep(self.delay_seconds)\n progress += 90 / self.cycles\n self.update_progress(progress, f'Progress message #{ii + 1}')\n # Call the self.complete method to end your task, passing any results as keyword arguments\n demo_result = 12\n self.complete(demo_result=demo_result)\n```\nStarting your custom worker is as simple as creating an AsyncManager, tying \nits signals to your callback methods, and passing your custom Worker to its *start_worker(worker)* method. \n*(We also need a QApplication running for the event loop \nto start, which you will already have in your PySide6 Application).* Let's see \nwhat this looks like in code:\n\n```python\nfrom PySide6.QtWidgets import QApplication\nfrom PySink import AsyncManager\n\n# Function to be called whenever progress is updated\ndef progress_callback(progress_value: int, message: str):\n print(f'Progress Received, value: {progress_value}, message: {message}')\n\n# Function to be called when the worker is finished\ndef completion_callback(results: dict):\n print(f'\\nWorker Complete!')\n print(f'\\tErrors: {results.get(\"errors\")}')\n print(f'\\tWarnings: {results.get(\"warnings\")}')\n print(f'\\tResult: {results.get(\"demo_result\")}')\n\n\ndef run_main():\n app = QApplication()\n manager = AsyncManager()\n # Connect the Manager's signals to your callbacks\n manager.worker_progress_signal.connect(progress_callback)\n manager.worker_finished_signal.connect(completion_callback)\n # Create your Worker, and pass in the necessary values\n demo_worker = DemoAsyncWorker(1, cycles=3)\n # Start the Worker\n manager.start_worker(demo_worker)\n \n app.exec()\n\nrun_main()\n```\n\nLet's first take a look at the progress callback. Progress is emitted by the manager's \n*worker_progress_signal* and contains the progress value as well as the optional message.\nIt should be tied to the callback function that handles progress events. In this example,\nthe progress callback simply prints out the progress value and the message.\nIn the next example, we will look at how to tie this to the *ProgressBarWidget* provided \nin the *PySink.Widgets* module.\n\nThe completion callback is tied to the manager's *worker_finished_signal*. This signal\nemits the results of the worker's task as a dictionary. It is keyed by the keyword arguments\ndefined when the *self.complete(\\*\\*kwargs)* method is called within the worker. This\ndictionary also contains the worker's *warnings* and *errors*.\n\nRunning the code above results in the following being printed to the console:\n```commandline\nProgress Received, value: 0, message: Starting Task\nProgress Received, value: 33, message: Progress message #1\nProgress Received, value: 66, message: Progress message #2\nProgress Received, value: 100, message: Progress message #3\n\nWorker Complete!\n\tErrors: []\n\tWarnings: []\n\tResult: 12\n```\nCongratulations! You've just implemented an AsyncWorker that runs a task in a background thread.\nRunning the task like this has freed up the UI thread, allowing your users to still interact\nwith your application without freezing the UI. Full example code can be found at \nhttps://github.com/zackjohnson298/PySink \nin the examples/example1 folder.\n\nIn the next example, we will see how to use PySink to create a basic asynchronous App. \nWe'll also see how to use the provided ProgressBarWidget to display the progress of your \nasynchronous task to the user.\n\n### Example 2: Create a Basic Asynchronous App\nNow let's create a desktop app that allows the user to start a long-running task and monitor \nits progress within the UI. This example will follow a basic MVC architecture.\n\nPySink includes a helper Widget called the ProgressBarWidget that packages up some helpful\nprogress bar functionality into a single class. This widget allows you to easily display \ndiscrete progress values from 0-100, show an indeterminate progress state by passing in a negative\nvalue, and (on Windows) set text that gets overlaid on the progress bar (refer to the docs for\nmore information).\n\nLet's get started building the App. Since this is a demo of PySink and not a PyQt tutorial, I \nwill not dive too deep into PySide6 Windows, Widgets, or layout management. Start by \nsetting up a basic View that inherits from QMainWindow, and populate the view with some \nwidgets:\n\n```python\nfrom PySide6.QtWidgets import QMainWindow, QVBoxLayout, QPushButton, QWidget, QGridLayout, QLabel\nfrom PySide6.QtCore import Signal\nfrom PySink.Widgets import ProgressBarWidget\n\n\nclass MainView(QMainWindow):\n button_pushed_signal = Signal()\n\n def __init__(self):\n super(MainView, self).__init__()\n\n # Widgets\n self.button = QPushButton('Start')\n self.progress_bar = ProgressBarWidget()\n self.result_label = QLabel()\n self.warnings_label = QLabel()\n self.errors_label = QLabel()\n\n # Connect Signals\n self.button.clicked.connect(self.button_pushed_signal.emit)\n\n # Layout\n grid_layout = QGridLayout()\n grid_layout.addWidget(QLabel('Result:'), 0, 0)\n grid_layout.addWidget(QLabel('Warnings:'), 1, 0)\n grid_layout.addWidget(QLabel('Error:'), 2, 0)\n grid_layout.addWidget(self.result_label, 0, 1)\n grid_layout.addWidget(self.warnings_label, 1, 1)\n grid_layout.addWidget(self.errors_label, 2, 1)\n\n central_layout = QVBoxLayout()\n central_layout.addLayout(grid_layout)\n central_layout.addWidget(self.button)\n central_layout.addWidget(self.progress_bar)\n central_widget = QWidget()\n central_widget.setLayout(central_layout)\n self.setCentralWidget(central_widget)\n\n def set_result(self, result):\n self.result_label.setText(str(result))\n\n def set_warnings(self, warnings):\n self.warnings_label.setText(str(warnings))\n\n def set_errors(self, errors):\n self.errors_label.setText(str(errors))\n\n def set_progress(self, progress_value, message=None):\n self.progress_bar.set_value(progress_value)\n if message:\n self.progress_bar.set_text(message)\n\n def clear(self):\n self.warnings_label.setText('')\n self.errors_label.setText('')\n self.result_label.setText('')\n \n def show_progress(self):\n self.button.setVisible(False)\n self.progress_bar.setVisible(True)\n \n def show_button(self):\n self.progress_bar.setVisible(False)\n self.button.setVisible(True)\n\n\n\nif __name__ == '__main__':\n from PySide6.QtWidgets import QApplication\n\n app = QApplication()\n window = MainView()\n window.show()\n\n app.exec()\n```\n\nIncluding the snippet within the *\\_\\_name__ == \\_\\_main__* block allows you to run this\nas a script on its own and see the window you've just created. Doing this allows you to make \nsure the UI looks correct before you connect any actions to the View. \nRun the script to see the app window:\n\n![alt text](docs/img/example2_main_view.png \"Title\")\n\nThis very simple app has a start button, progress bar, and some labels to display the data.\nThe View also has a signal that gets emitted on the button press, as well as a \nfew helper methods to reset the UI and set the progress and output values. Exposing the \nsignals and providing methods like this are not required to make an app work. However, \ndoing so decouples the UI from the application logic allowing for much more flexibility in\nthe future.\n\nNow let's make the app actually do something. In the MVC architecture, it is the Controller \nthat 'controls' the state of the UI, reacts to the signals it emits, and provides data to the\nView to be displayed. Again, let's look at the code for the Controller before diving in to how it\nworks:\n\n```python\nfrom PySink import AsyncManager\nfrom DemoAsyncWorker import DemoAsyncWorker\nfrom MainView import MainView\n\n\nclass MainController:\n def __init__(self, view: MainView):\n # Initialize/Store Attributes\n self.view = view\n self.async_manager = AsyncManager()\n # Connect UI Signals\n self.view.start_signal.connect(self.start_task)\n # Connect Async Signals\n self.async_manager.worker_progress_signal.connect(self.view.set_progress)\n self.async_manager.worker_finished_signal.connect(self.task_complete_callback)\n # Initialize UI State\n self.view.hide_progress()\n\n def start_task(self):\n # Update UI\n self.view.clear()\n self.view.show_progress()\n # Initialize/Start Worker\n worker = DemoAsyncWorker(2, cycles=5)\n self.async_manager.start_worker(worker)\n\n def task_complete_callback(self, results):\n # Update UI\n self.view.clear()\n self.view.hide_progress()\n # Handle results\n self.view.set_result(results.get('demo_result'))\n self.view.set_warnings(results.get('warnings'))\n self.view.set_errors(results.get('errors'))\n```\nIn this example, the Controller gets initialized with the view it is controlling. Injecting \nthe dependency like this allows you to create layers in your App Architecture, and moves the \nresponsibility of 'showing' the window up a level. It also makes the Controller testable. \nBy mocking the attributes and methods of our view in a different class (that\ndoesn't need UI at all) tests can be automated much faster. Again, this is not required to \nget a PySink app to work, but this practice will be beneficial in the long run.\n\nLet's take a closer look at the Controller's *\\_\\_init\\_\\_()* method:\n\n```python\nclass MainController:\n def __init__(self, view: MainView):\n # Initialize/Store Attributes\n self.view = view\n self.async_manager = AsyncManager()\n # Connect UI Signals\n self.view.start_signal.connect(self.start_task)\n # Connect Async Signals\n self.async_manager.worker_progress_signal.connect(self.view.set_progress)\n self.async_manager.worker_finished_signal.connect(self.task_complete_callback)\n # Initialize UI State\n self.view.hide_progress()\n```\n\nWithin the *\\_\\_init__()* method, any attributes needed by the controller are initialized and\nstored. In this case, those are the View and an AsyncManager (storing the manager as an \nattribute ensures that the worker and thread stay alive after calling the Manager's \n*start_worker* method).\n\nThe View's signals are then connected to the internal methods that respond to them. In this \ncase, it is a single signal called *button_pressed_signal* which gets connected to the \nController's *start_task* method. We also connect the Manager's signals to their respective \ncallback functions: *self.view.update_progress* for the progress signal and *self.task_complete_callback* for the \ncompletion signal.\n\nLastly, the UI is placed into the intended initial state that will be displayed upon \napplication launch. In this example, that just means displaying the start button.\n\nLet's take a closer look at the Controller's *start_task* method. This is the method that gets\ncalled when the user presses the start button:\n\n```python\n def start_task(self):\n # Update UI\n self.view.clear()\n self.view.show_progress()\n # Initialize/Start Worker\n worker = DemoAsyncWorker(2, cycles=5)\n self.async_manager.start_worker(worker)\n```\nHere, the UI is updated so that it is in the correct state for the long-running task to take\nplace. This usually means clearing out old data, showing the progress widgets, and \ndisabling/hiding anything that shouldn't be shown while the task is running.\n\nAfter the UI is dealt with, the task is started by simply creating a new instance of the Worker \n(passing in any values it needs), and passing it to the *start_worker* method of the \nAsyncManager. And that's it! The worker is now running in the background and the progress \nis getting updated within the UI. \n\nNow let's look at what happens when the worker is done running its task. Since the \nAsyncManager's *worker_finished_signal* was connected to the Controller's \n*task_complete_callback*, that callback will be executed upon the worker's completion. \nHere's what the callback looks like:\n\n```python\n def task_complete_callback(self, results):\n # Update UI\n self.view.clear()\n self.view.hide_progress()\n # Handle results\n self.view.set_result(results.get('demo_result'))\n self.view.set_warnings(results.get('warnings'))\n self.view.set_errors(results.get('errors'))\n```\n\nAs stated in the previous example, the results of the worker's task are provided as a \ndictionary that gets passed in to the completion callback. This dictionary contains the \nvalues defined as keyword arguments within the worker's *run* method, as well as any \nwarnings/errors encountered during the task. In this callback, the UI is again updated to \nreflect the task's completion and the results are displayed to the user by passing them into\nthe View. \n\nAnd that's it! You now have a fully functional asynchronous application. Since Dependency\nInjection was implemented in the Controller, you will need to instantiate the Controller\nand pass it an existing View. Create a new file, import the Controller and View, and start\na QApplication:\n\n```python\nfrom PySide6.QtWidgets import QApplication\nfrom MainController import MainController\nfrom MainView import MainView\n\n\napp = QApplication()\nview = MainView()\ncontroller = MainController(view)\nview.show()\napp.exec()\n```\n\nRun the script and the application will start. Pushing the start button within the app will \ntrigger the long-running task, and at its completion data from the worker will be displayed:\n\n![alt text](docs/img/example2_while_running.png \"Title\")\n\n![alt text](docs/img/example2_complete.png \"Title\")\n\n\nCongratulations! You've just created an asynchronous app with PySink!\nFull example code can be found at https://github.com/zackjohnson298/PySink \nin the examples/example2 folder.\n",
"bugtrack_url": null,
"license": "MIT License Copyright (c) 2023 Zack Johnson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
"summary": "PySide6 Helpers for Powerful Async Desktop Apps",
"version": "0.0.10",
"split_keywords": [
"python",
"app",
"gui",
"async",
"application",
"desktop",
"ui"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "1ee07a02ae26a7ec09f4ba3f7f7002d4efbc9f674de73b37a987062ee060ec0a",
"md5": "e3ed836040c31307819338752c582c58",
"sha256": "8403a0aab05aa9ed3d3cdf4f5499a189312100e8ba837432bedb8ac49fbeff37"
},
"downloads": -1,
"filename": "PySink-0.0.10-py3-none-any.whl",
"has_sig": false,
"md5_digest": "e3ed836040c31307819338752c582c58",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.9",
"size": 15669,
"upload_time": "2023-04-07T18:57:10",
"upload_time_iso_8601": "2023-04-07T18:57:10.608138Z",
"url": "https://files.pythonhosted.org/packages/1e/e0/7a02ae26a7ec09f4ba3f7f7002d4efbc9f674de73b37a987062ee060ec0a/PySink-0.0.10-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "e97d275b458733734e30f98837022e18ceee6615eafbe73542e6d90f8801c0c3",
"md5": "71aeb39c41b99ef48c4a9bddbecb47a3",
"sha256": "809df115811c59f99354a31488362d033a371e8d170aaec48aae453631623baf"
},
"downloads": -1,
"filename": "PySink-0.0.10.tar.gz",
"has_sig": false,
"md5_digest": "71aeb39c41b99ef48c4a9bddbecb47a3",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.9",
"size": 18042,
"upload_time": "2023-04-07T18:57:11",
"upload_time_iso_8601": "2023-04-07T18:57:11.894155Z",
"url": "https://files.pythonhosted.org/packages/e9/7d/275b458733734e30f98837022e18ceee6615eafbe73542e6d90f8801c0c3/PySink-0.0.10.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-04-07 18:57:11",
"github": false,
"gitlab": false,
"bitbucket": false,
"lcname": "pysink"
}