Home > Enterprise >  Unable to overwrite class method in pyqt6 application
Unable to overwrite class method in pyqt6 application

Time:08-26

So normally you can overwrite a class method by doing something like this.

class A():
    def test(self):
        return 1 1

def edit_patch(func):
    def inner(*args,**kwargs):
        print('this test worked')
    return inner

a=A()
a.test = edit_patch(a.test)

Now a.test will return 'this test worked' instead of 2. I'm trying to do something similar in my pyqt6 application. The function below belongs to the "main" class in my code and is connected to a button click. This function is meant to instantiate another class (which is another window in pyqt6). That part works, but I would like to alter the behavior of the select function in this instance. However the method above doesn't seem to work as the select function continues to exhibit the default behavior.

def edit_proj(self):
        self.psearch=PSearch(conn=self.conn,parent=self)
        self.psearch.select = edit_patch(self.psearch.select)
        self.psearch.show()

Any help on this would be great

As requested, here is an MRE

from PyQt6 import QtCore, QtGui, QtWidgets

def edit_patch(func):
    def inner(*args,**kwargs):
        print('this test worked')
    return inner

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(50, 50)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.EditProjButton = QtWidgets.QPushButton(self.centralwidget)
        self.EditProjButton.setObjectName("EditProjButton")

        self.EditProjButton.clicked.connect(self.nextwindow)

    def nextwindow(self):
        print('hello from main window')
        self.newwindow=Ui_ProjSearchForm(QtWidgets.QWidget())

        self.newwindow.select = edit_patch(self.newwindow.select)

        self.newwindow.show()

class Ui_ProjSearchForm(QtWidgets.QWidget):
    
    def __init__(self, *args, **kwargs):
        super().__init__()
        self.setupUi(self)

        self.SearchButton.clicked.connect(self.select)

    def setupUi(self, ProjSearchForm):
        ProjSearchForm.setObjectName("ProjSearchForm")
        ProjSearchForm.resize(100, 100)
        self.gridLayout = QtWidgets.QGridLayout(ProjSearchForm)
        self.gridLayout.setObjectName("gridLayout")
        self.SearchButton = QtWidgets.QPushButton(ProjSearchForm)
        self.SearchButton.setObjectName("SearchButton")
        self.gridLayout.addWidget(self.SearchButton, 0, 2, 1, 1)


    def select(self):
        print('this is default behavior')

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec())

CodePudding user response:

Signal connections work by passing a reference to a callable, and that reference is an "internal" pointer to that function. Overwriting the name of that function will have absolutely no result.

Take this example:

class Test(QPushButton):
    def __init__(self):
        super().__init__('Click me!')
        self.clicked.connect(self.doSomething)
        self.doSomething = lambda: print('bye!')

    def doSomething(self):
        print('hello!')

The code above will always print "hello!", because you passed the reference to the instance method doSomething that existed at the time of the connection; overwriting it will not change the result.

If you need to create a connection that can be overwritten, you have different possibilities.

Pass the function to the constructor

You can set the function as an optional argument in the __init__ and then connect it if specified, otherwise use the default behavior:

    def nextwindow(self):
        self.newwindow = Ui_ProjSearchForm(edit_patch(self.newwindow.select))

class Ui_ProjSearchForm(QtWidgets.QWidget):
    def __init__(self, func=None):
        super().__init__()
        self.setupUi(self)

        if func is not None:
            self.SearchButton.clicked.connect(func)
        else:
            self.SearchButton.clicked.connect(self.select)

Create a method for the connection

In this case we pass the reference to a specific method that will create the connection, eventually disconnecting any previous connection (remember that signals can be connected to multiple functions, and even the same function multiple times). This is similar to the approach above.

    def nextwindow(self):
        self.newwindow = Ui_ProjSearchForm()
        self.newwindow.setSelectFunc(edit_patch(self.newwindow.select))

class Ui_ProjSearchForm(QtWidgets.QWidget):
    def __init__(self, func=None):
        super().__init__()
        self.setupUi(self)
        self.SearchButton.clicked.connect(self.select)

    def select(self):
        print('this is default behavior')

    def setSelectFunc(self, func):
        try:
            self.SearchButton.clicked.disconnect(self.select)
        except TypeError:
            pass
        self.select = func
        self.SearchButton.clicked.connect(self.select)

Use a lambda

As said above, the problem was in trying to overwrite the function that was connected to the signal: even if the connected function is a wrapper, the direct reference for the connection is not actually overwritten.

If you, instead, connect to a lambda that finally calls the instance method, it will work as expected, because the lambda is dynamically computed at the time of its execution and at that time self.select will be a reference to the overwritten function.

    def nextwindow(self):
        self.newwindow = Ui_ProjSearchForm()
        self.newwindow.select = edit_patch(self.newwindow.select)

class Ui_ProjSearchForm(QtWidgets.QWidget):
    def __init__(self, func=None):
        super().__init__()
        self.setupUi(self)

        self.SearchButton.clicked.connect(lambda: self.select())

    def select(self):
        print('this is default behavior')

Some unrelated but still important notes:

  1. You should never edit pyuic generated files, nor try to merge their code into your script or mimic their behavior. Instead, follow the official guidelines about using Designer.
  2. Passing a new QWidget instance as argument is pointless (other than wrong and potentially dangerous); if you want to create a new window for the new widget, just avoid any parent at all, otherwise use QDialog for modal windows.
  3. Only classes and constants should have capitalized names, everything else should be named starting with lowercase letters (this includes object names created in Designer); read more about this and other important topics in the official Style Guide for Python Code.

CodePudding user response:

Ok I think I've figured out a solution (in the MRE posted in question). There's some shenanigans that go on in the back ground once you connect a button in the UI to a function. It's not a "live" connection like in the a.test example, so edits to the function later don't have an impact on how the button functions.

So, if we replace

self.newwindow.select = edit_patch(self.newwindow.select)

with

self.newwindow.SearchButton.clicked.disconnect()
self.newwindow.select = edit_patch(self.newwindow.select)
self.newwindow.SearchButton.clicked.connect(self.newwindow.select)

we suddenly get the desired behavoir from the button. This was entirely too frustrating.

  • Related