diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 370b7646..a8f11d01 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,9 @@ However, this required some backwards-incompatible changes: on timeouts. You can set ``qt_wait_signal_raising = false`` in your config to get back the old behaviour. +- ``PYTEST_QT_FORCE_PYQT`` environment variable is no longer supported. Set ``PYTEST_QT_API`` + to the appropriate value instead. + New Features ~~~~~~~~~~~~ @@ -41,9 +44,18 @@ New Features evaluate if the arguments of emitted signals should resume execution or not. Thanks `@MShekow`_ for the PR (`#141`_). +* Now which Qt binding ``pytest-qt`` will use can be configured by the ``qt_api`` config option. + Thanks `@The-Compiler`_ for the request (`#129`_). + +* While ``pytestqt.qt_compat`` is an internal module and shouldn't be imported directly, + it is known that some test suites did import it. This module now uses a lazy-load mechanism + to load Qt classes and objects, so the old symbols (``QtCore``, ``QApplication``, etc.) are + no longer available from it. + .. _#134: https://github.com/pytest-dev/pytest-qt/issues/134 .. _#141: https://github.com/pytest-dev/pytest-qt/pull/141 .. _#63: https://github.com/pytest-dev/pytest-qt/pull/63 +.. _#129: https://github.com/pytest-dev/pytest-qt/issues/129 Other Changes diff --git a/README.rst b/README.rst index 76705383..6b968eca 100644 --- a/README.rst +++ b/README.rst @@ -80,9 +80,20 @@ this order: - ``PySide`` - ``PyQt4`` -To force a particular API, set the environment variable ``PYTEST_QT_API`` to -``pyqt5``, ``pyside``, ``pyqt4``, or ``pyqt4v2``. ``pyqt4v2`` sets the ``PyQt4`` -API to `version 2 `_. +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`` +API to `version 2 `_. + +.. code-block:: ini + [pytest] + qt_api=pyqt5 + + +Alternatively, you can set the ``PYTEST_QT_API`` environment +variable to the same values described above (the environment variable wins over the configuration +if both are set). + +.. _version2: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html Documentation diff --git a/docs/intro.rst b/docs/intro.rst index a1ccc207..63d8b8e7 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -38,9 +38,19 @@ this order: - ``PySide`` - ``PyQt4`` -To force a particular API, set the environment variable ``PYTEST_QT_API`` to +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`` -API to `version 2 `_ +API to `version 2 `_. + +.. code-block:: ini + + [pytest] + qt_api=pyqt5 + + +Alternatively, you can set the ``PYTEST_QT_API`` environment +variable to the same values described above (the environment variable wins over the configuration +if both are set). .. _version2: http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html diff --git a/pytestqt/logging.py b/pytestqt/logging.py index acbd0ff8..6f0b5df4 100644 --- a/pytestqt/logging.py +++ b/pytestqt/logging.py @@ -4,8 +4,7 @@ import re from py._code.code import TerminalRepr, ReprFileLocation import pytest -from pytestqt.qt_compat import qInstallMsgHandler, qInstallMessageHandler, \ - QtDebugMsg, QtWarningMsg, QtCriticalMsg, QtFatalMsg +from pytestqt.qt_compat import qt_api class QtLoggingPlugin(object): @@ -71,6 +70,12 @@ def pytest_runtest_makereport(self, item, call): long_repr = getattr(report, 'longrepr', None) if hasattr(long_repr, 'addsection'): # pragma: no cover log_format = self.config.getoption('qt_log_format') + if log_format is None: + if qt_api.pytest_qt_api == 'pyqt5': + log_format = '{rec.context.file}:{rec.context.function}:' \ + '{rec.context.line}:\n {rec.type_name}: {rec.message}' + else: + log_format = '{rec.type_name}: {rec.message}' lines = [] for rec in item.qt_log_capture.records: suffix = ' (IGNORED)' if rec.ignored else '' @@ -103,11 +108,11 @@ def _start(self): """ Start receiving messages from Qt. """ - if qInstallMsgHandler: - previous_handler = qInstallMsgHandler(self._handle_no_context) + if qt_api.qInstallMsgHandler: + previous_handler = qt_api.qInstallMsgHandler(self._handle_no_context) else: - assert qInstallMessageHandler - previous_handler = qInstallMessageHandler(self._handle_with_context) + assert qt_api.qInstallMessageHandler + previous_handler = qt_api.qInstallMessageHandler(self._handle_with_context) self._previous_handler = previous_handler def _stop(self): @@ -115,11 +120,11 @@ def _stop(self): Stop receiving messages from Qt, restoring the previously installed handler. """ - if qInstallMsgHandler: - qInstallMsgHandler(self._previous_handler) + if qt_api.qInstallMsgHandler: + qt_api.qInstallMsgHandler(self._previous_handler) else: - assert qInstallMessageHandler - qInstallMessageHandler(self._previous_handler) + assert qt_api.qInstallMessageHandler + qt_api.qInstallMessageHandler(self._previous_handler) @contextmanager def disabled(self): @@ -230,10 +235,10 @@ def _get_msg_type_name(cls, msg_type): """ if not getattr(cls, '_type_name_map', None): cls._type_name_map = { - QtDebugMsg: 'QtDebugMsg', - QtWarningMsg: 'QtWarningMsg', - QtCriticalMsg: 'QtCriticalMsg', - QtFatalMsg: 'QtFatalMsg', + qt_api.QtDebugMsg: 'QtDebugMsg', + qt_api.QtWarningMsg: 'QtWarningMsg', + qt_api.QtCriticalMsg: 'QtCriticalMsg', + qt_api.QtFatalMsg: 'QtFatalMsg', } return cls._type_name_map[msg_type] @@ -245,10 +250,10 @@ def _get_log_type_name(cls, msg_type): """ if not getattr(cls, '_log_type_name_map', None): cls._log_type_name_map = { - QtDebugMsg: 'DEBUG', - QtWarningMsg: 'WARNING', - QtCriticalMsg: 'CRITICAL', - QtFatalMsg: 'FATAL', + qt_api.QtDebugMsg: 'DEBUG', + qt_api.QtWarningMsg: 'WARNING', + qt_api.QtCriticalMsg: 'CRITICAL', + qt_api.QtFatalMsg: 'FATAL', } return cls._log_type_name_map[msg_type] diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 1fd2d7ec..27d93858 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -34,8 +34,7 @@ from __future__ import print_function import collections -from pytestqt.qt_compat import QtCore, QtGui, extract_from_variant, \ - QAbstractListModel, QAbstractTableModel +from pytestqt.qt_compat import qt_api _Changing = collections.namedtuple('_Changing', 'parent, old_size, last, next') @@ -65,10 +64,10 @@ def _modelindex_debug(self, index): if not index.isValid(): return ' (0x{:x})'.format(id(index)) else: - data = self._model.data(index, QtCore.Qt.DisplayRole) + data = self._model.data(index, qt_api.QtCore.Qt.DisplayRole) return '{}/{} {!r} (0x{:x})'.format( index.row(), index.column(), - extract_from_variant(data), + qt_api.extract_from_variant(data), id(index)) def check(self, model): @@ -166,31 +165,31 @@ def _test_basic(self): Make sure the model doesn't outright segfault, testing the functions which make sense. """ - assert self._model.buddy(QtCore.QModelIndex()) == QtCore.QModelIndex() - self._model.canFetchMore(QtCore.QModelIndex()) - assert self._column_count(QtCore.QModelIndex()) >= 0 - display_data = self._model.data(QtCore.QModelIndex(), - QtCore.Qt.DisplayRole) - - assert extract_from_variant(display_data) is None - self._fetch_more(QtCore.QModelIndex()) - flags = self._model.flags(QtCore.QModelIndex()) - assert flags == QtCore.Qt.ItemIsDropEnabled or not flags - self._has_children(QtCore.QModelIndex()) + assert self._model.buddy(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() + self._model.canFetchMore(qt_api.QtCore.QModelIndex()) + assert self._column_count(qt_api.QtCore.QModelIndex()) >= 0 + display_data = self._model.data(qt_api.QtCore.QModelIndex(), + qt_api.QtCore.Qt.DisplayRole) + + assert qt_api.extract_from_variant(display_data) is None + self._fetch_more(qt_api.QtCore.QModelIndex()) + flags = self._model.flags(qt_api.QtCore.QModelIndex()) + assert flags == qt_api.QtCore.Qt.ItemIsDropEnabled or not flags + self._has_children(qt_api.QtCore.QModelIndex()) self._model.hasIndex(0, 0) - self._model.headerData(0, QtCore.Qt.Horizontal) + self._model.headerData(0, qt_api.QtCore.Qt.Horizontal) self._model.index(0, 0) - self._model.itemData(QtCore.QModelIndex()) + self._model.itemData(qt_api.QtCore.QModelIndex()) cache = None - self._model.match(QtCore.QModelIndex(), -1, cache) + self._model.match(qt_api.QtCore.QModelIndex(), -1, cache) self._model.mimeTypes() - assert self._parent(QtCore.QModelIndex()) == QtCore.QModelIndex() + assert self._parent(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() assert self._model.rowCount() >= 0 - self._model.setData(QtCore.QModelIndex(), None, -1) - self._model.setHeaderData(-1, QtCore.Qt.Horizontal, None) - self._model.setHeaderData(999999, QtCore.Qt.Horizontal, None) - self._model.sibling(0, 0, QtCore.QModelIndex()) - self._model.span(QtCore.QModelIndex()) + self._model.setData(qt_api.QtCore.QModelIndex(), None, -1) + self._model.setHeaderData(-1, qt_api.QtCore.Qt.Horizontal, None) + self._model.setHeaderData(999999, qt_api.QtCore.Qt.Horizontal, None) + self._model.sibling(0, 0, qt_api.QtCore.QModelIndex()) + self._model.span(qt_api.QtCore.QModelIndex()) self._model.supportedDropActions() def _test_row_count(self): @@ -202,7 +201,7 @@ def _test_row_count(self): but this catches the big mistakes. """ # check top row - top_index = self._model.index(0, 0, QtCore.QModelIndex()) + top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) rows = self._model.rowCount(top_index) assert rows >= 0 if rows > 0: @@ -223,7 +222,7 @@ def _test_column_count(self): but this catches the big mistakes. """ # check top row - top_index = self._model.index(0, 0, QtCore.QModelIndex()) + top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) assert self._column_count(top_index) >= 0 # check a column count where parent is valid @@ -259,9 +258,9 @@ def _test_index(self): but this catches the big mistakes. """ # Make sure that invalid values return an invalid index - assert self._model.index(-2, -2) == QtCore.QModelIndex() - assert self._model.index(-2, 0) == QtCore.QModelIndex() - assert self._model.index(0, -2) == QtCore.QModelIndex() + assert self._model.index(-2, -2) == qt_api.QtCore.QModelIndex() + assert self._model.index(-2, 0) == qt_api.QtCore.QModelIndex() + assert self._model.index(0, -2) == qt_api.QtCore.QModelIndex() rows = self._model.rowCount() columns = self._column_count() @@ -270,7 +269,7 @@ def _test_index(self): return # Catch off by one errors - assert self._model.index(rows, columns) == QtCore.QModelIndex() + assert self._model.index(rows, columns) == qt_api.QtCore.QModelIndex() assert self._model.index(0, 0).isValid() # Make sure that the same index is *always* returned @@ -282,7 +281,7 @@ def _test_parent(self): """Tests model's implementation of QAbstractItemModel::parent().""" # Make sure the model won't crash and will return an invalid # QModelIndex when asked for the parent of an invalid index. - assert self._parent(QtCore.QModelIndex()) == QtCore.QModelIndex() + assert self._parent(qt_api.QtCore.QModelIndex()) == qt_api.QtCore.QModelIndex() if self._model.rowCount() == 0: return @@ -294,8 +293,8 @@ def _test_parent(self): # Common error test #1, make sure that a top level index has a parent # that is a invalid QModelIndex. - top_index = self._model.index(0, 0, QtCore.QModelIndex()) - assert self._parent(top_index) == QtCore.QModelIndex() + top_index = self._model.index(0, 0, qt_api.QtCore.QModelIndex()) + assert self._parent(top_index) == qt_api.QtCore.QModelIndex() # Common error test #2, make sure that a second level index has a # parent that is the first level index. @@ -306,7 +305,7 @@ def _test_parent(self): # Common error test #3, the second column should NOT have the same # children as the first column in a row. # Usually the second column shouldn't have children. - top_index_1 = self._model.index(0, 1, QtCore.QModelIndex()) + top_index_1 = self._model.index(0, 1, qt_api.QtCore.QModelIndex()) if self._model.rowCount(top_index_1) > 0: child_index = self._model.index(0, 0, top_index) child_index_1 = self._model.index(0, 0, top_index_1) @@ -314,7 +313,7 @@ def _test_parent(self): # Full test, walk n levels deep through the model making sure that all # parent's children correctly specify their parent. - self._check_children(QtCore.QModelIndex()) + self._check_children(qt_api.QtCore.QModelIndex()) def _check_children(self, parent, current_depth=0): """Check parent/children relationships. @@ -392,9 +391,9 @@ def _check_children(self, parent, current_depth=0): assert index.row() == r assert index.column() == c - data = self._model.data(index, QtCore.Qt.DisplayRole) + data = self._model.data(index, qt_api.QtCore.Qt.DisplayRole) if not self.data_display_may_return_none: - assert extract_from_variant(data) is not None + assert qt_api.extract_from_variant(data) is not None # If the next test fails here is some somewhat useful debug you # play with. @@ -429,8 +428,8 @@ def _check_children(self, parent, current_depth=0): def _test_data(self): """Test model's implementation of data()""" # Invalid index should return an invalid qvariant - value = self._model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole) - assert extract_from_variant(value) is None + value = self._model.data(qt_api.QtCore.QModelIndex(), qt_api.QtCore.Qt.DisplayRole) + assert qt_api.extract_from_variant(value) is None if self._model.rowCount() == 0: return @@ -439,18 +438,18 @@ def _test_data(self): assert self._model.index(0, 0).isValid() # shouldn't be able to set data on an invalid index - ok = self._model.setData(QtCore.QModelIndex(), "foo", - QtCore.Qt.DisplayRole) + ok = self._model.setData(qt_api.QtCore.QModelIndex(), "foo", + qt_api.QtCore.Qt.DisplayRole) assert not ok types = [ - (QtCore.Qt.ToolTipRole, str), - (QtCore.Qt.StatusTipRole, str), - (QtCore.Qt.WhatsThisRole, str), - (QtCore.Qt.SizeHintRole, QtCore.QSize), - (QtCore.Qt.FontRole, QtGui.QFont), - (QtCore.Qt.BackgroundColorRole, QtGui.QColor), - (QtCore.Qt.TextColorRole, QtGui.QColor), + (qt_api.QtCore.Qt.ToolTipRole, str), + (qt_api.QtCore.Qt.StatusTipRole, str), + (qt_api.QtCore.Qt.WhatsThisRole, str), + (qt_api.QtCore.Qt.SizeHintRole, qt_api.QtCore.QSize), + (qt_api.QtCore.Qt.FontRole, qt_api.QtGui.QFont), + (qt_api.QtCore.Qt.BackgroundColorRole, qt_api.QtGui.QColor), + (qt_api.QtCore.Qt.TextColorRole, qt_api.QtGui.QColor), ] # General purpose roles with a fixed expected type @@ -460,22 +459,22 @@ def _test_data(self): # Check that the alignment is one we know about alignment = self._model.data(self._model.index(0, 0), - QtCore.Qt.TextAlignmentRole) - alignment = extract_from_variant(alignment) + qt_api.QtCore.Qt.TextAlignmentRole) + alignment = qt_api.extract_from_variant(alignment) if alignment is not None: try: alignment = int(alignment) except (TypeError, ValueError): assert 0, '%r should be a TextAlignmentRole enum' % alignment - mask = int(QtCore.Qt.AlignHorizontal_Mask | - QtCore.Qt.AlignVertical_Mask) + mask = int(qt_api.QtCore.Qt.AlignHorizontal_Mask | + qt_api.QtCore.Qt.AlignVertical_Mask) assert alignment == alignment & mask # Check that the "check state" is one we know about. state = self._model.data(self._model.index(0, 0), - QtCore.Qt.CheckStateRole) - assert state in [None, QtCore.Qt.Unchecked, QtCore.Qt.PartiallyChecked, - QtCore.Qt.Checked] + qt_api.QtCore.Qt.CheckStateRole) + assert state in [None, qt_api.QtCore.Qt.Unchecked, qt_api.QtCore.Qt.PartiallyChecked, + qt_api.QtCore.Qt.Checked] def _on_rows_about_to_be_inserted(self, parent, start, end): """Store what is about to be inserted. @@ -516,8 +515,8 @@ def _on_rows_inserted(self, parent, start, end): "next data {!r}, last data {!r}".format( self._modelindex_debug(c.parent), c.old_size, expected_size, - extract_from_variant(c.next), - extract_from_variant(c.last) + qt_api.extract_from_variant(c.next), + qt_api.extract_from_variant(c.last) ) ) @@ -525,12 +524,12 @@ def _on_rows_inserted(self, parent, start, end): "next data {!r}, last data {!r}".format( self._modelindex_debug(parent), current_size, - extract_from_variant(next_data), - extract_from_variant(last_data) + qt_api.extract_from_variant(next_data), + qt_api.extract_from_variant(last_data) ) ) - if not QtCore.qVersion().startswith('4.'): + if not qt_api.QtCore.qVersion().startswith('4.'): # Skipping this on Qt4 as the parent changes for some reason: # modeltest: rows about to be inserted: [...] # parent (0x7f8f540eacf8), [...] @@ -553,7 +552,7 @@ def _on_rows_inserted(self, parent, start, end): def _on_layout_about_to_be_changed(self): for i in range(max(self._model.rowCount(), 100)): - idx = QtCore.QPersistentModelIndex(self._model.index(i, 0)) + idx = qt_api.QtCore.QPersistentModelIndex(self._model.index(i, 0)) self._changing.append(idx) def _on_layout_changed(self): @@ -600,8 +599,8 @@ def _on_rows_removed(self, parent, start, end): "next data {!r}, last data {!r}".format( self._modelindex_debug(c.parent), c.old_size, expected_size, - extract_from_variant(c.next), - extract_from_variant(c.last) + qt_api.extract_from_variant(c.next), + qt_api.extract_from_variant(c.last) ) ) @@ -609,12 +608,12 @@ def _on_rows_removed(self, parent, start, end): "next data {!r}, last data {!r}".format( self._modelindex_debug(parent), current_size, - extract_from_variant(next_data), - extract_from_variant(last_data) + qt_api.extract_from_variant(next_data), + qt_api.extract_from_variant(last_data) ) ) - if not QtCore.qVersion().startswith('4.'): + if not qt_api.QtCore.qVersion().startswith('4.'): # Skipping this on Qt4 as the parent changes for some reason # see _on_rows_inserted for details assert c.parent == parent @@ -636,24 +635,24 @@ def _on_data_changed(self, top_left, bottom_right): assert bottom_right.column() < column_count def _on_header_data_changed(self, orientation, start, end): - assert orientation in [QtCore.Qt.Horizontal, QtCore.Qt.Vertical] + assert orientation in [qt_api.QtCore.Qt.Horizontal, qt_api.QtCore.Qt.Vertical] assert start >= 0 assert end >= 0 assert start <= end - if orientation == QtCore.Qt.Vertical: + if orientation == qt_api.QtCore.Qt.Vertical: item_count = self._model.rowCount() else: item_count = self._column_count() assert start < item_count assert end < item_count - def _column_count(self, parent=QtCore.QModelIndex()): + def _column_count(self, parent=qt_api.QtCore.QModelIndex()): """ Workaround for the fact that ``columnCount`` is a private method in QAbstractListModel/QAbstractTableModel subclasses. """ - if isinstance(self._model, QAbstractListModel): - return 1 if parent == QtCore.QModelIndex() else 0 + if isinstance(self._model, qt_api.QAbstractListModel): + return 1 if parent == qt_api.QtCore.QModelIndex() else 0 else: return self._model.columnCount(parent) @@ -661,19 +660,19 @@ def _parent(self, index): """ .. see:: ``_column_count`` """ - model_types = (QAbstractListModel, QAbstractTableModel) + model_types = (qt_api.QAbstractListModel, qt_api.QAbstractTableModel) if isinstance(self._model, model_types): - return QtCore.QModelIndex() + return qt_api.QtCore.QModelIndex() else: return self._model.parent(index) - def _has_children(self, parent=QtCore.QModelIndex()): + def _has_children(self, parent=qt_api.QtCore.QModelIndex()): """ .. see:: ``_column_count`` """ - model_types = (QAbstractListModel, QAbstractTableModel) + model_types = (qt_api.QAbstractListModel, qt_api.QAbstractTableModel) if isinstance(self._model, model_types): - return parent == QtCore.QModelIndex() and self._model.rowCount() > 0 + return parent == qt_api.QtCore.QModelIndex() and self._model.rowCount() > 0 else: return self._model.hasChildren(parent) diff --git a/pytestqt/plugin.py b/pytestqt/plugin.py index c17e0baa..4d6845a7 100644 --- a/pytestqt/plugin.py +++ b/pytestqt/plugin.py @@ -3,7 +3,7 @@ from pytestqt.exceptions import capture_exceptions, format_captured_exceptions, \ _is_exception_capture_enabled, _QtExceptionCaptureManager from pytestqt.logging import QtLoggingPlugin, _QtMessageCapture, Record -from pytestqt.qt_compat import QApplication, QT_API +from pytestqt.qt_compat import qt_api from pytestqt.qtbot import QtBot, _close_widgets from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker, SignalTimeoutError @@ -24,11 +24,10 @@ def qapp(): fixture that instantiates the QApplication instance that will be used by the tests. """ - from pytestqt.qt_compat import QApplication - app = QApplication.instance() + app = qt_api.QApplication.instance() if app is None: global _qapp_instance - _qapp_instance = QApplication([]) + _qapp_instance = qt_api.QApplication([]) yield _qapp_instance else: yield app # pragma: no cover @@ -71,6 +70,8 @@ def qtmodeltester(request): def pytest_addoption(parser): + parser.addini('qt_api', + 'Qt api version to use: "pyside", "pyqt4", "pyqt4v2", "pyqt5"') parser.addini('qt_no_exception_capture', 'disable automatic exception capture') parser.addini('qt_wait_signal_raising', @@ -88,14 +89,8 @@ def pytest_addoption(parser): group = parser.getgroup('qt', 'qt testing') group.addoption('--no-qt-log', dest='qt_log', action='store_false', default=True, help='disable pytest-qt logging capture') - if QT_API == 'pyqt5': - default = '{rec.context.file}:{rec.context.function}:' \ - '{rec.context.line}:\n {rec.type_name}: {rec.message}' - else: - default = '{rec.type_name}: {rec.message}' - group.addoption('--qt-log-format', dest='qt_log_format', default=default, - help='defines how qt log messages are displayed, ' - 'default: "{0}"'.format(default)) + group.addoption('--qt-log-format', dest='qt_log_format', default=None, + help='defines how qt log messages are displayed.') @pytest.mark.hookwrapper @@ -148,7 +143,7 @@ def _process_events(): """Calls app.processEvents() while taking care of capturing exceptions or not based on the given item's configuration. """ - app = QApplication.instance() + app = qt_api.QApplication.instance() if app is not None: app.processEvents() @@ -169,10 +164,15 @@ def pytest_configure(config): if config.getoption('qt_log'): config.pluginmanager.register(QtLoggingPlugin(config), '_qt_logging') + qt_api.set_qt_api(config.getini('qt_api')) + + from .qtbot import QtBot + QtBot._inject_qtest_methods() + def pytest_report_header(): - from pytestqt.qt_compat import get_versions - v = get_versions() + from pytestqt.qt_compat import qt_api + v = qt_api.get_versions() fields = [ '%s %s' % (v.qt_api, v.qt_api_version), 'Qt runtime %s' % v.runtime, diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 5a988c7c..22f0bf88 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -13,211 +13,180 @@ from collections import namedtuple import os -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' -if not on_rtd: # pragma: no cover +VersionTuple = namedtuple('VersionTuple', 'qt_api, qt_api_version, runtime, compiled') - def _try_import(name): - try: - __import__(name) - return True - except ImportError: - return False - def _guess_qt_api(): - if _try_import('PyQt5'): +class _QtApi: + """ + Interface to the underlying Qt API currently configured for pytest-qt. + + This object lazily loads all class references and other objects when the ``set_qt_api`` method + gets called, providing a uniform way to access the Qt classes. + """ + + 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 + msg = 'Invalid value for $PYTEST_QT_API: %s' + raise RuntimeError(msg % qt_api) + return api + + def _guess_qt_api(self): # pragma: no cover + def _can_import(name): + try: + __import__(name) + return True + except ImportError: + return False + + if _can_import('PyQt5'): return 'pyqt5' - elif _try_import('PySide'): + elif _can_import('PySide'): return 'pyside' - elif _try_import('PyQt4'): + elif _can_import('PyQt4'): return 'pyqt4' - else: + 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' raise RuntimeError(msg) - # backward compatibility support: PYTEST_QT_FORCE_PYQT - if os.environ.get('PYTEST_QT_FORCE_PYQT', 'false') == 'true': - QT_API = 'pyqt4' - else: - QT_API = os.environ.get('PYTEST_QT_API') - if QT_API is not None: - QT_API = QT_API.lower() - if QT_API not in ('pyside', 'pyqt4', 'pyqt4v2', 'pyqt5'): - msg = 'Invalid value for $PYTEST_QT_API: %s' - raise RuntimeError(msg % QT_API) - else: - QT_API = _guess_qt_api() - - # backward compatibility - USING_PYSIDE = QT_API == 'pyside' - - def _import_module(module_name): - m = __import__(_root_module, globals(), locals(), [module_name], 0) - return getattr(m, module_name) - - _root_modules = { - 'pyside': 'PySide', - 'pyqt4': 'PyQt4', - 'pyqt4v2': 'PyQt4', - 'pyqt5': 'PyQt5', - } - _root_module = _root_modules[QT_API] - - if QT_API == 'pyqt4v2': - # the v2 api in PyQt4 - # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html - import sip - sip.setapi("QDate", 2) - sip.setapi("QDateTime", 2) - sip.setapi("QString", 2) - sip.setapi("QTextStream", 2) - sip.setapi("QTime", 2) - sip.setapi("QUrl", 2) - sip.setapi("QVariant", 2) - - QtCore = _import_module('QtCore') - QtGui = _import_module('QtGui') - QtTest = _import_module('QtTest') - Qt = QtCore.Qt - QEvent = QtCore.QEvent - - qDebug = QtCore.qDebug - qWarning = QtCore.qWarning - qCritical = QtCore.qCritical - qFatal = QtCore.qFatal - QtDebugMsg = QtCore.QtDebugMsg - QtWarningMsg = QtCore.QtWarningMsg - QtCriticalMsg = QtCore.QtCriticalMsg - QtFatalMsg = QtCore.QtFatalMsg - - # Qt4 and Qt5 have different functions to install a message handler; - # the plugin will try to use the one that is not None - qInstallMsgHandler = None - qInstallMessageHandler = None - - VersionTuple = namedtuple('VersionTuple', - 'qt_api, qt_api_version, runtime, compiled') - - if QT_API == 'pyside': - import PySide - Signal = QtCore.Signal - Slot = QtCore.Slot - Property = QtCore.Property - QApplication = QtGui.QApplication - QWidget = QtGui.QWidget - qInstallMsgHandler = QtCore.qInstallMsgHandler - - QStandardItem = QtGui.QStandardItem - QStandardItemModel = QtGui.QStandardItemModel - QStringListModel = QtGui.QStringListModel - QSortFilterProxyModel = QtGui.QSortFilterProxyModel - QAbstractListModel = QtCore.QAbstractListModel - QAbstractTableModel = QtCore.QAbstractTableModel - - def extract_from_variant(variant): - """PySide does not expose QVariant API""" - return variant - - def make_variant(value=None): - """PySide does not expose QVariant API""" - return value - - def get_versions(): - return VersionTuple('PySide', PySide.__version__, QtCore.qVersion(), - QtCore.__version__) - - elif QT_API in ('pyqt4', 'pyqt4v2', 'pyqt5'): - Signal = QtCore.pyqtSignal - Slot = QtCore.pyqtSlot - Property = QtCore.pyqtProperty - - if QT_API == 'pyqt5': - _QtWidgets = _import_module('QtWidgets') - QApplication = _QtWidgets.QApplication - QWidget = _QtWidgets.QWidget - qInstallMessageHandler = QtCore.qInstallMessageHandler - - QStringListModel = QtCore.QStringListModel - QSortFilterProxyModel = QtCore.QSortFilterProxyModel + _root_modules = { + 'pyside': 'PySide', + 'pyqt4': 'PyQt4', + 'pyqt4v2': 'PyQt4', + 'pyqt5': 'PyQt5', + } + _root_module = _root_modules[self.pytest_qt_api] + + def _import_module(module_name): + m = __import__(_root_module, globals(), locals(), [module_name], 0) + return getattr(m, module_name) + + if self.pytest_qt_api == 'pyqt4v2': # pragma: no cover + # the v2 api in PyQt4 + # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html + import sip + sip.setapi("QDate", 2) + sip.setapi("QDateTime", 2) + sip.setapi("QString", 2) + sip.setapi("QTextStream", 2) + sip.setapi("QTime", 2) + sip.setapi("QUrl", 2) + sip.setapi("QVariant", 2) + + self.QtCore = QtCore = _import_module('QtCore') + self.QtGui = QtGui = _import_module('QtGui') + self.QtTest = _import_module('QtTest') + self.Qt = QtCore.Qt + self.QEvent = QtCore.QEvent + + self.qDebug = QtCore.qDebug + self.qWarning = QtCore.qWarning + self.qCritical = QtCore.qCritical + self.qFatal = QtCore.qFatal + self.QtDebugMsg = QtCore.QtDebugMsg + self.QtWarningMsg = QtCore.QtWarningMsg + self.QtCriticalMsg = QtCore.QtCriticalMsg + self.QtFatalMsg = QtCore.QtFatalMsg + + # Qt4 and Qt5 have different functions to install a message handler; + # the plugin will try to use the one that is not None + self.qInstallMsgHandler = None + self.qInstallMessageHandler = None + + if self.pytest_qt_api == '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 def extract_from_variant(variant): - """returns python object from the given QVariant""" - if isinstance(variant, QtCore.QVariant): - return variant.value() + """PySide does not expose QVariant API""" return variant - qt_api_name = 'PyQt5' - else: - QApplication = QtGui.QApplication - QWidget = QtGui.QWidget - qInstallMsgHandler = QtCore.qInstallMsgHandler + def make_variant(value=None): + """PySide does not expose QVariant API""" + return value - QStringListModel = QtGui.QStringListModel - QSortFilterProxyModel = QtGui.QSortFilterProxyModel + self.extract_from_variant = extract_from_variant + self.make_variant = make_variant - def extract_from_variant(variant): - """returns python object from the given QVariant""" - if isinstance(variant, QtCore.QVariant): - return variant.toPyObject() - return variant + elif self.pytest_qt_api in ('pyqt4', 'pyqt4v2', 'pyqt5'): + self.Signal = QtCore.pyqtSignal + self.Slot = QtCore.pyqtSlot + self.Property = QtCore.pyqtProperty - qt_api_name = 'PyQt4' + if self.pytest_qt_api == 'pyqt5': + _QtWidgets = _import_module('QtWidgets') + self.QApplication = _QtWidgets.QApplication + self.QWidget = _QtWidgets.QWidget + self.qInstallMessageHandler = QtCore.qInstallMessageHandler - QStandardItem = QtGui.QStandardItem - QStandardItemModel = QtGui.QStandardItemModel - QAbstractListModel = QtCore.QAbstractListModel - QAbstractTableModel = QtCore.QAbstractTableModel + self.QStringListModel = QtCore.QStringListModel + self.QSortFilterProxyModel = QtCore.QSortFilterProxyModel - def get_versions(): - return VersionTuple(qt_api_name, QtCore.PYQT_VERSION_STR, - QtCore.qVersion(), QtCore.QT_VERSION_STR) + def extract_from_variant(variant): + """not needed in PyQt5: Qt API always returns pure python objects""" + return variant + + def make_variant(value=None): + """Return a QVariant object from the given Python builtin""" + # PyQt4 doesn't allow one to instantiate any QVariant at all: + # QVariant represents a mapped type and cannot be instantiated + return QtCore.QVariant(value) - def make_variant(value=None): - """Return a QVariant object from the given Python builtin""" - # PyQt4 doesn't allow one to instantiate any QVariant at all: - # QVariant represents a mapped type and cannot be instantiated - if QT_API in ['pyqt4', 'pyqt4v2']: - return value - return QtCore.QVariant(value) - -else: # pragma: no cover - USING_PYSIDE = True - - # mock Qt when we are generating documentation at readthedocs.org - class Mock(object): - def __init__(self, *args, **kwargs): - pass - - def __call__(self, *args, **kwargs): - return Mock() - - @classmethod - def __getattr__(cls, name): - if name in ('__file__', '__path__'): - return '/dev/null' - elif name in ('__name__', '__qualname__'): - return name - elif name == '__annotations__': - return {} else: - return Mock() - - QtGui = Mock() - QtCore = Mock() - QtTest = Mock() - Qt = Mock() - QEvent = Mock() - QApplication = Mock() - QWidget = Mock() - qInstallMsgHandler = Mock() - qInstallMessageHandler = Mock() - qDebug = Mock() - qWarning = Mock() - qCritical = Mock() - qFatal = Mock() - QtDebugMsg = Mock() - QtWarningMsg = Mock() - QtCriticalMsg = Mock() - QtFatalMsg = Mock() - QT_API = '' - extract_from_variant = Mock() + self.QApplication = QtGui.QApplication + self.QWidget = QtGui.QWidget + self.qInstallMsgHandler = QtCore.qInstallMsgHandler + + self.QStringListModel = QtGui.QStringListModel + self.QSortFilterProxyModel = QtGui.QSortFilterProxyModel + + def extract_from_variant(variant): + """returns python object from the given QVariant""" + if isinstance(variant, QtCore.QVariant): + return variant.toPyObject() + return variant + + def make_variant(value=None): + """Return a QVariant object from the given Python builtin""" + # PyQt4 doesn't allow one to instantiate any QVariant at all: + # QVariant represents a mapped type and cannot be instantiated + return value + + self.QStandardItem = QtGui.QStandardItem + self.QStandardItemModel = QtGui.QStandardItemModel + self.QAbstractListModel = QtCore.QAbstractListModel + self.QAbstractTableModel = QtCore.QAbstractTableModel + + self.extract_from_variant = extract_from_variant + 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(), + self.QtCore.__version__) + else: + qt_api_name = 'PyQt5' if self.pytest_qt_api == 'pyqt5' else 'PyQt4' + return VersionTuple(qt_api_name, self.QtCore.PYQT_VERSION_STR, + self.QtCore.qVersion(), self.QtCore.QT_VERSION_STR) + +qt_api = _QtApi() diff --git a/pytestqt/qtbot.py b/pytestqt/qtbot.py index 6d820397..5fba0c4f 100644 --- a/pytestqt/qtbot.py +++ b/pytestqt/qtbot.py @@ -1,9 +1,8 @@ import functools import contextlib import weakref -from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker, SignalTimeoutError, \ - SignalEmittedSpy -from pytestqt.qt_compat import QtTest, QApplication +from pytestqt.wait_signal import SignalBlocker, MultiSignalBlocker, SignalTimeoutError, SignalEmittedSpy +from pytestqt.qt_compat import qt_api def _parse_ini_boolean(value): @@ -15,51 +14,6 @@ def _parse_ini_boolean(value): raise ValueError('unknown string for bool: %r' % value) -def _inject_qtest_methods(cls): - """ - Injects QTest methods into the given class QtBot, so the user can access - them directly without having to import QTest. - """ - - def create_qtest_proxy_method(method_name): - - if hasattr(QtTest.QTest, method_name): - qtest_method = getattr(QtTest.QTest, method_name) - - def result(*args, **kwargs): - return qtest_method(*args, **kwargs) - - functools.update_wrapper(result, qtest_method) - return staticmethod(result) - else: - return None # pragma: no cover - - # inject methods from QTest into QtBot - method_names = [ - 'keyPress', - 'keyClick', - 'keyClicks', - 'keyEvent', - 'keyPress', - 'keyRelease', - 'keyToAscii', - - 'mouseClick', - 'mouseDClick', - 'mouseEvent', - 'mouseMove', - 'mousePress', - 'mouseRelease', - ] - for method_name in method_names: - method = create_qtest_proxy_method(method_name) - if method is not None: - setattr(cls, method_name, method) - - return cls - - -@_inject_qtest_methods class QtBot(object): """ Instances of this class are responsible for sending events to `Qt` objects (usually widgets), @@ -200,12 +154,12 @@ def waitForWindowShown(self, widget): .. note:: This method is also available as ``wait_for_window_shown`` (pep-8 alias) """ - if hasattr(QtTest.QTest, 'qWaitForWindowShown'): # pragma: no cover + if hasattr(qt_api.QtTest.QTest, 'qWaitForWindowShown'): # pragma: no cover # PyQt4 and PySide - QtTest.QTest.qWaitForWindowShown(widget) + qt_api.QtTest.QTest.qWaitForWindowShown(widget) else: # pragma: no cover # PyQt5 - QtTest.QTest.qWaitForWindowExposed(widget) + qt_api.QtTest.QTest.qWaitForWindowExposed(widget) wait_for_window_shown = waitForWindowShown # pep-8 alias @@ -227,7 +181,7 @@ def stopForInteraction(self): if widget is not None: widget_and_visibility.append((widget, widget.isVisible())) - QApplication.instance().exec_() + qt_api.QApplication.instance().exec_() for widget, visible in widget_and_visibility: widget.setVisible(visible) @@ -472,6 +426,48 @@ def timed_out(): wait_until = waitUntil # pep-8 alias + @classmethod + def _inject_qtest_methods(cls): + """ + Injects QTest methods into the given class QtBot, so the user can access + them directly without having to import QTest. + """ + + def create_qtest_proxy_method(method_name): + + if hasattr(qt_api.QtTest.QTest, method_name): + qtest_method = getattr(qt_api.QtTest.QTest, method_name) + + def result(*args, **kwargs): + return qtest_method(*args, **kwargs) + + functools.update_wrapper(result, qtest_method) + return staticmethod(result) + else: + return None # pragma: no cover + + # inject methods from QTest into QtBot + method_names = [ + 'keyPress', + 'keyClick', + 'keyClicks', + 'keyEvent', + 'keyPress', + 'keyRelease', + 'keyToAscii', + + 'mouseClick', + 'mouseDClick', + 'mouseEvent', + 'mouseMove', + 'mousePress', + 'mouseRelease', + ] + for method_name in method_names: + method = create_qtest_proxy_method(method_name) + if method is not None: + setattr(cls, method_name, method) + # provide easy access to SignalTimeoutError to qtbot fixtures QtBot.SignalTimeoutError = SignalTimeoutError diff --git a/pytestqt/wait_signal.py b/pytestqt/wait_signal.py index 0f8de923..87572ce6 100644 --- a/pytestqt/wait_signal.py +++ b/pytestqt/wait_signal.py @@ -1,5 +1,5 @@ import functools -from pytestqt.qt_compat import QtCore +from pytestqt.qt_compat import qt_api class _AbstractSignalBlocker(object): @@ -16,14 +16,14 @@ class _AbstractSignalBlocker(object): """ def __init__(self, timeout=1000, raising=True): - self._loop = QtCore.QEventLoop() + self._loop = qt_api.QtCore.QEventLoop() self.timeout = timeout self.signal_triggered = False self.raising = raising if timeout is None: self._timer = None else: - self._timer = QtCore.QTimer(self._loop) + self._timer = qt_api.QtCore.QTimer(self._loop) self._timer.setSingleShot(True) self._timer.setInterval(timeout) diff --git a/tests/test_basics.py b/tests/test_basics.py index 73567e26..2149a533 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,8 +1,6 @@ import weakref import pytest -from pytestqt.qt_compat import QtGui, Qt, QEvent, QtCore, QApplication, \ - QWidget, make_variant, extract_from_variant -import pytestqt.qtbot +from pytestqt.qt_compat import qt_api def test_basics(qtbot): @@ -10,8 +8,8 @@ def test_basics(qtbot): Basic test that works more like a sanity check to ensure we are setting up a QApplication properly and are able to display a simple event_recorder. """ - assert QApplication.instance() is not None - widget = QWidget() + assert qt_api.QApplication.instance() is not None + widget = qt_api.QWidget() qtbot.addWidget(widget) widget.setWindowTitle('W1') widget.show() @@ -31,13 +29,13 @@ def extract(key_event): key_event.text(), ) - event_recorder.registerEvent(QtGui.QKeyEvent, extract) + event_recorder.registerEvent(qt_api.QtGui.QKeyEvent, extract) qtbot.keyPress(event_recorder, 'a') - assert event_recorder.event_data == (QEvent.KeyPress, int(Qt.Key_A), 'a') + assert event_recorder.event_data == (qt_api.QEvent.KeyPress, int(qt_api.Qt.Key_A), 'a') qtbot.keyRelease(event_recorder, 'a') - assert event_recorder.event_data == (QEvent.KeyRelease, int(Qt.Key_A), 'a') + assert event_recorder.event_data == (qt_api.QEvent.KeyRelease, int(qt_api.Qt.Key_A), 'a') def test_mouse_events(qtbot, event_recorder): @@ -51,23 +49,23 @@ def extract(mouse_event): mouse_event.modifiers(), ) - event_recorder.registerEvent(QtGui.QMouseEvent, extract) + event_recorder.registerEvent(qt_api.QtGui.QMouseEvent, extract) - qtbot.mousePress(event_recorder, Qt.LeftButton) - assert event_recorder.event_data == (QEvent.MouseButtonPress, Qt.LeftButton, Qt.NoModifier) + qtbot.mousePress(event_recorder, qt_api.Qt.LeftButton) + assert event_recorder.event_data == (qt_api.QEvent.MouseButtonPress, qt_api.Qt.LeftButton, qt_api.Qt.NoModifier) - qtbot.mousePress(event_recorder, Qt.RightButton, Qt.AltModifier) - assert event_recorder.event_data == (QEvent.MouseButtonPress, Qt.RightButton, Qt.AltModifier) + qtbot.mousePress(event_recorder, qt_api.Qt.RightButton, qt_api.Qt.AltModifier) + assert event_recorder.event_data == (qt_api.QEvent.MouseButtonPress, qt_api.Qt.RightButton, qt_api.Qt.AltModifier) def test_stop_for_interaction(qtbot): """ Test qtbot.stopForInteraction() """ - widget = QWidget() + widget = qt_api.QWidget() qtbot.addWidget(widget) qtbot.waitForWindowShown(widget) - QtCore.QTimer.singleShot(0, widget.close) + qt_api.QtCore.QTimer.singleShot(0, widget.close) qtbot.stopForInteraction() @@ -75,7 +73,7 @@ def test_widget_kept_as_weakref(qtbot): """ Test if the widget is kept as a weak reference in QtBot """ - widget = QWidget() + widget = qt_api.QWidget() qtbot.add_widget(widget) widget = weakref.ref(widget) assert widget() is None @@ -94,24 +92,24 @@ def test_event_processing_before_and_after_teardown(testdir): """ testdir.makepyfile( ''' - from pytestqt.qt_compat import QtCore, QEvent + from pytestqt.qt_compat import qt_api import pytest @pytest.fixture(scope='session') def events_queue(qapp): - class EventsQueue(QtCore.QObject): + class EventsQueue(qt_api.QtCore.QObject): def __init__(self): - QtCore.QObject.__init__(self) + qt_api.QtCore.QObject.__init__(self) self.events = [] def pop_later(self): - qapp.postEvent(self, QEvent(QEvent.User)) + qapp.postEvent(self, qt_api.QEvent(qt_api.QEvent.User)) def event(self, ev): - if ev.type() == QEvent.User: + if ev.type() == qt_api.QEvent.User: self.events.pop(-1) - return QtCore.QObject.event(self, ev) + return qt_api.QtCore.QObject.event(self, ev) return EventsQueue() @@ -140,12 +138,13 @@ def test_header(testdir): testdir.makeconftest( ''' from pytestqt import qt_compat + from pytestqt.qt_compat import qt_api def mock_get_versions(): return qt_compat.VersionTuple('PyQtAPI', '1.0', '2.5', '3.5') - assert hasattr(qt_compat, 'get_versions') - qt_compat.get_versions = mock_get_versions + assert hasattr(qt_api, 'get_versions') + qt_api.get_versions = mock_get_versions ''' ) res = testdir.runpytest() @@ -176,15 +175,15 @@ def test_qvariant(tmpdir): """Test that make_variant and extract_from_variant work in the same way across all supported Qt bindings. """ - settings = QtCore.QSettings(str(tmpdir / 'foo.ini'), - QtCore.QSettings.IniFormat) - settings.setValue('int', make_variant(42)) - settings.setValue('str', make_variant('Hello')) - settings.setValue('empty', make_variant()) + settings = qt_api.QtCore.QSettings(str(tmpdir / 'foo.ini'), + qt_api.QtCore.QSettings.IniFormat) + settings.setValue('int', qt_api.make_variant(42)) + settings.setValue('str', qt_api.make_variant('Hello')) + settings.setValue('empty', qt_api.make_variant()) - assert extract_from_variant(settings.value('int')) == 42 - assert extract_from_variant(settings.value('str')) == 'Hello' - assert extract_from_variant(settings.value('empty')) is None + assert qt_api.extract_from_variant(settings.value('int')) == 42 + assert qt_api.extract_from_variant(settings.value('str')) == 'Hello' + assert qt_api.extract_from_variant(settings.value('empty')) is None def test_widgets_closed_before_fixtures(testdir): @@ -194,9 +193,9 @@ def test_widgets_closed_before_fixtures(testdir): """ testdir.makepyfile(''' import pytest - from pytestqt.qt_compat import QWidget + from pytestqt.qt_compat import qt_api - class Widget(QWidget): + class Widget(qt_api.QWidget): closed = False @@ -227,35 +226,35 @@ def test_qtbot_wait(qtbot, stop_watch): assert stop_watch.elapsed >= 220 -class EventRecorder(QWidget): +@pytest.fixture +def event_recorder(qtbot): - """ - Widget that records some kind of events sent to it. + class EventRecorder(qt_api.QWidget): - When this event_recorder receives a registered event (by calling `registerEvent`), it will call - the associated *extract* function and hold the return value from the function in the - `event_data` member. - """ + """ + Widget that records some kind of events sent to it. - def __init__(self): - QWidget.__init__(self) - self._event_types = {} - self.event_data = None + When this event_recorder receives a registered event (by calling `registerEvent`), it will call + the associated *extract* function and hold the return value from the function in the + `event_data` member. + """ - def registerEvent(self, event_type, extract_func): - self._event_types[event_type] = extract_func + def __init__(self): + qt_api.QWidget.__init__(self) + self._event_types = {} + self.event_data = None - def event(self, ev): - for event_type, extract_func in self._event_types.items(): - if isinstance(ev, event_type): - self.event_data = extract_func(ev) - return True + def registerEvent(self, event_type, extract_func): + self._event_types[event_type] = extract_func - return False + def event(self, ev): + for event_type, extract_func in self._event_types.items(): + if isinstance(ev, event_type): + self.event_data = extract_func(ev) + return True + return False -@pytest.fixture -def event_recorder(qtbot): widget = EventRecorder() qtbot.addWidget(widget) return widget @@ -270,9 +269,42 @@ def event_recorder(qtbot): ('false', False), ]) def test_parse_ini_boolean_valid(value, expected): + import pytestqt.qtbot assert pytestqt.qtbot._parse_ini_boolean(value) == expected def test_parse_ini_boolean_invalid(): + import pytestqt.qtbot with pytest.raises(ValueError): pytestqt.qtbot._parse_ini_boolean('foo') + + +@pytest.mark.parametrize('option_api', ['pyqt4', 'pyqt5', 'pyside']) +def test_qt_api_ini_config(testdir, option_api): + """ + Test qt_api ini option handling. + """ + from pytestqt.qt_compat import qt_api + + testdir.makeini(""" + [pytest] + qt_api={option_api} + """.format(option_api=option_api)) + + testdir.makepyfile(''' + import pytest + + def test_foo(qtbot): + pass + ''') + + result = testdir.runpytest_subprocess() + if qt_api.pytest_qt_api.startswith(option_api): # handle pyqt4v2 + result.stdout.fnmatch_lines([ + '* 1 passed in *' + ]) + else: + result.stderr.fnmatch_lines([ + '*ImportError:*' + ]) + diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4a5929ff..acb2ca73 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -12,21 +12,21 @@ def test_catch_exceptions_in_virtual_methods(testdir, raise_error): :type testdir: _pytest.pytester.TmpTestdir """ testdir.makepyfile(''' - from pytestqt.qt_compat import QtCore, QApplication + from pytestqt.qt_compat import qt_api - class Receiver(QtCore.QObject): + class Receiver(qt_api.QtCore.QObject): def event(self, ev): if {raise_error}: raise ValueError('mistakes were made') - return QtCore.QObject.event(self, ev) + return qt_api.QtCore.QObject.event(self, ev) def test_exceptions(qtbot): v = Receiver() - app = QApplication.instance() - app.sendEvent(v, QtCore.QEvent(QtCore.QEvent.User)) - app.sendEvent(v, QtCore.QEvent(QtCore.QEvent.User)) + app = qt_api.QApplication.instance() + app.sendEvent(v, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.User)) + app.sendEvent(v, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.User)) app.processEvents() '''.format(raise_error=raise_error)) @@ -74,7 +74,9 @@ def test_no_capture(testdir, no_capture_by_marker): testdir.makepyfile(''' import pytest import sys - from pytestqt.qt_compat import QWidget, QtCore + from pytestqt.qt_compat import qt_api + QWidget = qt_api.QWidget + QtCore = qt_api.QtCore # PyQt 5.5+ will crash if there's no custom exception handler installed sys.excepthook = lambda *args: None @@ -103,7 +105,9 @@ def test_no_capture_preserves_custom_excepthook(testdir): testdir.makepyfile(''' import pytest import sys - from pytestqt.qt_compat import QWidget, QtCore + from pytestqt.qt_compat import qt_api + QWidget = qt_api.QWidget + QtCore = qt_api.QtCore def custom_excepthook(*args): sys.__excepthook__(*args) @@ -129,7 +133,10 @@ def test_exception_capture_on_call(testdir): """ testdir.makepyfile(''' import pytest - from pytestqt.qt_compat import QWidget, QtCore, QEvent + from pytestqt.qt_compat import qt_api + QWidget = qt_api.QWidget + QtCore = qt_api.QtCore + QEvent = qt_api.QtCore.QEvent class MyWidget(QWidget): @@ -156,7 +163,10 @@ def test_exception_capture_on_widget_close(testdir): """ testdir.makepyfile(''' import pytest - from pytestqt.qt_compat import QWidget, QtCore, QEvent + from pytestqt.qt_compat import qt_api + QWidget = qt_api.QWidget + QtCore = qt_api.QtCore + QEvent = qt_api.QtCore.QEvent class MyWidget(QWidget): @@ -192,7 +202,11 @@ def test_exception_capture_on_fixture_setup_and_teardown(testdir, mode): testdir.makepyfile(''' import pytest - from pytestqt.qt_compat import QWidget, QtCore, QEvent, QApplication + from pytestqt.qt_compat import qt_api + QWidget = qt_api.QWidget + QtCore = qt_api.QtCore + QEvent = qt_api.QtCore.QEvent + QApplication = qt_api.QApplication class MyWidget(QWidget): @@ -230,17 +244,17 @@ def test_capture_exceptions_context_manager(qapp): While not used internally anymore, it is still part of the API and therefore should be properly tested. """ - from pytestqt.qt_compat import QtCore + from pytestqt.qt_compat import qt_api from pytestqt.plugin import capture_exceptions - class Receiver(QtCore.QObject): + class Receiver(qt_api.QtCore.QObject): def event(self, ev): raise ValueError('mistakes were made') r = Receiver() with capture_exceptions() as exceptions: - qapp.sendEvent(r, QtCore.QEvent(QtCore.QEvent.User)) + qapp.sendEvent(r, qt_api.QtCore.QEvent(qt_api.QtCore.QEvent.User)) qapp.processEvents() assert [str(e) for (t, e, tb) in exceptions] == ['mistakes were made'] @@ -251,9 +265,9 @@ def test_exceptions_to_stderr(qapp, capsys): Exceptions should still be reported to stderr. """ called = [] - from pytestqt.qt_compat import QWidget, QEvent + from pytestqt.qt_compat import qt_api - class MyWidget(QWidget): + class MyWidget(qt_api.QWidget): def event(self, ev): called.append(1) @@ -261,7 +275,7 @@ def event(self, ev): w = MyWidget() with capture_exceptions() as exceptions: - qapp.postEvent(w, QEvent(QEvent.User)) + qapp.postEvent(w, qt_api.QEvent(qt_api.QEvent.User)) qapp.processEvents() assert called del exceptions[:] diff --git a/tests/test_logging.py b/tests/test_logging.py index 5f2a2be7..2f528e51 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -2,8 +2,7 @@ import pytest -from pytestqt.qt_compat import qDebug, qWarning, qCritical, QtDebugMsg, \ - QtWarningMsg, QtCriticalMsg, QT_API +from pytestqt.qt_compat import qt_api @pytest.mark.parametrize('test_succeeds', [True, False]) @@ -17,25 +16,24 @@ def test_basic_logging(testdir, test_succeeds, qt_log): testdir.makepyfile( """ import sys - from pytestqt.qt_compat import qDebug, qWarning, qCritical, \ - qInstallMessageHandler, qInstallMsgHandler + from pytestqt.qt_compat import qt_api def to_unicode(s): return s.decode('utf-8', 'replace') if isinstance(s, bytes) else s - if qInstallMessageHandler: + if qt_api.qInstallMessageHandler: def print_msg(msg_type, context, message): sys.stderr.write(to_unicode(message) + '\\n') - qInstallMessageHandler(print_msg) + qt_api.qInstallMessageHandler(print_msg) else: def print_msg(msg_type, message): sys.stderr.write(to_unicode(message) + '\\n') - qInstallMsgHandler(print_msg) + qt_api.qInstallMsgHandler(print_msg) def test_types(): - qDebug('this is a DEBUG message') - qWarning('this is a WARNING message') - qCritical('this is a CRITICAL message') + qt_api.qDebug('this is a DEBUG message') + qt_api.qWarning('this is a WARNING message') + qt_api.qCritical('this is a CRITICAL message') assert {0} """.format(test_succeeds) ) @@ -64,14 +62,14 @@ def test_qtlog_fixture(qtlog): """ Test qtlog fixture. """ - qDebug('this is a DEBUG message') - qWarning('this is a WARNING message') - qCritical('this is a CRITICAL message') + qt_api.qDebug('this is a DEBUG message') + qt_api.qWarning('this is a WARNING message') + qt_api.qCritical('this is a CRITICAL message') records = [(m.type, m.message.strip()) for m in qtlog.records] assert records == [ - (QtDebugMsg, 'this is a DEBUG message'), - (QtWarningMsg, 'this is a WARNING message'), - (QtCriticalMsg, 'this is a CRITICAL message'), + (qt_api.QtDebugMsg, 'this is a DEBUG message'), + (qt_api.QtWarningMsg, 'this is a WARNING message'), + (qt_api.QtCriticalMsg, 'this is a CRITICAL message'), ] # `records` attribute is read-only with pytest.raises(AttributeError): @@ -87,10 +85,10 @@ def test_fixture_with_logging_disabled(testdir): """ testdir.makepyfile( """ - from pytestqt.qt_compat import qWarning + from pytestqt.qt_compat import qt_api def test_types(qtlog): - qWarning('message') + qt_api.qWarning('message') assert qtlog.records == [] """ ) @@ -119,10 +117,10 @@ def test_disable_qtlog_context_manager(testdir, use_context_manager): testdir.makepyfile( """ - from pytestqt.qt_compat import qCritical + from pytestqt.qt_compat import qt_api def test_1(qtlog): {code} - qCritical('message') + qt_api.qCritical('message') """.format(code=code) ) res = testdir.inline_run() @@ -147,11 +145,11 @@ def test_disable_qtlog_mark(testdir, use_mark): testdir.makepyfile( """ - from pytestqt.qt_compat import qCritical + from pytestqt.qt_compat import qt_api import pytest {mark} def test_1(): - qCritical('message') + qt_api.qCritical('message') """.format(mark=mark) ) res = testdir.inline_run() @@ -167,9 +165,9 @@ def test_logging_formatting(testdir): """ testdir.makepyfile( """ - from pytestqt.qt_compat import qWarning + from pytestqt.qt_compat import qt_api def test_types(): - qWarning('this is a WARNING message') + qt_api.qWarning('this is a WARNING message') assert 0 """ ) @@ -200,13 +198,13 @@ def test_logging_fails_tests(testdir, level, expect_passes): ) testdir.makepyfile( """ - from pytestqt.qt_compat import qWarning, qCritical, qDebug + from pytestqt.qt_compat import qt_api def test_1(): - qDebug('this is a DEBUG message') + qt_api.qDebug('this is a DEBUG message') def test_2(): - qWarning('this is a WARNING message') + qt_api.qWarning('this is a WARNING message') def test_3(): - qCritical('this is a CRITICAL message') + qt_api.qCritical('this is a CRITICAL message') def test_4(): assert 1 """ @@ -265,19 +263,19 @@ def test_logging_fails_ignore(testdir): ) testdir.makepyfile( """ - from pytestqt.qt_compat import qCritical + from pytestqt.qt_compat import qt_api import pytest def test1(): - qCritical('a critical message') + qt_api.qCritical('a critical message') def test2(): - qCritical('WM_DESTROY was sent') + qt_api.qCritical('WM_DESTROY was sent') def test3(): - qCritical('WM_DESTROY was sent') + qt_api.qCritical('WM_DESTROY was sent') assert 0 def test4(): - qCritical('WM_PAINT not handled') - qCritical('another critical message') + qt_api.qCritical('WM_PAINT not handled') + qt_api.qCritical('another critical message') """ ) res = testdir.runpytest() @@ -328,12 +326,12 @@ def test_logging_mark_with_extend(testdir, message, marker_args): ) testdir.makepyfile( """ - from pytestqt.qt_compat import qCritical + from pytestqt.qt_compat import qt_api import pytest @pytest.mark.qt_log_ignore({marker_args}) def test1(): - qCritical('{message}') + qt_api.qCritical('{message}') """.format(message=message, marker_args=marker_args) ) res = testdir.inline_run() @@ -359,12 +357,12 @@ def test_logging_mark_without_extend(testdir, message, error_expected): ) testdir.makepyfile( """ - from pytestqt.qt_compat import qCritical + from pytestqt.qt_compat import qt_api import pytest @pytest.mark.qt_log_ignore('match-mark', extend=False) def test1(): - qCritical('{message}') + qt_api.qCritical('{message}') """.format(message=message) ) res = testdir.inline_run() @@ -416,12 +414,12 @@ def test_logging_fails_ignore_mark_multiple(testdir, apply_mark): mark = '' testdir.makepyfile( """ - from pytestqt.qt_compat import qCritical + from pytestqt.qt_compat import qt_api import pytest @pytest.mark.qt_log_level_fail('CRITICAL') {mark} def test1(): - qCritical('WM_PAINT was sent') + qt_api.qCritical('WM_PAINT was sent') """.format(mark=mark) ) res = testdir.inline_run() @@ -444,16 +442,16 @@ def test_lineno_failure(testdir): ) testdir.makepyfile( """ - from pytestqt.qt_compat import qWarning + from pytestqt.qt_compat import qt_api def test_foo(): assert foo() == 10 def foo(): - qWarning('this is a WARNING message') + qt_api.qWarning('this is a WARNING message') return 10 """ ) res = testdir.runpytest() - if QT_API == 'pyqt5': + if qt_api.pytest_qt_api == 'pyqt5': res.stdout.fnmatch_lines([ '*test_lineno_failure.py:2: Failure*', '*test_lineno_failure.py:foo:5:*', @@ -463,8 +461,6 @@ def foo(): res.stdout.fnmatch_lines('*test_lineno_failure.py:2: Failure*') -@pytest.mark.skipif(QT_API != 'pyqt5', - reason='Context information only available in PyQt5') def test_context_none(testdir): """ Sometimes PyQt5 will emit a context with some/all attributes set as None @@ -474,14 +470,16 @@ def test_context_none(testdir): :type testdir: _pytest.pytester.TmpTestdir """ + if qt_api.pytest_qt_api != 'pyqt5': + pytest.skip('Context information only available in PyQt5') testdir.makepyfile( """ - from pytestqt.qt_compat import QtWarningMsg + from pytestqt.qt_compat import qt_api def test_foo(request): log_capture = request.node.qt_log_capture context = log_capture._Context(None, None, None) - log_capture._handle_with_context(QtWarningMsg, + log_capture._handle_with_context(qt_api.QtWarningMsg, context, "WARNING message") assert 0 """ diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 474e05fb..5a00f9fc 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -1,38 +1,34 @@ -import os - import pytest -from pytestqt.qt_compat import QStandardItemModel, QStandardItem, \ - QStringListModel, QSortFilterProxyModel, QT_API, QAbstractListModel, \ - QtCore +from pytestqt.qt_compat import qt_api pytestmark = pytest.mark.usefixtures('qtbot') -class BasicModel(QtCore.QAbstractItemModel): +class BasicModel(qt_api.QtCore.QAbstractItemModel): - def data(self, index, role=QtCore.Qt.DisplayRole): + def data(self, index, role=qt_api.QtCore.Qt.DisplayRole): return None - def rowCount(self, parent=QtCore.QModelIndex()): + def rowCount(self, parent=qt_api.QtCore.QModelIndex()): return 0 - def columnCount(self, parent=QtCore.QModelIndex()): + def columnCount(self, parent=qt_api.QtCore.QModelIndex()): return 0 - def index(self, row, column, parent=QtCore.QModelIndex()): - return QtCore.QModelIndex() + def index(self, row, column, parent=qt_api.QtCore.QModelIndex()): + return qt_api.QtCore.QModelIndex() def parent(self, index): - return QtCore.QModelIndex() + return qt_api.QtCore.QModelIndex() def test_standard_item_model(qtmodeltester): """ - Basic test which uses qtmodeltester with a QStandardItemModel. + Basic test which uses qtmodeltester with a qt_api.QStandardItemModel. """ - model = QStandardItemModel() - items = [QStandardItem(str(i)) for i in range(6)] + model = qt_api.QStandardItemModel() + items = [qt_api.QStandardItem(str(i)) for i in range(6)] model.setItem(0, 0, items[0]) model.setItem(0, 1, items[1]) model.setItem(1, 0, items[2]) @@ -45,41 +41,41 @@ def test_standard_item_model(qtmodeltester): def test_string_list_model(qtmodeltester): - model = QStringListModel() + model = qt_api.QStringListModel() model.setStringList(['hello', 'world']) qtmodeltester.check(model) def test_sort_filter_proxy_model(qtmodeltester): - model = QStringListModel() + model = qt_api.QStringListModel() model.setStringList(['hello', 'world']) - proxy = QSortFilterProxyModel() + proxy = qt_api.QSortFilterProxyModel() proxy.setSourceModel(model) qtmodeltester.check(proxy) @pytest.mark.parametrize('broken_role', [ - QtCore.Qt.ToolTipRole, QtCore.Qt.StatusTipRole, - QtCore.Qt.WhatsThisRole, - QtCore.Qt.SizeHintRole, QtCore.Qt.FontRole, - QtCore.Qt.BackgroundColorRole, - QtCore.Qt.TextColorRole, QtCore.Qt.TextAlignmentRole, - QtCore.Qt.CheckStateRole, + qt_api.QtCore.Qt.ToolTipRole, qt_api.QtCore.Qt.StatusTipRole, + qt_api.QtCore.Qt.WhatsThisRole, + qt_api.QtCore.Qt.SizeHintRole, qt_api.QtCore.Qt.FontRole, + qt_api.QtCore.Qt.BackgroundColorRole, + qt_api.QtCore.Qt.TextColorRole, qt_api.QtCore.Qt.TextAlignmentRole, + qt_api.QtCore.Qt.CheckStateRole, ]) def test_broken_types(check_model, broken_role): """ Check that qtmodeltester correctly captures data() returning invalid values for various display roles. """ - class BrokenTypeModel(QAbstractListModel): + class BrokenTypeModel(qt_api.QAbstractListModel): - def rowCount(self, parent=QtCore.QModelIndex()): - if parent == QtCore.QModelIndex(): + def rowCount(self, parent=qt_api.QtCore.QModelIndex()): + if parent == qt_api.QtCore.QModelIndex(): return 1 else: return 0 - def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): + def data(self, index=qt_api.QtCore.QModelIndex(), role=qt_api.QtCore.Qt.DisplayRole): if role == broken_role: return object() # This will fail the type check for any role else: @@ -89,8 +85,8 @@ def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): @pytest.mark.parametrize('role_value, should_pass', [ - (QtCore.Qt.AlignLeft, True), - (QtCore.Qt.AlignRight, True), + (qt_api.QtCore.Qt.AlignLeft, True), + (qt_api.QtCore.Qt.AlignRight, True), (0xFFFFFF, False), ('foo', False), (object(), False), @@ -99,15 +95,15 @@ def test_data_alignment(role_value, should_pass, check_model): """Test a custom model which returns a good and alignments from data(). qtmodeltest should capture this problem and fail when that happens. """ - class MyModel(QAbstractListModel): + class MyModel(qt_api.QAbstractListModel): - def rowCount(self, parent=QtCore.QModelIndex()): - return 1 if parent == QtCore.QModelIndex() else 0 + def rowCount(self, parent=qt_api.QtCore.QModelIndex()): + return 1 if parent == qt_api.QtCore.QModelIndex() else 0 - def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): - if role == QtCore.Qt.TextAlignmentRole: + def data(self, index=qt_api.QtCore.QModelIndex(), role=qt_api.QtCore.Qt.DisplayRole): + if role == qt_api.QtCore.Qt.TextAlignmentRole: return role_value - elif role == QtCore.Qt.DisplayRole: + elif role == qt_api.QtCore.Qt.DisplayRole: if index == self.index(0, 0): return 'Hello' return None @@ -117,21 +113,21 @@ def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): def test_header_handling(check_model): - class MyModel(QAbstractListModel): + class MyModel(qt_api.QAbstractListModel): - def rowCount(self, parent=QtCore.QModelIndex()): - return 1 if parent == QtCore.QModelIndex() else 0 + def rowCount(self, parent=qt_api.QtCore.QModelIndex()): + return 1 if parent == qt_api.QtCore.QModelIndex() else 0 def set_header_text(self, header): self._header_text = header - self.headerDataChanged.emit(QtCore.Qt.Vertical, 0, 0) - self.headerDataChanged.emit(QtCore.Qt.Horizontal, 0, 0) + self.headerDataChanged.emit(qt_api.QtCore.Qt.Vertical, 0, 0) + self.headerDataChanged.emit(qt_api.QtCore.Qt.Horizontal, 0, 0) - def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): + def headerData(self, section, orientation, role=qt_api.QtCore.Qt.DisplayRole): return self._header_text - def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): - if role == QtCore.Qt.DisplayRole and index == self.index(0, 0): + def data(self, index=qt_api.QtCore.QModelIndex(), role=qt_api.QtCore.Qt.DisplayRole): + if role == qt_api.QtCore.Qt.DisplayRole and index == self.index(0, 0): return 'Contents' return None @@ -160,7 +156,7 @@ def check(model, should_pass=True): def test_invalid_column_count(qtmodeltester): """Basic check with an invalid model.""" class Model(BasicModel): - def columnCount(self, parent=QtCore.QModelIndex()): + def columnCount(self, parent=qt_api.QtCore.QModelIndex()): return -1 model = Model() @@ -170,33 +166,33 @@ def columnCount(self, parent=QtCore.QModelIndex()): def test_changing_model_insert(qtmodeltester): - model = QStandardItemModel() - item = QStandardItem('foo') + model = qt_api.QStandardItemModel() + item = qt_api.QStandardItem('foo') qtmodeltester.check(model) model.insertRow(0, item) def test_changing_model_remove(qtmodeltester): - model = QStandardItemModel() - item = QStandardItem('foo') + model = qt_api.QStandardItemModel() + item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) model.removeRow(0) def test_changing_model_data(qtmodeltester): - model = QStandardItemModel() - item = QStandardItem('foo') + model = qt_api.QStandardItemModel() + item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) model.setData(model.index(0, 0), 'hello world') -@pytest.mark.parametrize('orientation', [QtCore.Qt.Horizontal, - QtCore.Qt.Vertical]) +@pytest.mark.parametrize('orientation', [qt_api.QtCore.Qt.Horizontal, + qt_api.QtCore.Qt.Vertical]) def test_changing_model_header_data(qtmodeltester, orientation): - model = QStandardItemModel() - item = QStandardItem('foo') + model = qt_api.QStandardItemModel() + item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) model.setHeaderData(0, orientation, 'blah') @@ -204,8 +200,8 @@ def test_changing_model_header_data(qtmodeltester, orientation): def test_changing_model_sort(qtmodeltester): """Sorting emits layoutChanged""" - model = QStandardItemModel() - item = QStandardItem('foo') + model = qt_api.QStandardItemModel() + item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) model.sort(0) @@ -239,7 +235,7 @@ def rowCount(self, parent=None): def test_fetch_more(qtmodeltester): - class Model(QStandardItemModel): + class Model(qt_api.QStandardItemModel): def canFetchMore(self, parent): return True @@ -249,25 +245,25 @@ def fetchMore(self, parent): self.setData(self.index(0, 0), 'bar') model = Model() - item = QStandardItem('foo') + item = qt_api.QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model) def test_invalid_parent(qtmodeltester): - class Model(QStandardItemModel): + class Model(qt_api.QStandardItemModel): def parent(self, index): if index == self.index(0, 0, parent=self.index(0, 0)): return self.index(0, 0) else: - return QtCore.QModelIndex() + return qt_api.QtCore.QModelIndex() model = Model() - item = QStandardItem('foo') - item2 = QStandardItem('bar') - item3 = QStandardItem('bar') + item = qt_api.QStandardItem('foo') + item2 = qt_api.QStandardItem('bar') + item3 = qt_api.QStandardItem('bar') model.setItem(0, 0, item) item.setChild(0, item2) item2.setChild(0, item3) diff --git a/tests/test_qtest_proxies.py b/tests/test_qtest_proxies.py index f6144026..3f531496 100644 --- a/tests/test_qtest_proxies.py +++ b/tests/test_qtest_proxies.py @@ -1,10 +1,10 @@ import pytest -from pytestqt.qt_compat import USING_PYSIDE +# noinspection PyUnresolvedReferences +from pytestqt.qt_compat import qt_api -fails_on_pyqt = pytest.mark.xfail(not USING_PYSIDE, - reason='not exported by PyQt') +fails_on_pyqt = pytest.mark.xfail('qt_api.pytest_qt_api != "pyside"') @pytest.mark.parametrize('expected_method', [ diff --git a/tests/test_wait_signal.py b/tests/test_wait_signal.py index b8d1395f..b1159ba6 100644 --- a/tests/test_wait_signal.py +++ b/tests/test_wait_signal.py @@ -3,7 +3,7 @@ import pytest -from pytestqt.qt_compat import QtCore, Signal, QT_API +from pytestqt.qt_compat import qt_api from pytestqt.wait_signal import SignalEmittedError @@ -110,10 +110,10 @@ def test_raising(qtbot, testdir, configval, raises): testdir.makepyfile(""" import pytest - from pytestqt.qt_compat import QtCore, Signal + from pytestqt.qt_compat import qt_api - class Signaller(QtCore.QObject): - signal = Signal() + class Signaller(qt_api.QtCore.QObject): + signal = qt_api.Signal() def test_foo(qtbot): signaller = Signaller() @@ -137,10 +137,10 @@ def test_raising_by_default_overridden(qtbot, testdir): testdir.makepyfile(""" import pytest - from pytestqt.qt_compat import QtCore, Signal + from pytestqt.qt_compat import qt_api - class Signaller(QtCore.QObject): - signal = Signal() + class Signaller(qt_api.QtCore.QObject): + signal = qt_api.Signal() def test_foo(qtbot): signaller = Signaller() @@ -223,11 +223,11 @@ def signaller(timer): This was the reason for some random crashes experienced on Windows (#80). """ - class Signaller(QtCore.QObject): - signal = Signal() - signal_2 = Signal() - signal_args = Signal(str, int) - signal_args_2 = Signal(str, int) + class Signaller(qt_api.QtCore.QObject): + signal = qt_api.Signal() + signal_2 = qt_api.Signal() + signal_args = qt_api.Signal(str, int) + signal_args_2 = qt_api.Signal(str, int) assert timer @@ -243,9 +243,9 @@ def timer(): The fixture is responsible for cleaning up after the timers. """ - class Timer(QtCore.QObject): + class Timer(qt_api.QtCore.QObject): def __init__(self): - QtCore.QObject.__init__(self) + qt_api.QtCore.QObject.__init__(self) self.timers_and_slots = [] def shutdown(self): @@ -255,7 +255,7 @@ def shutdown(self): self.timers_and_slots[:] = [] def single_shot(self, signal, delay): - t = QtCore.QTimer(self) + t = qt_api.QtCore.QTimer(self) t.setSingleShot(True) slot = functools.partial(self._emit, signal) t.timeout.connect(slot) @@ -317,15 +317,22 @@ def test_wait_twice(qtbot, timer, multiple, do_timeout, signaller): signaller.signal.emit() -@pytest.mark.skipif(QT_API == 'pyside', reason='test crashes PySide') +def test_wait_signals_invalid_strict_parameter(qtbot, signaller): + with pytest.raises(ValueError): + qtbot.waitSignals([signaller.signal], order='invalid') + + def test_destroyed(qtbot): """Test that waitSignal works with the destroyed signal (#82). For some reason, this crashes PySide although it seems perfectly fine code. """ + if qt_api.pytest_qt_api == 'pyside': + pytest.skip('test crashes PySide') + import sip - class Obj(QtCore.QObject): + class Obj(qt_api.QtCore.QObject): pass obj = Obj() diff --git a/tests/test_wait_until.py b/tests/test_wait_until.py index 2011227a..6067e94e 100644 --- a/tests/test_wait_until.py +++ b/tests/test_wait_until.py @@ -41,13 +41,13 @@ def tick_counter(): """ Returns an object which counts timer "ticks" periodically. """ - from pytestqt.qt_compat import QtCore + from pytestqt.qt_compat import qt_api class Counter: def __init__(self): self._ticks = 0 - self.timer = QtCore.QTimer() + self.timer = qt_api.QtCore.QTimer() self.timer.timeout.connect(self._tick) def start(self, ms):