PySink


NamePySink JSON
Version 0.0.10 PyPI version JSON
download
home_page
SummaryPySide6 Helpers for Powerful Async Desktop Apps
upload_time2023-04-07 18:57:11
maintainer
docs_urlNone
author
requires_python>=3.9
licenseMIT 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"
}
        
Elapsed time: 0.06318s