PyQt5: Using decorators with methods connected to signals - python

I want to use a decorator to handle exceptions within a class. However, when using the decorator, it always gives me:
TypeError: f1() takes 1 positional argument but 2 were given
I am quite sure it occurs due to self, because it works perfectly outside the class. But I cannot manage to use it inside the class. Can someone help me?
Here is a MWE:
from PyQt5.QtWidgets import QPushButton, QVBoxLayout, QApplication, QWidget
import sys
def report_exceptions(f):
def wrapped_f(*args, **kwargs):
try:
f(*args, **kwargs)
except Exception as e:
print(f"caught: {e}")
return wrapped_f
class Window(QWidget):
def __init__(self):
super().__init__()
b1 = QPushButton('1')
b1.clicked.connect(self.f1)
layout = QVBoxLayout(self)
layout.addWidget(b1)
self.setLayout(layout)
#report_exceptions
def f1(self):
raise Exception("Error inside f1")
app = QApplication([])
window = Window()
window.show()
sys.exit(app.exec_())

The error occurs because the clicked signal sends a default checked parameter which the f1 method does not provide an argument for. There are several ways to work around this:
change the signature to allow for the extra argument:
def f1(self, checked=False):
use a lambda to consume the unwanted argument:
b1.clicked.connect(lambda: self.f1())
wrap the method as a pyqt slot:
#QtCore.pyqtSlot()
#report_exceptions
def f1(self):
The reason why this last one works is because signals with default arguments are treated as two separate overloads: one which which sends the parameter, and one which doesn't. The slot decorator therefore allows you to explicitly select which one is required (the other overload being #QtCore.pyqtSlot(bool)).
Another peculiarity specific to PyQt is that signals connected to undecorated slots will normally ignore unused parameters. The lambda solution above takes advantage of this mechanism, but your report_exceptions decorator effectively bypasses it, which is why you get the TypeError.

Related

How to access self object within class from outside

I am writing an application that uses a Qt GUI and am now trying to multithread it (first time learner). A longer task will be contained within the worker thread eventually but output logs will be required to be written as it goes. The GUI class has a method to output these logs to a plain text widget. Up until now, everything has been running within the GUI class.
I currently have the following code (included the important bits for brevity):
class Worker(QRunnable):
#pyqtSlot()
def run(self):
Ui.logentry(self, "Test")
class Ui(QtWidgets.QMainWindow):
def __init__(self):
super(Ui, self).__init__()
uic.loadUi(resourcepath('./pycdra.ui'), self)
self.outputlog = self.findChild(QtWidgets.QPlainTextEdit, 'outputlog')
self.button = self.findChild(QtWidgets.QPushButton, 'button')
self.button.clicked.connect(self.Button)
self.threadpool = QThreadPool()
self.logentry("Available threads: %d" % self.threadpool.maxThreadCount())
def Button(self):
worker = Worker()
self.threadpool.start(worker)
def logentry(self, returntext):
self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext))
self.outputlog.repaint()
def timestamp(self):
import datetime
ts = datetime.datetime.now(tz=None).strftime("%Y-%m-%d %H:%M:%S"))
return ts
def applic():
app = QApplication(sys.argv)
window = Ui()
window.show()
sys.exit(app.exec_())
applic()
When I try running this, the GUI loads perfectly, but upon pushing button the Ui.logentry part of Worker returns the error: AttributeError: 'Worker' object has no attribute 'outputlog'
I attempted making logentry() and timestamp() global so they can access and be accessed by both classes, but the closest I got with the line Ui.self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext)) was that 'Ui' object has no attribute 'self'.
What am I missing? Am I right in making them global or are there other means to do this?
There are various problems with your code. I'll try to address them in order of importance.
logentry is an instance method of Ui, which means that it requires an instance of Ui in order to be run; what you're trying to achieve would never work since that self refers to an instance of Worker, which clearly doesn't have any outputlog attribute (which is what logentry expects instead). Remember: the self argument is not there just for fun, it always refers to the object (the instance, for instance methods) the function is called from; self.logentry means "call the function logentry using self as first argument. Since, in your case, self refers to an instance of Ui, that explains your main issue, since that instance doesn't have that attribute.
None is exactly what its name says: nothing. The timestamp function will throw another AttributeError, since there's no strftime attribute in None. That is part of a datetime object, so you have to get a now object, then call its strftime function against it. In any case, the tz argument requires a tzinfo subclass.
pyqtSlot only works for Qt classes that inherit from QObject (such as QWidget). QRunnable isn't one of those classes. You can check the whole inheritance tree in the header of the documentation in each class: if there's a "Inherits:" field, go up until there's none. If you get a QObject inheritance at some point, then you can use slots and signals for that object, otherwise not. Consider that: QWidget also inherits from QObject; all Qt widgets inherit from QWidget; there are Qt classes that inherit from QObject but are not widgets.
Even ignoring all the above, access to UI elements is always forbidden from external threads (including QRunnable objects); while you can theoretically (but unreliably) get their properties, trying to set them will most likely cause a crash; in order to change something in a UI element, using signals is mandatory. Note that "access" also includes creation, and always results in a crash.
Calling repaint is a common (and wrong) attempt to solve the above issue; that's unnecessary, as properly setting widget properties (like using appendPlainText()) already results in a scheduled repaint on its own; there are very few cases for which repaint is actually necessary, and the rule of thumb is that if you're calling it you probably don't know what your doing or why your doing it. In any case, calling update() is always preferred, and it must always be called from the main UI thread anyway.
Using imports in a function is rarely required, as import statements should always be in the very beginning of the script; while there are cases for which imports can (or should) be done later or in a specific function, doing them in a function that is likely to be called often, makes using them there completely pointless. Also, datetime is part of the standard library, so importing it on demand will hardly affect performance (especially considering its "performance weight" against what a big library like Qt is compared to it).
When the ui is loaded from a .ui file (or a pyuic generated file), PyQt alread creates all widgets as instance attributes, so there's no need for findChild. Unfortunately there are a lot of tutorials that suggest that approach, and they are just completely and plain wrong. You can already access those widgets as self.outputlog and self.button right after uic.loadUi.
Function names (like variables and attributes) should always begin with a lower case letter, as only classes and constants should begin with upper cases (see the official Style Guide for Python Code). Also, object names should always explain what those object do (see "self-documenting code"): a function that does an "action" should have a verb; if it's named "Button" it doesn't tell me that it's going to do some processing, and that's not a very good thing.
A "main" function (like your applic) usually makes sense within the common if __name__ == '__main__': block, which ensures that that function doesn't get called in case the file gets imported instead of being directly run. (See this answer and the related question).
Since QRunnable doesn't inherit from QObject, we can create a QObject subclass that acts as a signal "proxy", and make it a member of the QRunnable instance. Then we must connect to that signal everytime we create a new Worker object.
Here is a revised version of your code, based on the above points.
import datetime
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUi
class WorkerSignals(QObject):
mySignal = pyqtSignal(str)
class Worker(QRunnable):
def __init__(self):
super().__init__()
self.signalProxy = WorkerSignals()
self.mySignal = self.signalProxy.mySignal
def run(self):
self.mySignal.emit("Test")
class Ui(QMainWindow):
def __init__(self):
super(Ui, self).__init__()
loadUi('./pycdra.ui', self)
self.button.clicked.connect(self.startWorker)
self.threadpool = QThreadPool()
self.logentry("Available threads: %d" % self.threadpool.maxThreadCount())
def startWorker(self):
worker = Worker()
worker.mySignal.connect(self.logentry)
self.threadpool.start(worker)
def logentry(self, returntext):
self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext))
def timestamp(self):
ts = datetime.datetime.now()
return ts.strftime("%Y-%m-%d %H:%M:%S")
def applic():
import sys
app = QApplication(sys.argv)
window = Ui()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
applic()
I suggest you to carefully study the provided code and the differences with yours, and then do some careful, patient research both on the links given above and the following topics:
classes and instances, and the meaning of self;
python types (including None);
what is event driven programming and how it relates with graphical interfaces;
the most important Qt classes, QObject and QWidget, and all their properties and functions (yes, they are a lot);
general PyQt related topics
(most importantly, signals/slots and properties);
code styling and good practices;
Use the class' initialised form
class foo:pass
selfuotsideclass=foo()
Use variable selfoutsideclass as self

Click a button and change MainWindow

I am learning Qt and I am interesting in how to change some features of MainWindow.
I was trying this code, but there were some errors when I clicked the first button:
Traceback (most recent call last):
File "\main.py", line 15, in run_the_first_button_was_clicked
the_first_button_was_clicked(self)
File "clickedButton.py", line 15, in the_first_button_was_clicked
self.button2.clicked.connect(self.the_second_button_was_clicked)
AttributeError: 'MainWindow' object has no attribute 'the_second_button_was_clicked'
what I did wrong (how could I do 'the_second_button_was_clicked' callable )?
main.py
from PySide2.QtWidgets import QApplication, QMainWindow, QPushButton
import sys
from clickedButton import the_first_button_was_clicked, the_second_button_was_clicked
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("MainWindow")
self.button1 = QPushButton("button1")
self.button1.clicked.connect(self.run_the_first_button_was_clicked)
self.setCentralWidget(self.button1)
def run_the_first_button_was_clicked(self):
the_first_button_was_clicked(self)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
clickedButton.py
from PySide2.QtWidgets import QPushButton
from PySide2 import QtCore
def the_first_button_was_clicked(self):
self.setWindowTitle("the_first_button_was_clicked next will be the_second_button_was_clicked")
self.resize(800, 600)
self.button1.setText("the_first_button_was_clicked")
self.button1.setEnabled(False)
self.button2 = QPushButton("button2")
self.button2.setGeometry(QtCore.QRect(100, 100, 150, 150))
self.button2.setVisible(True)
self.button2.clicked.connect(self.the_second_button_was_clicked)
def the_second_button_was_clicked(self):
self.setWindowTitle("the_second_button_was_clicked")
self.resize(600, 800)
The issue has nothing to do with PyQt, but with how classes and instances work.
The first argument of instance methods always refers to the instance of the class, and it's called self just for convention: it could actually be named in any way as long as its syntax is valid, just like any other variable.
When using functions that are declared outside a class, it's good practice to avoid that naming convention (mostly to avoid confusion when reading code).
What is happening is that the self in def the_first_button_was_clicked(self): refers to the instance of MainWindow, which has no the_second_button_was_clicked method, hence the AttributeError exception.
The point is that both your functions are just functions, not methods (which are functions of an instance or a class): they are not members of the class.
Also note that creating a direct connection to the function will not work, as the self argument is only "created" when a function is a method.
As Heike pointed out in the comments, a possibility is to use lambda, which allows keeping an actual reference to the instance, while directly calling the function, which will be executed using the self argument provided, exactly as you did in run_the_first_button_was_clicked.
In the following examples I'm replacing self with mainWinInstance in order to make things more clear (which is the reason for which self should not be used in these cases).
def the_first_button_was_clicked(mainWinInstance):
# we reference the function locally to this script, adding the "self" argument
mainWinInstance.button2.clicked.connect(lambda:
the_second_button_was_clicked(mainWinInstance))
def the_second_button_was_clicked(mainWinInstance):
# "self" (mainWinInstance) was manually added to the function arguments
mainWinInstance.setWindowTitle("the_second_button_was_clicked")
mainWinInstance.resize(600, 800)
Another possibility is to make the second function a member of the instance:
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# ...
self.the_second_button_was_clicked = the_second_button_was_clicked
or:
def the_first_button_was_clicked(mainWinInstance):
# ...
mainWinInstance.the_second_button_was_clicked = the_second_button_was_clicked
mainWinInstance.button2.clicked.connect(mainWinInstance.the_second_button_was_clicked)
In both cases the instance attribute has to be created before the connection (which also means before calling the first function in the first case).
Consider that this "monkey patching" approaches should only be used in special cases (mostly due to objects that cannot be subclassed because created autonomously), especially if done outside the class or even the script.
In most cases, what you're doing is considered bad practice, and if you're doing this with a class created on your own, there's probably something really wrong in your implementation: you should better rethink your logic and implement everything within the class itself.

Creating an array of pyqtSignal

For a QThread I would like to create an array of pyqtSignal
class MyThread(QtCore.QThread):
Trigger = []
for i in range(0,10):
Trigger.append(QtCore.pyqtSignal(int))
def __init__(self, Function):
self.Function = Function
super(MyThread, self).__init__(None)
def run(self):
self.Function()
The main part of the following code looks like:
class Main(QtWidgets.QMainWindow):
def __init__(self):
self.MyQThread = MyThread(lambda: self.PrintTest(5))
def StartTestThread(self):
self.MyQThread.Trigger[0].connect(self.update_text)
self.MyQThread.start()
def PrintTest(self,InputValue):
for i in range (0,100):
print(InputValue*i)
time.sleep(0.2)
self.MyQThread.Trigger[0].emit(5)
def update_text(self, thread_no):
self.ui.MY_LISTWIDGET.addItem('123')
executing the StartTestThread leads to the following error
AttributeError: 'PyQt5.QtCore.pyqtSignal' object has no attribute
'connect'
If I initiliaize the pyqtSignal without being an array, it works.
What am I doing wrong? Thanks for the help in advance!
You cannot create a list of pyqtSignal(s).
Unfortunately the way pyqt implement signals use a bit of python magics, and the pyqtSignal objects are actually "converted" into pyqtBoundSignal when a QObject subclass (technically a class that have pyqtWrapperType as a metaclass) is loaded.
You can solve your problem in different ways:
1) Wrap the signal
I'm not 100% sure about this, but it's a modified version of your attempt:
class FooWrap(QObject):
Signal = QtCore.pyqtSignal(int)
class MyThread(QtCore.QThread):
Trigger = [FooWrap] * 10
2) Don't use a list
If the number of signals is fixed, just create them directly as signal1, signal2, signalN, then you can call them directly
If you can determine which signal to call only at runtime you can get the signal you need using the gettattr(object, name) function, for example:
getattr(self.MyQThread, 'signal' + str(n)).connect(self.update_text)
and
getattr(self.MyQThread, 'signal' + str(n)).emit(value)
3) Use only one signal
Use only one signal that emit two values, one identify the "step" in which the signal is emitted, and the second is your value, doing so the connected functions can decide what to do based on the "step" value.

Custom pyqtSignal implementation

In PyQt, you can use QtCore.pyqtSignal() to create custom signals.
I tried making my own implementation of the Observer pattern in place of pyqtSignal to circumvent some of its limitations (e.g. no dynamic creation).
It works for the most part, with at least one difference.
Here is my implementation so far
class Signal:
def __init__(self):
self.__subscribers = []
def emit(self, *args, **kwargs):
for subs in self.__subscribers:
subs(*args, **kwargs)
def connect(self, func):
self.__subscribers.append(func)
def disconnect(self, func):
try:
self.__subscribers.remove(func)
except ValueError:
print('Warning: function %s not removed from signal %s'%(func,self))
The one thing noticed was a difference in how QObject.sender() works.
I generally stay clear of sender(), but if it works differently then so may other things.
With regular pyqtSignal signals, the sender is always the widget closest in a chain of signals.
In the example at the bottom, you'll see two objects, ObjectA and ObjectB. ObjectA forwards signals from ObjectB and is finally received by Window.
With pyqtSignal, the object received by sender() is ObjectA, which is the one forwarding the signal from ObjectB.
With the Signal class above, the object received is instead ObjectB, the first object in the chain.
Why is this?
Full example
# Using PyQt5 here although the same occurs with PyQt4
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
object_a = ObjectA(self)
object_a.signal.connect(self.listen)
layout = QBoxLayout(QBoxLayout.TopToBottom, self)
layout.addWidget(object_a)
def listen(self):
print(self.sender().__class__.__name__)
class ObjectA(QWidget):
signal = Signal()
# signal = pyqtSignal()
def __init__(self, parent=None):
super(ObjectA, self).__init__(parent)
object_b = ObjectB()
object_b.signal.connect(self.signal.emit)
layout = QBoxLayout(QBoxLayout.TopToBottom, self)
layout.addWidget(object_b)
class ObjectB(QPushButton):
signal = Signal()
# signal = pyqtSignal()
def __init__(self, parent=None):
super(ObjectB, self).__init__('Push me', parent)
self.pressed.connect(self.signal.emit)
if __name__ == '__main__':
import sys
app = QApplication([])
win = Window()
win.show()
sys.exit(app.exec_())
More reference
Edit:
Apologies, I should have provided a use-case.
Here are some of the limitations of using pyqtSignals:
pyqtSignal:
..only works with class attributes
..cannot be used in an already instantiated class
..must be pre-specified with the data-types you wish to emit
..produces signals that does not support keyword arguments and
..produces signals that cannot be modified after instantiation
Thus my main concern is using it together with baseclasses.
Consider the following.
6 different widgets of a list-type container widget share the same interface, but look and behave slightly different. A baseclass provides the basic variables and methods, along with signals.
Using pyqtSignal, you would have to first inherit your baseclass from at least QObject or QWidget.
The problem is neither of these can be use in as mix-ins or in multiple inheritance, if for instance one of the widgets also inherits from QPushButton.
class PinkListItem(QPushButton, Baseclass)
Using the Signal class above, you could instead make baseclasses without any previously inherited classes (or just object) and then use them as mix-ins to any derived subclasses.
Careful not to make the question about whether or not multiple inheritance or mix-ins are good, or of other ways to achieve the same thing. I'd love your feedback on that as well, but perhaps this isn't the place.
I would be much more interested in adding bits to the Signal class to make it work similar to what pyqtSignal produces.
Edit 2:
Just noticed a down-vote, so here comes some more use cases.
Key-word arguments when emitting.
signal.emit(5)
Could instead be written as
signal.emit(velocity=5)
Use with a Builder or with any sort of dependency injection
def create(type):
w = MyWidget()
w.my_signal = Signal()
return w
Looser coupling
I'm using both PyQt4 and PyQt5. With the Signal class above, I could produce baseclasses for both without having it depend on either.
You can do this with a metaclass that inherits from pyqtWrapperType. Inside __new__, call pyqtSignal() as needed and set the attributes on the result class.

Event - catch only Qt.Key_Delete

I would like to change behavior of Key_Delete for QTableWidget, but because of using Designer I don't want to make any changes in py file with form. So instead of reimplementing QTableWidget like it is this answer: How to implement MousePressEvent for a Qt-Designer Widget in PyQt I do something like this:
class MyForm(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.tableWidget.__class__.keyPressEvent = self.test
def test(self,event):
if event.key() == Qt.Key_Delete:
print "test"
return QTableWidget.keyPressEvent(self, event)
The problem is that I don't know how to keep original behavior of other keys than Qt.Key_Delete.
I have already changed last line like this:
return QtGui.QTableWidget.keyPressEvent(self, event)
return QtGui.QTableWidget.event(self, event)
but it doesn't work.
First: "but it doesn't work" is usually not descriptive enough. What behavior did you expect? What was the behavior you saw instead? Were there any error messages?
I can easily see a few mistakes in here, though.
You're overriding the method QTableWidget.keyPressEvent, which expects 2 arguments: (QTableWidget instance, event). But in the code you show above, the function you are using to override it only takes 1 argument (the first argument, 'self', does not count since it is automatically supplied).
Since you have set QTableWidget.keyPressEvent = self.test, and you are also trying to call this function from within self.test(), you have created an infinitely recursive function.
When you call QTableWidget.keyPressEvent, the first argument you have passed in (self) is a QMainWindow object. However as I mentioned above, this function expects a QTableWidget as its first argument.
Since you are overriding this method at the class level, ALL QTableWidgets will be forced to use the same keyPressEvent function (this would be very problematic). Instead, you should override just your specific widget's method: self.ui.tableWidget.keyPressEvent = self.test (also note that the signature for tableWidget.keyPressEvent is different from QTableWidget.keyPressEvent)
Example:
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
win = QtGui.QMainWindow()
win.show()
table = QtGui.QTableWidget()
win.setCentralWidget(table)
def test(event):
if event.key() == QtCore.Qt.Key_Delete:
print "delete"
return QtGui.QTableWidget.keyPressEvent(table, event)
table.keyPressEvent = test
Finally, another (possibly cleaner) approach would be to create a subclass of QTableWdget and, within Designer, 'promote' the table widget to your new class.

Categories