How to use @pyqtSlot decorator with other decorators in QML?

SOLVED: See my answer below. I cannot accept for 2 days.

I have a Python object derived from QObject whose methods I call from QML. These methods are of course decorated with @pyqtSlot. However, there is a problem when I try to combine with my own decorators like so:

@pyqtSlot()
@decorator
def call(self):
    print('Called!')

I get the error file:X.qml:99: TypeError: Property 'call' of object X(0x7fed4c07fa50) is not a function

This is the decorator I am using:

def decorator(f):
    def wrapped(self):
        print('get decorated')
        return f(self)
    return wrapped

Could anyone clarify why this doesn't work? Some behavior of PyQt? Or am I doing something wrong?

NOTE: It will work if the decorator simply returns f, the problems are when the decorator returns a nested function such as wrapped.

3 answers

  • answered 2018-10-11 19:35 eyllanesc

    Using the decorator library does not generate problems, in the following part I show an example:

    main.py

    import sys
    from PyQt5 import QtCore, QtGui, QtQml
    import decorator
    
    
    @decorator.decorator
    def foo_decorator(f, *args, **kwargs):
        print('get decorated')
        return f(*args, **kwargs)
    
    
    class Helper(QtCore.QObject):    
        @foo_decorator
        @QtCore.pyqtSlot()
        def call(self):
            print('Called!')
    
    
    if __name__ == "__main__":
    
        app = QtGui.QGuiApplication(sys.argv)
        obj = Helper()
        engine = QtQml.QQmlApplicationEngine()
        engine.rootContext().setContextProperty("obj", obj)
        engine.load(QtCore.QUrl.fromLocalFile('main.qml'))
        if not engine.rootObjects():
            sys.exit(-1)
        sys.exit(app.exec_()) 
    

    main.qml

    import QtQuick 2.11
    import QtQuick.Controls 2.4
    
    ApplicationWindow {
        title: "Hello World"
        width: 640
        height: 480
        visible: true
    
        Component.onCompleted: obj.call()
    }
    

    Output:

    get decorated
    Called!
    

  • answered 2018-10-11 19:45 S. Nick

    Try it:

    import sys
    from PyQt5.QtWidgets import *
    from PyQt5.QtGui     import *
    from PyQt5.QtCore    import *
    
    def decorator(f):
        def wrapped(self):
            print('get decorated')
            return f(self)
        return wrapped
    
    class App(QWidget):
        def __init__(self):
            super().__init__()
            self.setWindowTitle("PyQt5 button")
            self.setGeometry(500, 100, 320, 200)
            button = QPushButton('  \n   PyQt5\n   button\n  ', self)
            button.setIcon(QIcon("E:/_Qt/img/qt-logo.png"))
            button.setIconSize(QSize(34, 34))
            button.setToolTip('This is an example button')
            button.setFlat(True) 
            button.move(120,70) 
            button.clicked.connect(self.on_click)
    
        @pyqtSlot()
        @decorator
        def on_click(self):
            print('PyQt5 button click')
    
    if __name__ == '__main__':
        app = QApplication(sys.argv)
        ex = App()
        ex.show()
        sys.exit(app.exec_())
    

    enter image description here

  • answered 2018-10-12 00:28 user985779

    Okay, with the help of @eyllanesc's answer I was able to get to the root of the problem: which is that the decorator I was using was not preserving the function signature of my slot.

    Simply changing my decorator to this fixes the problem:

    def decorator(f):
        def call(self):
            print('get decorated')
            return f(self)
        return wrapped
    

    However, this decorator isn't very reusable because of the hard-coded wrapper function name. The "correct" (only using standard libraries) way to fix this is by using the wraps decorator from functools:

    from functools import wraps
    def decorator(f):
        @wraps(f)
        def any_name_you_want(self):
            print('get decorated')
            return f(self)
        return any_name_you_want