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.
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 .
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With