├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── requirements.txt ├── setup.py ├── src ├── __init__.py ├── app.py ├── button.py ├── controllers │ ├── __init__.py │ └── main_ctrl.py ├── model │ ├── __init__.py │ └── model.py ├── mvc_app.py ├── resources │ ├── __init__.py │ └── main_view.ui └── views │ ├── __init__.py │ ├── main_view.py │ └── main_view_ui.py └── tests ├── __init__.py ├── context.py ├── test_app.py └── test_button.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | .vscode/settings.json 125 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 3.7 4 | sudo: required 5 | services: xvfb 6 | before_install: 7 | - python --version 8 | - uname -a 9 | - lsb_release -a 10 | - sudo apt-get install -y xvfb herbstluftwm dzen2 11 | - pip install -U pip 12 | - pip install -U pytest 13 | # command to install dependencies 14 | install: 15 | - "export DISPLAY=:99.0" 16 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset" 17 | - sleep 5 18 | - pip install -r requirements.txt 19 | 20 | # command to run tests 21 | before_script: 22 | - "herbstluftwm &" 23 | - sleep 5 24 | script: 25 | - py.test --cov-report term --cov=src/ 26 | env: 27 | - CODECOV_TOKEN="0d56028c-9cfd-4536-964a-5151c4115d3d" 28 | after_success: 29 | - codecov 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Samuel Krieg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyQt5-MVC-Template 2 | 3 | :warning: Template is Outdated and needs rework 4 | 5 | ## Usage 6 | 1. Install Dependencies: ```pip3 install -r requirements.txt``` 7 | 2. Start App: ```python3 src/mvc_app.py``` 8 | 9 | ## Development 10 | 11 | ### Dev Requirements 12 | 13 | - Install `QT-Creator` 14 | 1. Download [QT-Creator](https://www.qt.io/product/development-tools) installer. 15 | 2. Select `QT *.* for desktop development` in the installer. 16 | 17 | ### Various Infos 18 | 19 | - Style Guide: [PEP 8](https://www.python.org/dev/peps/pep-0008/) 20 | - Architecture: [Model-View-programming](https://doc.qt.io/qt-5/model-view-programming.html) 21 | 22 | ### Change to GUI 23 | 24 | QTCreator is used to create the visual representation of the GUI (view component). With the QTCreator, the GUI can be created with a graphical interface, and then the corresponding python code can be generated. The generated code must not be modified by hand to make GUI layout adjustments, otherwise the QTCreator and the generated files are no longer coherent. 25 | 26 | The QTCreator files to create the views can be found in the [./src/views](./src/views) folder. 27 | 28 | #### Workflow 29 | 30 | 1. Edit `.ui` files in QT-Creator 31 | 2. Convert `.ui` files to `.py` for [PyQt5](https://pypi.org/project/PyQt5/) with [pyuic5](https://pypi.org/project/pyuic5-tool/) 32 | 33 | ```bash 34 | pyuic5 .\src\views\mainView.ui -o .\src\views\main_view_ui.p 35 | ``` 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | PyQt5 3 | 4 | #Testing 5 | pytest-cov 6 | pytest-qt 7 | # GUI for testing 8 | pytest-xvfb 9 | pytest 10 | codecov -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='Example', 5 | version='1.0', 6 | description='Python-UI-DB-Example', 7 | author='Samuel Krieg', 8 | author_email='sikcd90@gmail.com', 9 | install_requires=['bar', 'greek'], #external packages as dependencies 10 | url='https://github.com/develmusa/Python-UI-DB-Example.git', 11 | license=license, 12 | packages=find_packages(exclude=('tests')) 13 | ) -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develmusa/PyQt5-MVC-Template/626a2d9cd06318645ef7bf3a8f45499d9ce77a9c/src/__init__.py -------------------------------------------------------------------------------- /src/app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def hello_world(): 4 | return "Hello, World!" -------------------------------------------------------------------------------- /src/button.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel 3 | from PyQt5.QtGui import QIcon 4 | from PyQt5.QtCore import pyqtSlot 5 | 6 | class SimpleButton(QWidget): 7 | 8 | def __init__(self): 9 | super().__init__() 10 | self.title = 'PyQt5 button - pythonspot.com' 11 | self.left = 50 12 | self.top = 50 13 | self.width = 320 14 | self.height = 200 15 | self.initUI() 16 | 17 | def initUI(self): 18 | self.setWindowTitle(self.title) 19 | self.setGeometry(self.left, self.top, self.width, self.height) 20 | 21 | self.buttonA = QPushButton('PyQt5 button', self) 22 | self.buttonA.setToolTip('This is an example button') 23 | self.buttonA.move(100,70) 24 | self.buttonA.clicked.connect(self.on_click) 25 | 26 | self.labelA = QLabel(self) 27 | self.labelA.setText('Label Example') 28 | 29 | self.show() 30 | 31 | @pyqtSlot() 32 | def on_click(self): 33 | self.labelA.setText('Changed') 34 | print('PyQt5 button click') 35 | 36 | 37 | if __name__ == '__main__': 38 | app = QApplication(sys.argv) 39 | ex = SimpleButton() 40 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /src/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develmusa/PyQt5-MVC-Template/626a2d9cd06318645ef7bf3a8f45499d9ce77a9c/src/controllers/__init__.py -------------------------------------------------------------------------------- /src/controllers/main_ctrl.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QObject, pyqtSlot 2 | 3 | 4 | class MainController(QObject): 5 | def __init__(self, model): 6 | super().__init__() 7 | 8 | self._model = model 9 | 10 | # Takes Signal from UI 11 | @pyqtSlot(int) 12 | def change_amount(self, value): 13 | self._model.amount = value 14 | 15 | # calculate even or odd 16 | self._model.even_odd = 'odd' if value % 2 else 'even' 17 | 18 | # calculate button enabled state 19 | self._model.enable_reset = True if value else False 20 | 21 | @pyqtSlot(str) 22 | def add_user(self, value): 23 | self._model.add_user(value) 24 | # calculate button enabled state 25 | #if(self._model.users.count > 0): 26 | # self._model.enable_del_user = True if value else False 27 | 28 | @pyqtSlot(int) 29 | def delete_user(self, value): 30 | self._model.delete_user(value) 31 | # calculate button enabled state 32 | #if(self._model.users.count > 0): 33 | # self._model.enable_del_user = True if value else False -------------------------------------------------------------------------------- /src/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develmusa/PyQt5-MVC-Template/626a2d9cd06318645ef7bf3a8f45499d9ce77a9c/src/model/__init__.py -------------------------------------------------------------------------------- /src/model/model.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtCore import QObject, pyqtSignal 2 | 3 | 4 | class Model(QObject): 5 | 6 | amount_changed = pyqtSignal(int) 7 | even_odd_changed = pyqtSignal(str) 8 | enable_reset_changed = pyqtSignal(bool) 9 | users_changed = pyqtSignal(list) 10 | 11 | @property 12 | def users(self): 13 | return self._users 14 | 15 | 16 | def add_user(self, value): 17 | self._users.append(value) 18 | self.users_changed.emit(self._users) 19 | 20 | def delete_user(self, value): 21 | del self._users[value] 22 | self.users_changed.emit(self._users) 23 | 24 | @property 25 | def amount(self): 26 | return self._amount 27 | 28 | @amount.setter 29 | def amount(self, value): 30 | self._amount = value 31 | self.amount_changed.emit(value) 32 | 33 | @property 34 | def even_odd(self): 35 | return self._even_odd 36 | 37 | @even_odd.setter 38 | def even_odd(self, value): 39 | self._even_odd = value 40 | self.even_odd_changed.emit(value) 41 | 42 | @property 43 | def enable_reset(self): 44 | return self._enable_reset 45 | 46 | @enable_reset.setter 47 | def enable_reset(self, value): 48 | self._enable_reset = value 49 | self.enable_reset_changed.emit(value) 50 | 51 | def __init__(self): 52 | super().__init__() 53 | 54 | self._amount = 0 55 | self._even_odd = '' 56 | self._enable_reset = False 57 | 58 | self._users = ["hans"] -------------------------------------------------------------------------------- /src/mvc_app.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from PyQt5.QtWidgets import QApplication 3 | from model.model import Model 4 | from controllers.main_ctrl import MainController 5 | from views.main_view import MainView 6 | 7 | 8 | class App(QApplication): 9 | def __init__(self, sys_argv): 10 | super(App, self).__init__(sys_argv) 11 | # Connect everything together 12 | self.model = Model() 13 | self.main_ctrl = MainController(self.model) 14 | self.main_view = MainView(self.model, self.main_ctrl) 15 | self.main_view.show() 16 | 17 | 18 | if __name__ == '__main__': 19 | app = App(sys.argv) 20 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /src/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develmusa/PyQt5-MVC-Template/626a2d9cd06318645ef7bf3a8f45499d9ce77a9c/src/resources/__init__.py -------------------------------------------------------------------------------- /src/resources/main_view.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 301 10 | 249 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | asdsa 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Add 33 | 34 | 35 | 36 | 37 | 38 | 39 | true 40 | 41 | 42 | Delete 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Qt::Horizontal 52 | 53 | 54 | 55 | 40 56 | 20 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | false 70 | 71 | 72 | Reset 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develmusa/PyQt5-MVC-Template/626a2d9cd06318645ef7bf3a8f45499d9ce77a9c/src/views/__init__.py -------------------------------------------------------------------------------- /src/views/main_view.py: -------------------------------------------------------------------------------- 1 | from PyQt5.QtWidgets import QMainWindow 2 | from PyQt5.QtCore import pyqtSlot 3 | from views.main_view_ui import Ui_MainWindow 4 | 5 | 6 | class MainView(QMainWindow): 7 | def __init__(self, model, main_controller): 8 | super().__init__() 9 | 10 | self._model = model 11 | self._main_controller = main_controller 12 | self._ui = Ui_MainWindow() 13 | self._ui.setupUi(self) 14 | 15 | # connect ui-widget to controller 16 | # if ui changes, it sends a signal to an slot on which we connect a controller class. 17 | # therefore we can recive the signal in the controller 18 | self._ui.spinBox_amount.valueChanged.connect(self._main_controller.change_amount) 19 | # Lambda to execute function with value 20 | self._ui.pushButton_reset.clicked.connect(lambda: self._main_controller.change_amount(0)) 21 | 22 | self._ui.pushButton_add.clicked.connect(lambda: self._main_controller.add_user(self._ui.lineEdit_name.text())) 23 | self._ui.pushButton_delete.clicked.connect(lambda: self._main_controller.delete_user(self._ui.listWidget_names.currentRow())) 24 | 25 | # listen for model event signals 26 | # connect the method to update the ui to the slots of the model 27 | # if model sends/emits a signal the ui gets notified 28 | self._model.amount_changed.connect(self.on_amount_changed) 29 | self._model.even_odd_changed.connect(self.on_even_odd_changed) 30 | self._model.enable_reset_changed.connect(self.on_enable_reset_changed) 31 | 32 | self._model.users_changed.connect(self.on_list_changed) 33 | 34 | # set a default value 35 | self._main_controller.change_amount(42) 36 | 37 | 38 | 39 | @pyqtSlot(int) 40 | def on_amount_changed(self, value): 41 | self._ui.spinBox_amount.setValue(value) 42 | 43 | @pyqtSlot(str) 44 | def on_even_odd_changed(self, value): 45 | self._ui.label_even_odd.setText(value) 46 | 47 | @pyqtSlot(bool) 48 | def on_enable_reset_changed(self, value): 49 | self._ui.pushButton_reset.setEnabled(value) 50 | 51 | @pyqtSlot(list) 52 | def on_list_changed(self, value): 53 | self._ui.listWidget_names.clear() 54 | self._ui.listWidget_names.addItems(value) -------------------------------------------------------------------------------- /src/views/main_view_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file '.\src\resources\main_view.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.11.3 6 | # 7 | # WARNING! All changes made in this file will be lost! 8 | 9 | from PyQt5 import QtCore, QtGui, QtWidgets 10 | 11 | class Ui_MainWindow(object): 12 | def setupUi(self, MainWindow): 13 | MainWindow.setObjectName("MainWindow") 14 | MainWindow.resize(301, 249) 15 | self.centralwidget = QtWidgets.QWidget(MainWindow) 16 | self.centralwidget.setObjectName("centralwidget") 17 | self.vboxlayout = QtWidgets.QVBoxLayout(self.centralwidget) 18 | self.vboxlayout.setObjectName("vboxlayout") 19 | self.verticalLayout = QtWidgets.QVBoxLayout() 20 | self.verticalLayout.setObjectName("verticalLayout") 21 | self.listWidget_names = QtWidgets.QListWidget(self.centralwidget) 22 | self.listWidget_names.setObjectName("listWidget_names") 23 | self.verticalLayout.addWidget(self.listWidget_names) 24 | self.lineEdit_name = QtWidgets.QLineEdit(self.centralwidget) 25 | self.lineEdit_name.setObjectName("lineEdit_name") 26 | self.verticalLayout.addWidget(self.lineEdit_name) 27 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 28 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 29 | self.pushButton_add = QtWidgets.QPushButton(self.centralwidget) 30 | self.pushButton_add.setObjectName("pushButton_add") 31 | self.horizontalLayout_2.addWidget(self.pushButton_add) 32 | self.pushButton_delete = QtWidgets.QPushButton(self.centralwidget) 33 | self.pushButton_delete.setEnabled(True) 34 | self.pushButton_delete.setObjectName("pushButton_delete") 35 | self.horizontalLayout_2.addWidget(self.pushButton_delete) 36 | self.verticalLayout.addLayout(self.horizontalLayout_2) 37 | spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 38 | self.verticalLayout.addItem(spacerItem) 39 | self.horizontalLayout = QtWidgets.QHBoxLayout() 40 | self.horizontalLayout.setObjectName("horizontalLayout") 41 | self.spinBox_amount = QtWidgets.QSpinBox(self.centralwidget) 42 | self.spinBox_amount.setObjectName("spinBox_amount") 43 | self.horizontalLayout.addWidget(self.spinBox_amount) 44 | self.pushButton_reset = QtWidgets.QPushButton(self.centralwidget) 45 | self.pushButton_reset.setEnabled(False) 46 | self.pushButton_reset.setObjectName("pushButton_reset") 47 | self.horizontalLayout.addWidget(self.pushButton_reset) 48 | self.verticalLayout.addLayout(self.horizontalLayout) 49 | self.vboxlayout.addLayout(self.verticalLayout) 50 | self.label_even_odd = QtWidgets.QLabel(self.centralwidget) 51 | self.label_even_odd.setObjectName("label_even_odd") 52 | self.vboxlayout.addWidget(self.label_even_odd) 53 | MainWindow.setCentralWidget(self.centralwidget) 54 | 55 | self.retranslateUi(MainWindow) 56 | QtCore.QMetaObject.connectSlotsByName(MainWindow) 57 | 58 | def retranslateUi(self, MainWindow): 59 | _translate = QtCore.QCoreApplication.translate 60 | self.lineEdit_name.setText(_translate("MainWindow", "asdsa")) 61 | self.pushButton_add.setText(_translate("MainWindow", "Add")) 62 | self.pushButton_delete.setText(_translate("MainWindow", "Delete")) 63 | self.pushButton_reset.setText(_translate("MainWindow", "Reset")) 64 | 65 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/develmusa/PyQt5-MVC-Template/626a2d9cd06318645ef7bf3a8f45499d9ce77a9c/tests/__init__.py -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 4 | 5 | import src 6 | -------------------------------------------------------------------------------- /tests/test_app.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from src import app 4 | 5 | 6 | 7 | 8 | # With unittest, tests are grouped as methods of classes. 9 | # Each such class must be a sub-class of 'unittest.TestCase'. 10 | # And that's about all you need to know about these classes! 11 | class TestHelloWorld(unittest.TestCase): 12 | """Tests for the hello_world() function""" 13 | 14 | # Each test is written as a method with a name beginning with "test_" 15 | def test_return_value(self): 16 | # Writing a doc-string for each test, explaining what it tests, 17 | # is a good idea. 18 | """test that hello_world() returns 'Hello, World!'""" 19 | 20 | # self.assertEqual() will make the test fail if the arguments are not equal. 21 | self.assertEqual(app.hello_world(), "Hello, World!") 22 | 23 | # If no assertions fail, the test passes successfully. Note that this 24 | # happens automatically; we don't have to return a value or anything 25 | # of the sort. -------------------------------------------------------------------------------- /tests/test_button.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pytest 3 | import pytest_xvfb 4 | 5 | from PyQt5.QtCore import * 6 | 7 | from src import button 8 | 9 | 10 | def test_hello(qtbot): 11 | widget = button.SimpleButton() 12 | qtbot.addWidget(widget) 13 | 14 | # click in the Greet button and make sure it updates the appropriate label 15 | qtbot.mouseClick(widget.buttonA, Qt.LeftButton) 16 | 17 | assert widget.labelA.text() == "Changed" --------------------------------------------------------------------------------