Skip to content

Commit fb3a810

Browse files
Fabio Zadroznyrth
Fabio Zadrozny
authored andcommitted
Adding support for PySide2 in pytest-qt.
1 parent aa70124 commit fb3a810

File tree

6 files changed

+77
-35
lines changed

6 files changed

+77
-35
lines changed

.pydevproject

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<?eclipse-pydev version="1.0"?><pydev_project>
33
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
44
<path>/${PROJECT_DIR_NAME}</path>
5+
<path>/${PROJECT_DIR_NAME}/tests</path>
56
</pydev_pathproperty>
67
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
78
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>

pytestqt/qt_compat.py

+40-15
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
"""
22
Provide a common way to import Qt classes used by pytest-qt in a unique manner,
3-
abstracting API differences between PyQt4, PyQt5 and PySide.
3+
abstracting API differences between PyQt4, PyQt5, PySide and PySide2.
44
55
.. note:: This module is not part of pytest-qt public API, hence its interface
66
may change between releases and users should not rely on it.
77
88
Based on from https://github.com/epage/PythonUtils.
99
"""
1010

11-
from __future__ import with_statement
12-
from __future__ import division
11+
from __future__ import with_statement, division
1312
from collections import namedtuple
1413
import os
1514

@@ -29,7 +28,7 @@ def _get_qt_api_from_env(self):
2928
api = os.environ.get('PYTEST_QT_API')
3029
if api is not None:
3130
api = api.lower()
32-
if api not in ('pyside', 'pyqt4', 'pyqt4v2', 'pyqt5'): # pragma: no cover
31+
if api not in ('pyside', 'pyside2', 'pyqt4', 'pyqt4v2', 'pyqt5'): # pragma: no cover
3332
msg = 'Invalid value for $PYTEST_QT_API: %s'
3433
raise RuntimeError(msg % api)
3534
return api
@@ -42,22 +41,27 @@ def _can_import(name):
4241
except ImportError:
4342
return False
4443

45-
if _can_import('PyQt5'):
44+
# Note, not importing only the root namespace because when uninstalling from conda,
45+
# the namespace can still be there.
46+
if _can_import('PyQt5.QtCore'):
4647
return 'pyqt5'
47-
elif _can_import('PySide'):
48+
elif _can_import('PySide.QtCore'):
4849
return 'pyside'
49-
elif _can_import('PyQt4'):
50+
elif _can_import('PySide2.QtCore'):
51+
return 'pyside2'
52+
elif _can_import('PyQt4.QtCore'):
5053
return 'pyqt4'
5154
return None
5255

5356
def set_qt_api(self, api):
5457
self.pytest_qt_api = api or self._get_qt_api_from_env() or self._guess_qt_api()
5558
if not self.pytest_qt_api: # pragma: no cover
56-
msg = 'pytest-qt requires either PySide, PyQt4 or PyQt5 to be installed'
59+
msg = 'pytest-qt requires either PySide, PySide2, PyQt4 or PyQt5 to be installed'
5760
raise RuntimeError(msg)
5861

5962
_root_modules = {
6063
'pyside': 'PySide',
64+
'pyside2': 'PySide2',
6165
'pyqt4': 'PyQt4',
6266
'pyqt4v2': 'PyQt4',
6367
'pyqt5': 'PyQt5',
@@ -100,21 +104,35 @@ def _import_module(module_name):
100104
self.qInstallMsgHandler = None
101105
self.qInstallMessageHandler = None
102106

103-
if self.pytest_qt_api == 'pyside':
107+
if self.pytest_qt_api in ('pyside', 'pyside2'):
104108
self.Signal = QtCore.Signal
105109
self.Slot = QtCore.Slot
106110
self.Property = QtCore.Property
107111
self.QApplication = QtGui.QApplication
108112
self.QWidget = QtGui.QWidget
109113
self.QStringListModel = QtGui.QStringListModel
110-
self.qInstallMsgHandler = QtCore.qInstallMsgHandler
111114

112115
self.QStandardItem = QtGui.QStandardItem
113116
self.QStandardItemModel = QtGui.QStandardItemModel
114-
self.QStringListModel = QtGui.QStringListModel
115-
self.QSortFilterProxyModel = QtGui.QSortFilterProxyModel
116117
self.QAbstractListModel = QtCore.QAbstractListModel
117118
self.QAbstractTableModel = QtCore.QAbstractTableModel
119+
self.QStringListModel = QtGui.QStringListModel
120+
121+
if self.pytest_qt_api == 'pyside2':
122+
_QtWidgets = _import_module('QtWidgets')
123+
self.QApplication = _QtWidgets.QApplication
124+
self.QWidget = _QtWidgets.QWidget
125+
self.QLineEdit = _QtWidgets.QLineEdit
126+
self.qInstallMessageHandler = QtCore.qInstallMessageHandler
127+
128+
self.QSortFilterProxyModel = QtCore.QSortFilterProxyModel
129+
else:
130+
self.QApplication = QtGui.QApplication
131+
self.QWidget = QtGui.QWidget
132+
self.QLineEdit = QtGui.QLineEdit
133+
self.qInstallMsgHandler = QtCore.qInstallMsgHandler
134+
135+
self.QSortFilterProxyModel = QtGui.QSortFilterProxyModel
118136

119137
def extract_from_variant(variant):
120138
"""PySide does not expose QVariant API"""
@@ -180,9 +198,16 @@ def make_variant(value=None):
180198
self.make_variant = make_variant
181199

182200
def get_versions(self):
183-
if self.pytest_qt_api == 'pyside':
184-
import PySide
185-
return VersionTuple('PySide', PySide.__version__, self.QtCore.qVersion(),
201+
if self.pytest_qt_api in ('pyside', 'pyside2'):
202+
qt_api_name = 'PySide2' if self.pytest_qt_api == 'pyside2' else 'PySide'
203+
if self.pytest_qt_api == 'pyside2':
204+
import PySide2
205+
version = PySide2.__version__
206+
else:
207+
import PySide
208+
version = PySide.__version__
209+
210+
return VersionTuple(qt_api_name, version, self.QtCore.qVersion(),
186211
self.QtCore.__version__)
187212
else:
188213
qt_api_name = 'PyQt5' if self.pytest_qt_api == 'pyqt5' else 'PyQt4'

pytestqt/qtbot.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def waitForWindowShown(self, widget):
218218
219219
.. note:: This method is also available as ``wait_for_window_shown`` (pep-8 alias)
220220
"""
221-
if qt_api.pytest_qt_api == 'pyqt5':
221+
if hasattr(qt_api.QtTest.QTest, 'qWaitForWindowExposed'):
222222
return qt_api.QtTest.QTest.qWaitForWindowExposed(widget)
223223
else:
224224
return qt_api.QtTest.QTest.qWaitForWindowShown(widget)

tests/test_logging.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from pytestqt.qt_compat import qt_api
66

7+
pytestmark = pytest.mark.skipif(qt_api.pytest_qt_api == 'pyside2', reason="https://bugreports.qt.io/browse/PYSIDE-435")
8+
79

810
@pytest.mark.parametrize('test_succeeds', [True, False])
911
@pytest.mark.parametrize('qt_log', [True, False])

tests/test_qtest_proxies.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pytestqt.qt_compat import qt_api
55

66

7-
fails_on_pyqt = pytest.mark.xfail('qt_api.pytest_qt_api != "pyside"')
7+
fails_on_pyqt = pytest.mark.xfail('qt_api.pytest_qt_api not in ("pyside", "pyside2")')
88

99

1010
@pytest.mark.parametrize('expected_method', [
@@ -29,4 +29,4 @@ def test_expected_qtest_proxies(qtbot, expected_method):
2929
Ensure that we are exporting expected QTest API methods.
3030
"""
3131
assert hasattr(qtbot, expected_method)
32-
assert getattr(qtbot, expected_method).__name__ == expected_method
32+
assert getattr(qtbot, expected_method).__name__ == expected_method

tests/test_wait_signal.py

+31-17
Original file line numberDiff line numberDiff line change
@@ -290,22 +290,36 @@ def test_wait_signals_invalid_strict_parameter(qtbot, signaller):
290290

291291
def test_destroyed(qtbot):
292292
"""Test that waitSignal works with the destroyed signal (#82).
293-
294-
For some reason, this crashes PySide although it seems perfectly fine code.
295293
"""
296-
if qt_api.pytest_qt_api == 'pyside':
297-
pytest.skip('test crashes PySide')
294+
if qt_api.pytest_qt_api.startswith('pyside'):
295+
# PySide uses shiboken instead of sip.
296+
if qt_api.pytest_qt_api == 'pyside':
297+
try:
298+
from PySide import shiboken
299+
except ImportError:
300+
import shiboken
301+
else:
302+
from PySide2 import shiboken2 as shiboken
298303

299-
import sip
304+
class Obj(qt_api.QtCore.QObject):
305+
pass
300306

301-
class Obj(qt_api.QtCore.QObject):
302-
pass
307+
obj = Obj()
308+
with qtbot.waitSignal(obj.destroyed):
309+
obj.deleteLater()
310+
311+
assert not shiboken.isValid(obj)
312+
else:
313+
import sip
303314

304-
obj = Obj()
305-
with qtbot.waitSignal(obj.destroyed):
306-
obj.deleteLater()
315+
class Obj(qt_api.QtCore.QObject):
316+
pass
317+
318+
obj = Obj()
319+
with qtbot.waitSignal(obj.destroyed):
320+
obj.deleteLater()
307321

308-
assert sip.isdeleted(obj)
322+
assert sip.isdeleted(obj)
309323

310324

311325
class TestArgs:
@@ -708,7 +722,7 @@ def get_mixed_signals_with_guaranteed_name(signaller):
708722
Returns a list of signals with the guarantee that the signals have names (i.e. the names are
709723
manually provided in case of using PySide, where the signal names cannot be determined at run-time).
710724
"""
711-
if qt_api.pytest_qt_api == 'pyside':
725+
if qt_api.pytest_qt_api.startswith('pyside'):
712726
signals = [(signaller.signal, "signal()"), (signaller.signal_args, "signal_args(QString,int)"),
713727
(signaller.signal_args, "signal_args(QString,int)")]
714728
else:
@@ -790,7 +804,7 @@ def test_without_callback_and_args(self, qtbot, signaller):
790804
In a situation where a signal without args is expected but not emitted, tests that the TimeoutError
791805
message contains the name of the signal (without arguments).
792806
"""
793-
if qt_api.pytest_qt_api == 'pyside':
807+
if qt_api.pytest_qt_api.startswith('pyside'):
794808
signal = (signaller.signal, "signal()")
795809
else:
796810
signal = signaller.signal
@@ -811,7 +825,7 @@ def test_unable_to_get_callback_name(self, qtbot, signaller):
811825
if sys.version_info >= (3,5):
812826
pytest.skip("Only on Python 3.4 and lower double-wrapped functools.partial callbacks are a problem")
813827

814-
if qt_api.pytest_qt_api == 'pyside':
828+
if qt_api.pytest_qt_api.startswith('pyside'):
815829
signal = (signaller.signal_single_arg, "signal_single_arg(int)")
816830
else:
817831
signal = signaller.signal_single_arg
@@ -836,7 +850,7 @@ def test_with_single_arg(self, qtbot, signaller):
836850
rejected by a callback, tests that the TimeoutError message contains the name of the signal and the
837851
list of non-accepted arguments.
838852
"""
839-
if qt_api.pytest_qt_api == 'pyside':
853+
if qt_api.pytest_qt_api.startswith('pyside'):
840854
signal = (signaller.signal_single_arg, "signal_single_arg(int)")
841855
else:
842856
signal = signaller.signal_single_arg
@@ -858,7 +872,7 @@ def test_with_multiple_args(self, qtbot, signaller):
858872
rejected by a callback, tests that the TimeoutError message contains the name of the signal and the
859873
list of tuples of the non-accepted arguments.
860874
"""
861-
if qt_api.pytest_qt_api == 'pyside':
875+
if qt_api.pytest_qt_api.startswith('pyside'):
862876
signal = (signaller.signal_args, "signal_args(QString,int)")
863877
else:
864878
signal = signaller.signal_args
@@ -999,7 +1013,7 @@ def test_degenerate_error_msg(self, qtbot, signaller):
9991013
by the user. This degenerate messages doesn't contain the signals' names, and includes a hint to the user how
10001014
to fix the situation.
10011015
"""
1002-
if qt_api.pytest_qt_api != 'pyside':
1016+
if qt_api.pytest_qt_api not in ('pyside', 'pyside2'):
10031017
pytest.skip("test only makes sense for PySide, whose signals don't contain a name!")
10041018

10051019
with pytest.raises(TimeoutError) as excinfo:

0 commit comments

Comments
 (0)