Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pydevproject
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}</path>
<path>/${PROJECT_DIR_NAME}/tests</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
Expand Down
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 5 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -57,4 +60,4 @@ install:
build: false

test_script:
- "%CMD_IN_ENV% python -m pytest tests/"
- "%CMD_IN_ENV% python -m pytest -v tests/"
57 changes: 40 additions & 17 deletions pytestqt/qt_compat.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
"""
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.

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

Expand All @@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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'
Expand Down
2 changes: 1 addition & 1 deletion pytestqt/qtbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_basics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 *'
])
Expand Down
2 changes: 2 additions & 0 deletions tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
4 changes: 2 additions & 2 deletions tests/test_qtest_proxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand All @@ -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
assert getattr(qtbot, expected_method).__name__ == expected_method
16 changes: 8 additions & 8 deletions tests/test_wait_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down