diff --git a/.pydevproject b/.pydevproject index a946dfc2..f062ff5c 100644 --- a/.pydevproject +++ b/.pydevproject @@ -2,6 +2,7 @@ /${PROJECT_DIR_NAME} +/${PROJECT_DIR_NAME}/tests python 2.7 Default diff --git a/.travis.yml b/.travis.yml index 8d4205bb..aa1c0bbb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ env: - PYTEST_QT_API=pyqt4v2 PYQT_PACKAGE="pyqt=4.*" PYTHON_VERSION=3.4 - PYTEST_QT_API=pyside PYQT_PACKAGE="pyside=1.*" PYTHON_VERSION=3.4 - PYTEST_QT_API=pyqt5 PYQT_PACKAGE="pyqt=5.*" PYTHON_VERSION=3.4 + - PYTEST_QT_API=pyside2 PYQT_PACKAGE="pyside2=2.*" PYTHON_VERSION=3.5 install: - sudo apt-get update @@ -34,7 +35,7 @@ before_script: - sleep 1 script: - - source activate test && catchsegv coverage run --source=pytestqt -m pytest tests + - source activate test && catchsegv coverage run --source=pytestqt -m pytest -v tests # for some reason tox doesn't get installed with a u+x flag - | chmod u+x /home/travis/miniconda/envs/test/bin/tox diff --git a/README.rst b/README.rst index 39d12698..88b17a95 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ pytest-qt ========= pytest-qt is a `pytest`_ plugin that allows programmers to write tests -for `PySide`_ and `PyQt`_ applications. +for `PySide`_, `PySide2` and `PyQt`_ applications. The main usage is to use the `qtbot` fixture, responsible for handling `qApp` creation as needed and provides methods to simulate user interaction, @@ -23,6 +23,7 @@ like key presses and mouse clicks: .. _PySide: https://pypi.python.org/pypi/PySide +.. _PySide2: https://wiki.qt.io/PySide2 .. _PyQt: http://www.riverbankcomputing.com/software/pyqt .. _pytest: http://pytest.org @@ -72,16 +73,17 @@ Features Requirements ============ -Works with either PySide_ or PyQt_ (``PyQt5`` and ``PyQt4``) picking whichever +Works with either PySide_, PySide2_ or PyQt_ (``PyQt5`` and ``PyQt4``) picking whichever is available on the system, giving preference to the first one installed in this order: +- ``PySide2`` - ``PyQt5`` - ``PySide`` - ``PyQt4`` To force a particular API, set the configuration variable ``qt_api`` in your ``pytest.ini`` file to -``pyqt5``, ``pyside``, ``pyqt4`` or ``pyqt4v2``. ``pyqt4v2`` sets the ``PyQt4`` +``pyqt5``, ``pyside``, ``pyside2``, ``pyqt4`` or ``pyqt4v2``. ``pyqt4v2`` sets the ``PyQt4`` API to `version 2`_. .. code-block:: ini diff --git a/appveyor.yml b/appveyor.yml index 44a69469..cd86073e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,6 +33,9 @@ environment: - PYTHON_VERSION: "3.4" PYTEST_QT_API: "pyqt5" CONDA_DEPENDENCIES: "pytest pyqt=5.*" + - PYTHON_VERSION: "3.5" + PYTEST_QT_API: "pyside2" + CONDA_DEPENDENCIES: "pytest pyside2=2.*" platform: -x64 @@ -57,4 +60,4 @@ install: build: false test_script: - - "%CMD_IN_ENV% python -m pytest tests/" + - "%CMD_IN_ENV% python -m pytest -v tests/" diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 796ebaaf..0cc003e3 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -1,6 +1,6 @@ """ Provide a common way to import Qt classes used by pytest-qt in a unique manner, -abstracting API differences between PyQt4, PyQt5 and PySide. +abstracting API differences between PyQt4, PyQt5, PySide and PySide2. .. note:: This module is not part of pytest-qt public API, hence its interface may change between releases and users should not rely on it. @@ -8,8 +8,7 @@ Based on from https://github.com/epage/PythonUtils. """ -from __future__ import with_statement -from __future__ import division +from __future__ import with_statement, division from collections import namedtuple import os @@ -29,7 +28,7 @@ def _get_qt_api_from_env(self): api = os.environ.get('PYTEST_QT_API') if api is not None: api = api.lower() - if api not in ('pyside', 'pyqt4', 'pyqt4v2', 'pyqt5'): # pragma: no cover + if api not in ('pyside', 'pyside2', 'pyqt4', 'pyqt4v2', 'pyqt5'): # pragma: no cover msg = 'Invalid value for $PYTEST_QT_API: %s' raise RuntimeError(msg % api) return api @@ -42,22 +41,27 @@ def _can_import(name): except ImportError: return False - if _can_import('PyQt5'): + # Note, not importing only the root namespace because when uninstalling from conda, + # the namespace can still be there. + if _can_import('PySide2.QtCore'): + return 'pyside2' + elif _can_import('PyQt5.QtCore'): return 'pyqt5' - elif _can_import('PySide'): + elif _can_import('PySide.QtCore'): return 'pyside' - elif _can_import('PyQt4'): + elif _can_import('PyQt4.QtCore'): return 'pyqt4' return None def set_qt_api(self, api): self.pytest_qt_api = api or self._get_qt_api_from_env() or self._guess_qt_api() if not self.pytest_qt_api: # pragma: no cover - msg = 'pytest-qt requires either PySide, PyQt4 or PyQt5 to be installed' + msg = 'pytest-qt requires either PySide, PySide2, PyQt4 or PyQt5 to be installed' raise RuntimeError(msg) _root_modules = { 'pyside': 'PySide', + 'pyside2': 'PySide2', 'pyqt4': 'PyQt4', 'pyqt4v2': 'PyQt4', 'pyqt5': 'PyQt5', @@ -100,21 +104,33 @@ def _import_module(module_name): self.qInstallMsgHandler = None self.qInstallMessageHandler = None - if self.pytest_qt_api == 'pyside': + if self.pytest_qt_api.startswith('pyside'): self.Signal = QtCore.Signal self.Slot = QtCore.Slot self.Property = QtCore.Property - self.QApplication = QtGui.QApplication - self.QWidget = QtGui.QWidget self.QStringListModel = QtGui.QStringListModel - self.qInstallMsgHandler = QtCore.qInstallMsgHandler self.QStandardItem = QtGui.QStandardItem self.QStandardItemModel = QtGui.QStandardItemModel - self.QStringListModel = QtGui.QStringListModel - self.QSortFilterProxyModel = QtGui.QSortFilterProxyModel self.QAbstractListModel = QtCore.QAbstractListModel self.QAbstractTableModel = QtCore.QAbstractTableModel + self.QStringListModel = QtGui.QStringListModel + + if self.pytest_qt_api == 'pyside2': + _QtWidgets = _import_module('QtWidgets') + self.QApplication = _QtWidgets.QApplication + self.QWidget = _QtWidgets.QWidget + self.QLineEdit = _QtWidgets.QLineEdit + self.qInstallMessageHandler = QtCore.qInstallMessageHandler + + self.QSortFilterProxyModel = QtCore.QSortFilterProxyModel + else: + self.QApplication = QtGui.QApplication + self.QWidget = QtGui.QWidget + self.QLineEdit = QtGui.QLineEdit + self.qInstallMsgHandler = QtCore.qInstallMsgHandler + + self.QSortFilterProxyModel = QtGui.QSortFilterProxyModel def extract_from_variant(variant): """PySide does not expose QVariant API""" @@ -180,9 +196,16 @@ def make_variant(value=None): self.make_variant = make_variant def get_versions(self): - if self.pytest_qt_api == 'pyside': - import PySide - return VersionTuple('PySide', PySide.__version__, self.QtCore.qVersion(), + if self.pytest_qt_api in ('pyside', 'pyside2'): + qt_api_name = 'PySide2' if self.pytest_qt_api == 'pyside2' else 'PySide' + if self.pytest_qt_api == 'pyside2': + import PySide2 + version = PySide2.__version__ + else: + import PySide + version = PySide.__version__ + + return VersionTuple(qt_api_name, version, self.QtCore.qVersion(), self.QtCore.__version__) else: qt_api_name = 'PyQt5' if self.pytest_qt_api == 'pyqt5' else 'PyQt4' diff --git a/pytestqt/qtbot.py b/pytestqt/qtbot.py index 67e8ae6f..7333f93f 100644 --- a/pytestqt/qtbot.py +++ b/pytestqt/qtbot.py @@ -218,7 +218,7 @@ def waitForWindowShown(self, widget): .. note:: This method is also available as ``wait_for_window_shown`` (pep-8 alias) """ - if qt_api.pytest_qt_api == 'pyqt5': + if hasattr(qt_api.QtTest.QTest, 'qWaitForWindowExposed'): return qt_api.QtTest.QTest.qWaitForWindowExposed(widget) else: return qt_api.QtTest.QTest.qWaitForWindowShown(widget) diff --git a/tests/test_basics.py b/tests/test_basics.py index fddac5c3..e0af5613 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -324,7 +324,7 @@ def test_parse_ini_boolean_invalid(): pytestqt.qtbot._parse_ini_boolean('foo') -@pytest.mark.parametrize('option_api', ['pyqt4', 'pyqt5', 'pyside']) +@pytest.mark.parametrize('option_api', ['pyqt4', 'pyqt5', 'pyside', 'pyside2']) def test_qt_api_ini_config(testdir, option_api): """ Test qt_api ini option handling. @@ -344,7 +344,7 @@ def test_foo(qtbot): ''') result = testdir.runpytest_subprocess() - if qt_api.pytest_qt_api.startswith(option_api): # handle pyqt4v2 + if qt_api.pytest_qt_api.replace('v2', '') == option_api: # handle pyqt4v2 result.stdout.fnmatch_lines([ '* 1 passed in *' ]) diff --git a/tests/test_logging.py b/tests/test_logging.py index 2f528e51..dfd97e2a 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -4,6 +4,8 @@ from pytestqt.qt_compat import qt_api +pytestmark = pytest.mark.skipif(qt_api.pytest_qt_api == 'pyside2', reason="https://bugreports.qt.io/browse/PYSIDE-435") + @pytest.mark.parametrize('test_succeeds', [True, False]) @pytest.mark.parametrize('qt_log', [True, False]) diff --git a/tests/test_qtest_proxies.py b/tests/test_qtest_proxies.py index 3f531496..715f20fb 100644 --- a/tests/test_qtest_proxies.py +++ b/tests/test_qtest_proxies.py @@ -4,7 +4,7 @@ from pytestqt.qt_compat import qt_api -fails_on_pyqt = pytest.mark.xfail('qt_api.pytest_qt_api != "pyside"') +fails_on_pyqt = pytest.mark.xfail('not qt_api.pytest_qt_api.startswith("pyside")') @pytest.mark.parametrize('expected_method', [ @@ -29,4 +29,4 @@ def test_expected_qtest_proxies(qtbot, expected_method): Ensure that we are exporting expected QTest API methods. """ assert hasattr(qtbot, expected_method) - assert getattr(qtbot, expected_method).__name__ == expected_method \ No newline at end of file + assert getattr(qtbot, expected_method).__name__ == expected_method diff --git a/tests/test_wait_signal.py b/tests/test_wait_signal.py index c0a69602..7cde471a 100644 --- a/tests/test_wait_signal.py +++ b/tests/test_wait_signal.py @@ -293,8 +293,8 @@ def test_destroyed(qtbot): For some reason, this crashes PySide although it seems perfectly fine code. """ - if qt_api.pytest_qt_api == 'pyside': - pytest.skip('test crashes PySide') + if qt_api.pytest_qt_api.startswith('pyside'): + pytest.skip('test crashes PySide and PySide2') import sip @@ -708,7 +708,7 @@ def get_mixed_signals_with_guaranteed_name(signaller): Returns a list of signals with the guarantee that the signals have names (i.e. the names are manually provided in case of using PySide, where the signal names cannot be determined at run-time). """ - if qt_api.pytest_qt_api == 'pyside': + if qt_api.pytest_qt_api.startswith('pyside'): signals = [(signaller.signal, "signal()"), (signaller.signal_args, "signal_args(QString,int)"), (signaller.signal_args, "signal_args(QString,int)")] else: @@ -790,7 +790,7 @@ def test_without_callback_and_args(self, qtbot, signaller): In a situation where a signal without args is expected but not emitted, tests that the TimeoutError message contains the name of the signal (without arguments). """ - if qt_api.pytest_qt_api == 'pyside': + if qt_api.pytest_qt_api.startswith('pyside'): signal = (signaller.signal, "signal()") else: signal = signaller.signal @@ -811,7 +811,7 @@ def test_unable_to_get_callback_name(self, qtbot, signaller): if sys.version_info >= (3,5): pytest.skip("Only on Python 3.4 and lower double-wrapped functools.partial callbacks are a problem") - if qt_api.pytest_qt_api == 'pyside': + if qt_api.pytest_qt_api.startswith('pyside'): signal = (signaller.signal_single_arg, "signal_single_arg(int)") else: signal = signaller.signal_single_arg @@ -836,7 +836,7 @@ def test_with_single_arg(self, qtbot, signaller): rejected by a callback, tests that the TimeoutError message contains the name of the signal and the list of non-accepted arguments. """ - if qt_api.pytest_qt_api == 'pyside': + if qt_api.pytest_qt_api.startswith('pyside'): signal = (signaller.signal_single_arg, "signal_single_arg(int)") else: signal = signaller.signal_single_arg @@ -858,7 +858,7 @@ def test_with_multiple_args(self, qtbot, signaller): rejected by a callback, tests that the TimeoutError message contains the name of the signal and the list of tuples of the non-accepted arguments. """ - if qt_api.pytest_qt_api == 'pyside': + if qt_api.pytest_qt_api.startswith('pyside'): signal = (signaller.signal_args, "signal_args(QString,int)") else: signal = signaller.signal_args @@ -999,7 +999,7 @@ def test_degenerate_error_msg(self, qtbot, signaller): by the user. This degenerate messages doesn't contain the signals' names, and includes a hint to the user how to fix the situation. """ - if qt_api.pytest_qt_api != 'pyside': + if not qt_api.pytest_qt_api.startswith('pyside'): pytest.skip("test only makes sense for PySide, whose signals don't contain a name!") with pytest.raises(TimeoutError) as excinfo: