Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding inconsistent cythonized code behavior - PyQt5 vs. PySide2

While cythonizing some PyQt5 code, I was encountering TypeError: method() takes exactly 1 positional argument (2 given). Strangely, replacing PyQt5 with PySide2 seems to not cause this behavior. I was hoping someone could help me understand why this is happening. NOTE: running directly from source does not cause this problem for either PyQt5 or PySide2.

I am using Python 3.6.8, cython 0.28.5.

I created a sample application to reproduce this behavior. The folder structure is as follows:

root/
|- main.py
|- setup.py
|- lib/
    |- __init__.py
    |- test.py

setup.py is performing the same function as $ cythonize -i <filename> while allowing me to change the compiler_directives. The actual code can be found in the cython repo here.

setup.py

import os
import tempfile
import shutil
from distutils.core import setup
from Cython.Build.Dependencies import cythonize
from multiprocessing import pool

def run_distutils(args):
    base_dir, ext_modules = args
    script_args = ['build_ext', '-i']
    cwd = os.getcwd()
    temp_dir = None
    try:
        if base_dir:
            os.chdir(base_dir)
            temp_dir = tempfile.mkdtemp(dir=base_dir)
            script_args.extend(['--build-temp', temp_dir])
            setup(
                    script_name='setup.py',
                    script_args=script_args,
                    ext_modules=ext_modules,
                )
    finally:
        if base_dir:
            os.chdir(cwd)
            if temp_dir and os.path.isdir(temp_dir):
                shutil.rmtree(temp_dir)

if __name__ == "__main__":
    ext_paths = ['lib\\test.py']
    cython_exts = cythonize(ext_paths,
                            nthreads=1,
                            compiler_directives={
                                "always_allow_keywords": True,
                            })
    try:
        process_pool = pool.Pool()
        process_pool.map_async(run_distutils, [(".", [ext]) for ext in cython_exts])
    except:
        if process_pool is not None:
            process_pool.terminate()
        raise
    finally:
        if process_pool is not None:
            process_pool.close()
            process_pool.join()

main.py is used to call the main inside test.py which initiates the UI.

test.py

import sys
from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication

def print_arg(arg):
    print(arg)

class Example(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.btn1 = QPushButton("Button 1", self)
        self.btn1.move(30, 50)
        self.btn2 = QPushButton("Button 2", self)
        self.btn2.move(150, 50)
        self.btn1.clicked.connect(self.buttonClicked)
        self.btn2.clicked.connect(self.buttonClicked)
        self.statusBar()
        self.setGeometry(300, 300, 290, 150)
        self.setWindowTitle('Event sender')
        self.show()

    def buttonClicked(self):
        sender = self.sender()
        self.statusBar().showMessage(sender.text() + ' was pressed')
        print_arg(arg=self.sender())

def main():
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

I create .pyd from test.pyd by executing $ python setup.py from the root directory. Once the build has completed, I move test.py outside of lib/ for testing using $ python main.py.

When building and running test.py as shown above (using PyQt5), clicking on any button will cause:

Traceback (most recent call last):
  File "lib\test.py", line 26, in lib.test.Example.buttonClicked
    def buttonClicked(self):
TypeError: buttonClicked() takes exactly 1 positional argument (2 given)

Replacing PyQt5 with PySide2 in test.py, building and then running the code, the same TypeError is not raised. This is the behavior I want to investigate.

In setup.py, changing the compiler directive, always_allow_keywords to False, will stop the TypeError from happening but will cause this error to be raised (this happens for both PyQt5 and PySide):

Traceback (most recent call last):
  File "lib\test.py", line 29, in lib.test.Example.buttonClicked
    print_arg(arg=self.sender())
TypeError: print_arg() takes no keyword arguments

It would be great if someone could shed some light on why is the behavior different for PyQt5 and PySide2.

Thanks.

like image 276
Soham Patel Avatar asked Apr 18 '19 01:04

Soham Patel


People also ask

Should I use PyQt5 or PySide2?

Which should you use? Well, honestly, it doesn't really matter. Both packages are wrapping the same library — Qt5 — and so have 99.9% identical APIs (see below for the few differences). Code that is written for one can often be used as-is with other, simply changing the imports from PyQt5 to PySide2 .

What is PySide2 used for?

PySide2 is the official Python module from the Qt for Python project, which provides access to the complete Qt 5.12+ framework. The Qt for Python project is developed in the open, with all facilities you'd expect from any modern OSS project such as all code in a git repository and an open design process.


1 Answers

The clicked signal is overloaded, that is, it has 2 signatures: clicked = pyqtSignal([], [bool]) so by not indicating which signature will be used generates this type of problems. So the solution is to indicate the signature by pyqtSlot:

import sys
from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication
from PyQt5.QtCore import pyqtSlot # <--- add this line

def print_arg(arg):
    print(arg)

class Example(QMainWindow):
    # ...

    @pyqtSlot() # <--- add this line
    def buttonClicked(self):
        sender = self.sender()
        self.statusBar().showMessage(sender.text() + ' was pressed')
        print_arg(arg=self.sender())

def main():
    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

And in the case of PySide2, the signature is deducted, but PyQt5 expects you to clearly indicate it, otherwise it will check with all possibles cases.

like image 119
eyllanesc Avatar answered Sep 29 '22 21:09

eyllanesc