From 507929b4ea1998dc8a6e4293e59d51862033b7e4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Fri, 17 Jul 2015 07:50:26 +0200 Subject: [PATCH 01/60] WIP Qt model tester. --- pytestqt/_tests/test_modeltest.py | 19 + pytestqt/modeltest.py | 559 ++++++++++++++++++++++++++++++ pytestqt/plugin.py | 11 + pytestqt/qt_compat.py | 10 + 4 files changed, 599 insertions(+) create mode 100644 pytestqt/_tests/test_modeltest.py create mode 100644 pytestqt/modeltest.py diff --git a/pytestqt/_tests/test_modeltest.py b/pytestqt/_tests/test_modeltest.py new file mode 100644 index 00000000..6b3bf2d4 --- /dev/null +++ b/pytestqt/_tests/test_modeltest.py @@ -0,0 +1,19 @@ +import pytest +from pytestqt.qt_compat import QtGui + + +def test_valid_model(qtmodeltester): + """ + Basic test which uses qtmodeltester with a QStandardItemModel. + """ + model = QtGui.QStandardItemModel() + + items = [QtGui.QStandardItem(str(i)) for i in range(5)] + + items[0].setChild(0, items[4]) + model.setItem(0, 0, items[0]) + model.setItem(0, 1, items[1]) + model.setItem(0, 0, items[2]) + model.setItem(0, 1, items[3]) + + qtmodeltester.setup_and_run(model) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py new file mode 100644 index 00000000..acd32f17 --- /dev/null +++ b/pytestqt/modeltest.py @@ -0,0 +1,559 @@ +# This file is based on the original C++ modeltest.cpp, licensed under +# the following terms: +# +# Copyright (C) 2015 The Qt Company Ltd. +# Contact: http://www.qt.io/licensing/ +# +# This file is part of the test suite of the Qt Toolkit. +# +# $QT_BEGIN_LICENSE:LGPL21$ +# Commercial License Usage +# Licensees holding valid commercial Qt licenses may use this file in +# accordance with the commercial license agreement provided with the +# Software or, alternatively, in accordance with the terms contained in +# a written agreement between you and The Qt Company. For licensing terms +# and conditions see http://www.qt.io/terms-conditions. For further +# information use the contact form at http://www.qt.io/contact-us. +# +# GNU Lesser General Public License Usage +# Alternatively, this file may be used under the terms of the GNU Lesser +# General Public License version 2.1 or version 3 as published by the Free +# Software Foundation and appearing in the file LICENSE.LGPLv21 and +# LICENSE.LGPLv3 included in the packaging of this file. Please review the +# following information to ensure the GNU Lesser General Public License +# requirements will be met: https://www.gnu.org/licenses/lgpl.html and +# http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +# +# As a special exception, The Qt Company gives you certain additional +# rights. These rights are described in The Qt Company LGPL Exception +# version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +# +# $QT_END_LICENSE$ + + +import collections + +from pytestqt.qt_compat import QtCore, QtGui, cast + + +_Changing = collections.namedtuple('_Changing', 'parent, oldSize, last, next') + + +class ModelTester: + + """A tester for Qt's QAbstractItemModel's.""" + + def __init__(self): + self._model = None + self._orig_model = None + self._fetching_more = None + self._insert = None + self._remove = None + self._verbose = None + + def setup_and_run(self, model, verbose=False): + """Connect to all of the models signals. + + Whenever anything happens recheck everything. + """ + assert model is not None + self._model = cast(model, QtCore.QAbstractItemModel) + self._orig_model = model + self._fetching_more = False + self._insert = [] + self._remove = [] + self._changing = [] + + self._model.columnsAboutToBeInserted.connect(self.run) + self._model.columnsAboutToBeRemoved.connect(self.run) + self._model.columnsInserted.connect(self.run) + self._model.columnsRemoved.connect(self.run) + self._model.dataChanged.connect(self.run) + self._model.headerDataChanged.connect(self.run) + self._model.layoutAboutToBeChanged.connect(self.run) + self._model.layoutChanged.connect(self.run) + self._model.modelReset.connect(self.run) + self._model.rowsAboutToBeInserted.connect(self.run) + self._model.rowsAboutToBeRemoved.connect(self.run) + self._model.rowsInserted.connect(self.run) + self._model.rowsRemoved.connect(self.run) + + # Special checks for changes + self._model.layoutAboutToBeChanged.connect( + self._on_layout_about_to_be_changed) + self._model.layoutChanged.connect(self._on_layout_changed) + self._model.rowsAboutToBeInserted.connect( + self._on_rows_about_to_be_inserted) + self._model.rowsAboutToBeRemoved.connect( + self._on_rows_about_to_be_removed) + self._model.rowsInserted.connect(self._on_rows_inserted) + self._model.rowsRemoved.connect(self._on_rows_removed) + self._model.dataChanged.connect(self._on_data_changed) + self._model.headerDataChanged.connect(self._on_header_data_changed) + + self.run(verbose=verbose) + + def cleanup(self): + if self._model is None: + return + + self._model.columnsAboutToBeInserted.disconnect(self.run) + self._model.columnsAboutToBeRemoved.disconnect(self.run) + self._model.columnsInserted.disconnect(self.run) + self._model.columnsRemoved.disconnect(self.run) + self._model.dataChanged.disconnect(self.run) + self._model.headerDataChanged.disconnect(self.run) + self._model.layoutAboutToBeChanged.disconnect(self.run) + self._model.layoutChanged.disconnect(self.run) + self._model.modelReset.disconnect(self.run) + self._model.rowsAboutToBeInserted.disconnect(self.run) + self._model.rowsAboutToBeRemoved.disconnect(self.run) + self._model.rowsInserted.disconnect(self.run) + self._model.rowsRemoved.disconnect(self.run) + + self._model.layoutAboutToBeChanged.disconnect( + self._on_layout_about_to_be_changed) + self._model.layoutChanged.disconnect(self._on_layout_changed) + self._model.rowsAboutToBeInserted.disconnect( + self._on_rows_about_to_be_inserted) + self._model.rowsAboutToBeRemoved.disconnect( + self._on_rows_about_to_be_removed) + self._model.rowsInserted.disconnect(self._on_rows_inserted) + self._model.rowsRemoved.disconnect(self._on_rows_removed) + self._model.dataChanged.disconnect(self._on_data_changed) + self._model.headerDataChanged.disconnect(self._on_header_data_changed) + + + def run(self, verbose=False): + self._verbose = verbose + assert self._model is not None + if self._fetching_more: + return + self._test_basic() + self._test_row_count() + self._test_column_count() + self._test_has_index() + self._test_index() + self._test_parent() + self._test_data() + + def _test_basic(self): + """ + Try to call a number of the basic functions (not all). + + Make sure the model doesn't outright segfault, testing the functions + that makes sense. + """ + assert self._model.buddy(QtCore.QModelIndex()) == QtCore.QModelIndex() + self._model.canFetchMore(QtCore.QModelIndex()) + assert self._model.columnCount(QtCore.QModelIndex()) >= 0 + display_data = self._model.data(QtCore.QModelIndex(), + QtCore.Qt.DisplayRole) + assert display_data == QtCore.QVariant() + self._fetching_more = True + self._model.fetchMore(QtCore.QModelIndex()) + self._fetching_more = False + flags = self._model.flags(QtCore.QModelIndex()) + assert flags == QtCore.Qt.ItemIsDropEnabled or not flags + self._model.hasChildren(QtCore.QModelIndex()) + self._model.hasIndex(0, 0) + self._model.headerData(0, QtCore.Qt.Horizontal) + self._model.index(0, 0) + self._model.itemData(QtCore.QModelIndex()) + cache = QtCore.QVariant() + self._model.match(QtCore.QModelIndex(), -1, cache) + self._model.mimeTypes() + assert self._model.parent(QtCore.QModelIndex()) == QtCore.QModelIndex() + assert self._model.rowCount() >= 0 + variant = QtCore.QVariant() + self._model.setData(QtCore.QModelIndex(), variant, -1) + self._model.setHeaderData(-1, QtCore.Qt.Horizontal, QtCore.QVariant()) + self._model.setHeaderData(999999, QtCore.Qt.Horizontal, + QtCore.QVariant()) + self._model.sibling(0, 0, QtCore.QModelIndex()) + self._model.span(QtCore.QModelIndex()) + self._model.supportedDropActions() + + + def _test_row_count(self): + """Test model's implementation of rowCount() and hasChildren(). + + Models that are dynamically populated are not as fully tested here. + """ + # check top row + topIndex = self._model.index(0, 0, QtCore.QModelIndex()) + rows = self._model.rowCount(topIndex) + assert rows >= 0 + if rows > 0: + assert self._model.hasChildren(topIndex) + + secondLevelIndex = self._model.index(0, 0, topIndex) + if secondLevelIndex.isValid(): # not the top level + # check a row count where parent is valid + rows = self._model.rowCount(secondLevelIndex) + assert rows >= 0 + if rows > 0: + assert self._model.hasChildren(secondLevelIndex) + + # The models rowCount() is tested more extensively in + # _check_children(), but this catches the big mistakes + + def _test_column_count(self): + """Test model's implementation of columnCount() and hasChildren().""" + + # check top row + topIndex = self._model.index(0, 0, QtCore.QModelIndex()) + assert self._model.columnCount(topIndex) >= 0 + + # check a column count where parent is valid + childIndex = self._model.index(0, 0, topIndex) + if childIndex.isValid(): + assert self._model.columnCount(childIndex) >= 0 + + # columnCount() is tested more extensively in _check_children(), + # but this catches the big mistakes + + def _test_has_index(self): + """Test model's implementation of hasIndex().""" + # Make sure that invalid values returns an invalid index + assert not self._model.hasIndex(-2, -2) + assert not self._model.hasIndex(-2, 0) + assert not self._model.hasIndex(0, -2) + + rows = self._model.rowCount() + columns = self._model.columnCount() + + # check out of bounds + assert not self._model.hasIndex(rows, columns) + assert not self._model.hasIndex(rows + 1, columns + 1) + + if rows > 0: + assert self._model.hasIndex(0, 0) + + # hasIndex() is tested more extensively in _check_children(), + # but this catches the big mistakes + + def _test_index(self): + """Test model's implementation of index()""" + # Make sure that invalid values returns 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() + + rows = self._model.rowCount() + columns = self._model.columnCount() + + if rows == 0: + return + + # Catch off by one errors + assert self._model.index(rows, columns) == QtCore.QModelIndex() + assert self._model.index(0, 0).isValid() + + # Make sure that the same index is *always* returned + a = self._model.index(0, 0) + b = self._model.index(0, 0) + assert a == b + + # index() is tested more extensively in _check_children(), + # but this catches the big mistakes + + 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._model.parent(QtCore.QModelIndex()) == QtCore.QModelIndex() + + if self._model.rowCount() == 0: + return + + # Column 0 | Column 1 | + # QModelIndex() | | + # \- topIndex | topIndex1 | + # \- childIndex | childIndex1 | + + # Common error test #1, make sure that a top level index has a parent + # that is a invalid QModelIndex. + topIndex = self._model.index(0, 0, QtCore.QModelIndex()) + assert self._model.parent(topIndex) == QtCore.QModelIndex() + + # Common error test #2, make sure that a second level index has a + # parent that is the first level index. + if self._model.rowCount(topIndex) > 0: + childIndex = self._model.index(0, 0, topIndex) + assert self._model.parent(childIndex) == topIndex + + # 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. + topIndex1 = self._model.index(0, 1, QtCore.QModelIndex()) + if self._model.rowCount(topIndex1) > 0: + childIndex = self._model.index(0, 0, topIndex) + childIndex1 = self._model.index(0, 0, topIndex1) + assert childIndex != childIndex1 + + # 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()) + + def _check_children(self, parent, currentDepth=0): + """Check parent/children relationships. + + Called from the parent() test. + + A model that returns an index of parent X should also return X when + asking for the parent of the index. + + This recursive function does pretty extensive testing on the whole + model in an effort to catch edge cases. + + This function assumes that rowCount(), columnCount() and index() + already work. If they have a bug it will point it out, but the above + tests should have already found the basic bugs because it is easier to + figure out the problem in those tests then this one. + """ + + # First just try walking back up the tree. + p = parent + while p.isValid(): + p = p.parent() + + # For models that are dynamically populated + if self._model.canFetchMore(parent): + self._fetching_more = True + self._model.fetchMore(parent) + self._fetching_more = False + + rows = self._model.rowCount(parent) + columns = self._model.columnCount(parent) + + if rows > 0: + assert self._model.hasChildren(parent) + + # Some further testing against rows(), columns(), and hasChildren() + assert rows >= 0 + assert columns >= 0 + if rows > 0: + assert self._model.hasChildren(parent) + + #qDebug() << "parent:" << self._model.data(parent).toString() << "rows:" << rows + # << "columns:" << columns << "parent column:" << parent.column() + + topLeftChild = self._model.index(0, 0, parent) + + assert not self._model.hasIndex(rows + 1, 0, parent) + for r in range(rows): + if self._model.canFetchMore(parent): + self._fetching_more = True + self._model.fetchMore(parent) + self._fetching_more = False + assert not self._model.hasIndex(r, columns + 1, parent) + for c in range(columns): + assert self._model.hasIndex(r, c, parent) + index = self._model.index(r, c, parent) + # rowCount() and columnCount() said that it existed... + assert index.isValid() + + # index() should always return the same index when called twice + # in a row + modifiedIndex = self._model.index(r, c, parent) + assert index == modifiedIndex + + # Make sure we get the same index if we request it twice in a + # row + a = self._model.index(r, c, parent) + b = self._model.index(r, c, parent) + assert a == b + + sibling = self._model.sibling(r, c, topLeftChild) + assert index == sibling + + sibling = topLeftChild.sibling(r, c) + assert index == sibling + + # Some basic checking on the index that is returned + assert index.model() == self._orig_model + assert index.row() == r + assert index.column() == c + # While you can technically return a QVariant usually this is a + # sign of a bug in data(). Disable if this really is ok in + # your model. + # assert self._model.data(index, QtCore.Qt.DisplayRole).isValid() + + # If the next test fails here is some somewhat useful debug you + # play with. + + if self._model.parent(index) != parent: + # FIXME + # qDebug() << r << c << currentDepth << self._model.data(index).toString() + # << self._model.data(parent).toString() + # qDebug() << index << parent << self._model.parent(index) + # And a view that you can even use to show the model. + # QTreeView view + # view.setModel(self._model) + # view.show() + pass + + # Check that we can get back our real parent. + assert self._model.parent(index) == parent + + # recursively go down the children + if self._model.hasChildren(index) and currentDepth < 10: + #qDebug() << r << c << "has children" << self._model.rowCount(index) + self._check_children(index, currentDepth + 1) + # elif currentDepth >= 10: + # print("checked 10 deep") + # FIXME + + # make sure that after testing the children that the index + # doesn't change. + newerIndex = self._model.index(r, c, parent) + assert index == newerIndex + + + def _test_data(self): + """Test model's implementation of data()""" + # Invalid index should return an invalid qvariant + assert self._model.data(QtCore.QModelIndex()) is None + + if self._model.rowCount() == 0: + return + + # A valid index should have a valid QVariant data + 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) + 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), + ] + + # General Purpose roles that should return a QString + for role, typ in types: + data = self._model.data(self._model.index(0, 0), role) + assert data is None or isinstance(data, typ), role + + # Check that the alignment is one we know about + alignment = self._model.data(self._model.index(0, 0), + QtCore.Qt.TextAlignmentRole) + if alignment is not None: + alignment = int(alignment) + mask = int(QtCore.Qt.AlignHorizontal_Mask | + 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] + + def _on_rows_about_to_be_inserted(self, parent, start, end): + """Store what is about to be inserted. + + This gets stored to make sure it actually happens in rowsInserted. + """ + + # Q_UNUSED(end) + # qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << self._model.data(parent).toString() + # << "current count of parent=" << self._model.rowCount(parent); # << "display of last=" << self._model.data(self._model.index(start-1, 0, parent)) + # qDebug() << self._model.index(start-1, 0, parent) << self._model.data(self._model.index(start-1, 0, parent)) + + last_data = self._model.data(self._model.index(start - 1, 0, parent)) + next_data = self._model.data(self._model.index(start, 0, parent)) + c = _Changing(parent=parent, oldSize=self._model.rowCount(parent), + last=last_data, next=next_data) + self._insert.append(c) + + def _on_rows_inserted(self, parent, start, end): + """Confirm that what was said was going to happen actually did.""" + c = self._insert.pop() + assert c.parent == parent + # qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize + # << "parent=" << self._model.data(parent).toString() << "current rowcount of parent=" << self._model.rowCount(parent) + # + # for ii in range(start, end): + # qDebug() << "itemWasInserted:" << ii << self._model.data(self._model.index(ii, 0, parent)) + # qDebug() + + last_data = self._model.data(self._model.index(start - 1, 0, parent)) + + assert c.oldSize + (end - start + 1) == self._model.rowCount(parent) + assert c.last == last_data + + expected = self._model.data(self._model.index(end + 1, 0, c.parent)) + + if c.next != expected: + # FIXME + # qDebug() << start << end + # for i in range(self._model.rowCount(); ++i): + # qDebug() << self._model.index(i, 0).data().toString() + # qDebug() << c.next << self._model.data(self._model.index(end + 1, 0, c.parent)) + pass + + assert c.next == expected + + 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)) + self._changing.append(idx) + + def _on_layout_changed(self): + for p in self._changing: + assert p == self._model.index(p.row(), p.column(), p.parent()) + self._changing.clear() + + def _on_rows_about_to_be_removed(self, parent, start, end): + """Store what is about to be removed to make sure it actually happens. + + This gets stored to make sure it actually happens in rowsRemoved. + """ + last_data = self._model.data(self._model.index(start - 1, 0, parent)) + next_data = self._model.data(self._model.index(end + 1, 0, parent)) + c = _Changing(parent=parent, oldSize=self._model.rowCount(parent), + last=last_data, next=next_data) + self._remove.append(c) + + def _on_rows_removed(self, parent, start, end): + """Confirm that what was said was going to happen actually did.""" + c = self._remove.pop() + last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) + next_data = self._model.data(self._model.index(start, 0, c.parent)) + + assert c.parent == parent + assert c.oldSize - (end - start + 1) == self._model.rowCount(parent) + assert c.last == last_data + assert c.next == next_data + + def _on_data_changed(self, topLeft, bottomRight): + assert topLeft.isValid() + assert bottomRight.isValid() + commonParent = bottomRight.parent() + assert topLeft.parent() == commonParent + assert topLeft.row() <= bottomRight.row() + assert topLeft.column() <= bottomRight.column() + rowCount = self._model.rowCount(commonParent) + columnCount = self._model.columnCount(commonParent) + assert bottomRight.row() < rowCount + assert bottomRight.column() < columnCount + + def _on_header_data_changed(self, orientation, start, end): + assert orientation in [QtCore.Qt.Horizontal, QtCore.Qt.Vertical] + assert start >= 0 + assert end >= 0 + assert start <= end + if orientation == QtCore.Qt.Vertical: + itemCount = self._model.rowCount() + else: + itemCount = self._model.columnCount() + assert start < itemCount + assert end < itemCount diff --git a/pytestqt/plugin.py b/pytestqt/plugin.py index de47aa48..0e22de64 100644 --- a/pytestqt/plugin.py +++ b/pytestqt/plugin.py @@ -15,6 +15,7 @@ from pytestqt.qt_compat import QtCore, QtTest, QApplication, QT_API, \ qInstallMsgHandler, qInstallMessageHandler, QtDebugMsg, QtWarningMsg, \ QtCriticalMsg, QtFatalMsg +from pytestqt.modeltest import ModelTester def _inject_qtest_methods(cls): @@ -557,6 +558,16 @@ def qtbot(qapp, request): result._close() +@pytest.yield_fixture +def qtmodeltester(): + """ + Fixture used to create a ModelTester instance to test models. + """ + tester = ModelTester() + yield tester + tester.cleanup() + + def pytest_addoption(parser): parser.addini('qt_no_exception_capture', 'disable automatic exception capture') diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index bad197c4..fb6b08c7 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -89,7 +89,11 @@ def _import_module(module_name): QWidget = QtGui.QWidget qInstallMsgHandler = QtCore.qInstallMsgHandler + def cast(obj, typ): + raise Exception('TODO') + elif QT_API in ('pyqt4', 'pyqt5'): + import sip Signal = QtCore.pyqtSignal Slot = QtCore.pyqtSlot Property = QtCore.pyqtProperty @@ -104,6 +108,9 @@ def _import_module(module_name): QWidget = QtGui.QWidget qInstallMsgHandler = QtCore.qInstallMsgHandler + def cast(obj, typ): + return sip.cast(obj, typ) + else: # pragma: no cover USING_PYSIDE = True @@ -144,3 +151,6 @@ def __getattr__(cls, name): QtCriticalMsg = Mock() QtFatalMsg = Mock() QT_API = '' + + def cast(obj, typ): + return obj From 9f37d1da87633df46fb33e80747c8912e6d4d68a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Jul 2015 21:23:40 -0300 Subject: [PATCH 02/60] Fix compatibility for PySide and PyQt4 --- pytestqt/modeltest.py | 23 +++++++++++++---------- pytestqt/qt_compat.py | 12 +++++++++++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index acd32f17..2e4338de 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -33,7 +33,7 @@ import collections -from pytestqt.qt_compat import QtCore, QtGui, cast +from pytestqt.qt_compat import QtCore, QtGui, cast, extract_from_variant _Changing = collections.namedtuple('_Changing', 'parent, oldSize, last, next') @@ -149,7 +149,11 @@ def _test_basic(self): assert self._model.columnCount(QtCore.QModelIndex()) >= 0 display_data = self._model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole) - assert display_data == QtCore.QVariant() + + # note: compare against None using "==" on purpose, as depending + # on the Qt API this will be a QVariant object which compares using + # "==" correctly against other Python types, including None + assert display_data == None self._fetching_more = True self._model.fetchMore(QtCore.QModelIndex()) self._fetching_more = False @@ -160,16 +164,14 @@ def _test_basic(self): self._model.headerData(0, QtCore.Qt.Horizontal) self._model.index(0, 0) self._model.itemData(QtCore.QModelIndex()) - cache = QtCore.QVariant() + cache = None self._model.match(QtCore.QModelIndex(), -1, cache) self._model.mimeTypes() assert self._model.parent(QtCore.QModelIndex()) == QtCore.QModelIndex() assert self._model.rowCount() >= 0 - variant = QtCore.QVariant() - self._model.setData(QtCore.QModelIndex(), variant, -1) - self._model.setHeaderData(-1, QtCore.Qt.Horizontal, QtCore.QVariant()) - self._model.setHeaderData(999999, QtCore.Qt.Horizontal, - QtCore.QVariant()) + 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.supportedDropActions() @@ -414,7 +416,7 @@ def _check_children(self, parent, currentDepth=0): def _test_data(self): """Test model's implementation of data()""" # Invalid index should return an invalid qvariant - assert self._model.data(QtCore.QModelIndex()) is None + assert self._model.data(QtCore.QModelIndex()) == None if self._model.rowCount() == 0: return @@ -440,11 +442,12 @@ def _test_data(self): # General Purpose roles that should return a QString for role, typ in types: data = self._model.data(self._model.index(0, 0), role) - assert data is None or isinstance(data, typ), role + assert data == None or isinstance(data, typ), role # 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) if alignment is not None: alignment = int(alignment) mask = int(QtCore.Qt.AlignHorizontal_Mask | diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index fb6b08c7..e3f92bba 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -90,7 +90,12 @@ def _import_module(module_name): qInstallMsgHandler = QtCore.qInstallMsgHandler def cast(obj, typ): - raise Exception('TODO') + """no cast operation is available in PySide""" + return obj + + def extract_from_variant(variant): + """returns python object from the given QVariant""" + return variant elif QT_API in ('pyqt4', 'pyqt5'): import sip @@ -109,8 +114,13 @@ def cast(obj, typ): qInstallMsgHandler = QtCore.qInstallMsgHandler def cast(obj, typ): + """casts from a subclass to a parent class""" return sip.cast(obj, typ) + def extract_from_variant(variant): + """returns python object from the given QVariant""" + return variant.toPyObject() if variant is not None else None + else: # pragma: no cover USING_PYSIDE = True From c218dc8e2fc7de342b79691c6cb63c366fa35b51 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 21 Jul 2015 22:33:05 -0300 Subject: [PATCH 03/60] Adding new qt_compat functions when building docs in rtd --- pytestqt/qt_compat.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index e3f92bba..695df2cb 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -161,6 +161,5 @@ def __getattr__(cls, name): QtCriticalMsg = Mock() QtFatalMsg = Mock() QT_API = '' - - def cast(obj, typ): - return obj + cast = Mock() + extract_from_variant = Mock() From b948eadbc51a030e3b432c960b9c391d6527263a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 31 Aug 2015 21:57:30 -0300 Subject: [PATCH 04/60] Rename methods slightly to ensure a thin public API for ModelTester fixture --- pytestqt/_tests/test_modeltest.py | 4 +- pytestqt/modeltest.py | 68 ++++++++++++++++--------------- pytestqt/plugin.py | 2 +- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/pytestqt/_tests/test_modeltest.py b/pytestqt/_tests/test_modeltest.py index 6b3bf2d4..b4bd651f 100644 --- a/pytestqt/_tests/test_modeltest.py +++ b/pytestqt/_tests/test_modeltest.py @@ -13,7 +13,7 @@ def test_valid_model(qtmodeltester): items[0].setChild(0, items[4]) model.setItem(0, 0, items[0]) model.setItem(0, 1, items[1]) - model.setItem(0, 0, items[2]) + model.setItem(1, 0, items[2]) model.setItem(0, 1, items[3]) - qtmodeltester.setup_and_run(model) + qtmodeltester.check(model) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 2e4338de..82505abb 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -30,7 +30,6 @@ # # $QT_END_LICENSE$ - import collections from pytestqt.qt_compat import QtCore, QtGui, cast, extract_from_variant @@ -51,8 +50,10 @@ def __init__(self): self._remove = None self._verbose = None - def setup_and_run(self, model, verbose=False): - """Connect to all of the models signals. + def check(self, model, verbose=False): + """Runs a series of checks in the given model. + + Connect to all of the models signals. Whenever anything happens recheck everything. """ @@ -64,19 +65,19 @@ def setup_and_run(self, model, verbose=False): self._remove = [] self._changing = [] - self._model.columnsAboutToBeInserted.connect(self.run) - self._model.columnsAboutToBeRemoved.connect(self.run) - self._model.columnsInserted.connect(self.run) - self._model.columnsRemoved.connect(self.run) - self._model.dataChanged.connect(self.run) - self._model.headerDataChanged.connect(self.run) - self._model.layoutAboutToBeChanged.connect(self.run) - self._model.layoutChanged.connect(self.run) - self._model.modelReset.connect(self.run) - self._model.rowsAboutToBeInserted.connect(self.run) - self._model.rowsAboutToBeRemoved.connect(self.run) - self._model.rowsInserted.connect(self.run) - self._model.rowsRemoved.connect(self.run) + self._model.columnsAboutToBeInserted.connect(self._run) + self._model.columnsAboutToBeRemoved.connect(self._run) + self._model.columnsInserted.connect(self._run) + self._model.columnsRemoved.connect(self._run) + self._model.dataChanged.connect(self._run) + self._model.headerDataChanged.connect(self._run) + self._model.layoutAboutToBeChanged.connect(self._run) + self._model.layoutChanged.connect(self._run) + self._model.modelReset.connect(self._run) + self._model.rowsAboutToBeInserted.connect(self._run) + self._model.rowsAboutToBeRemoved.connect(self._run) + self._model.rowsInserted.connect(self._run) + self._model.rowsRemoved.connect(self._run) # Special checks for changes self._model.layoutAboutToBeChanged.connect( @@ -91,25 +92,25 @@ def setup_and_run(self, model, verbose=False): self._model.dataChanged.connect(self._on_data_changed) self._model.headerDataChanged.connect(self._on_header_data_changed) - self.run(verbose=verbose) + self._run(verbose=verbose) - def cleanup(self): + def _cleanup(self): if self._model is None: return - self._model.columnsAboutToBeInserted.disconnect(self.run) - self._model.columnsAboutToBeRemoved.disconnect(self.run) - self._model.columnsInserted.disconnect(self.run) - self._model.columnsRemoved.disconnect(self.run) - self._model.dataChanged.disconnect(self.run) - self._model.headerDataChanged.disconnect(self.run) - self._model.layoutAboutToBeChanged.disconnect(self.run) - self._model.layoutChanged.disconnect(self.run) - self._model.modelReset.disconnect(self.run) - self._model.rowsAboutToBeInserted.disconnect(self.run) - self._model.rowsAboutToBeRemoved.disconnect(self.run) - self._model.rowsInserted.disconnect(self.run) - self._model.rowsRemoved.disconnect(self.run) + self._model.columnsAboutToBeInserted.disconnect(self._run) + self._model.columnsAboutToBeRemoved.disconnect(self._run) + self._model.columnsInserted.disconnect(self._run) + self._model.columnsRemoved.disconnect(self._run) + self._model.dataChanged.disconnect(self._run) + self._model.headerDataChanged.disconnect(self._run) + self._model.layoutAboutToBeChanged.disconnect(self._run) + self._model.layoutChanged.disconnect(self._run) + self._model.modelReset.disconnect(self._run) + self._model.rowsAboutToBeInserted.disconnect(self._run) + self._model.rowsAboutToBeRemoved.disconnect(self._run) + self._model.rowsInserted.disconnect(self._run) + self._model.rowsRemoved.disconnect(self._run) self._model.layoutAboutToBeChanged.disconnect( self._on_layout_about_to_be_changed) @@ -123,8 +124,10 @@ def cleanup(self): self._model.dataChanged.disconnect(self._on_data_changed) self._model.headerDataChanged.disconnect(self._on_header_data_changed) + self._model = None + self._orig_model = None - def run(self, verbose=False): + def _run(self, verbose=False): self._verbose = verbose assert self._model is not None if self._fetching_more: @@ -176,7 +179,6 @@ def _test_basic(self): self._model.span(QtCore.QModelIndex()) self._model.supportedDropActions() - def _test_row_count(self): """Test model's implementation of rowCount() and hasChildren(). diff --git a/pytestqt/plugin.py b/pytestqt/plugin.py index 0e22de64..74df105a 100644 --- a/pytestqt/plugin.py +++ b/pytestqt/plugin.py @@ -565,7 +565,7 @@ def qtmodeltester(): """ tester = ModelTester() yield tester - tester.cleanup() + tester._cleanup() def pytest_addoption(parser): From d1f1b535f60dd70f1ff8cdf129e13ff07b76fe6d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 31 Aug 2015 22:10:36 -0300 Subject: [PATCH 05/60] Add debug() function which uses pytest's verbose config --- pytestqt/modeltest.py | 39 ++++++++++++++++++++++++--------------- pytestqt/plugin.py | 4 ++-- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 82505abb..5f78a27e 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -30,6 +30,7 @@ # # $QT_END_LICENSE$ +from __future__ import print_function import collections from pytestqt.qt_compat import QtCore, QtGui, cast, extract_from_variant @@ -42,13 +43,17 @@ class ModelTester: """A tester for Qt's QAbstractItemModel's.""" - def __init__(self): + def __init__(self, config): self._model = None self._orig_model = None self._fetching_more = None self._insert = None self._remove = None - self._verbose = None + self._verbose = config.getoption('verbose') > 0 + + def _debug(self, *args): + if self._verbose: + print(*args) def check(self, model, verbose=False): """Runs a series of checks in the given model. @@ -340,8 +345,8 @@ def _check_children(self, parent, currentDepth=0): if rows > 0: assert self._model.hasChildren(parent) - #qDebug() << "parent:" << self._model.data(parent).toString() << "rows:" << rows - # << "columns:" << columns << "parent column:" << parent.column() + self._debug("parent:", self._model.data(parent), "rows:", rows, + "columns:", columns, "parent column:", parent.column()) topLeftChild = self._model.index(0, 0, parent) @@ -389,9 +394,9 @@ def _check_children(self, parent, currentDepth=0): if self._model.parent(index) != parent: # FIXME - # qDebug() << r << c << currentDepth << self._model.data(index).toString() - # << self._model.data(parent).toString() - # qDebug() << index << parent << self._model.parent(index) + self._debug(r, c, currentDepth, self._model.data(index), + self._model.data(parent)) + self._debug(index, parent, self._model.parent(index)) # And a view that you can even use to show the model. # QTreeView view # view.setModel(self._model) @@ -403,7 +408,8 @@ def _check_children(self, parent, currentDepth=0): # recursively go down the children if self._model.hasChildren(index) and currentDepth < 10: - #qDebug() << r << c << "has children" << self._model.rowCount(index) + self._debug(r, c, "has children", + self._model.rowCount(index)) self._check_children(index, currentDepth + 1) # elif currentDepth >= 10: # print("checked 10 deep") @@ -469,9 +475,11 @@ def _on_rows_about_to_be_inserted(self, parent, start, end): """ # Q_UNUSED(end) - # qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << self._model.data(parent).toString() - # << "current count of parent=" << self._model.rowCount(parent); # << "display of last=" << self._model.data(self._model.index(start-1, 0, parent)) - # qDebug() << self._model.index(start-1, 0, parent) << self._model.data(self._model.index(start-1, 0, parent)) + self._debug("rowsAboutToBeInserted", "start=", start, "end=", end, "parent=", + self._model.data(parent), "current count of parent=", + self._model.rowCount(parent), "display of last=", + self._model.data(self._model.index(start-1, 0, parent))) + self._debug(self._model.index(start-1, 0, parent), self._model.data(self._model.index(start-1, 0, parent))) last_data = self._model.data(self._model.index(start - 1, 0, parent)) next_data = self._model.data(self._model.index(start, 0, parent)) @@ -499,10 +507,11 @@ def _on_rows_inserted(self, parent, start, end): if c.next != expected: # FIXME - # qDebug() << start << end - # for i in range(self._model.rowCount(); ++i): - # qDebug() << self._model.index(i, 0).data().toString() - # qDebug() << c.next << self._model.data(self._model.index(end + 1, 0, c.parent)) + self._debug(start, end) + for i in xrange(self._model.rowCount()): + self._debug(self._model.index(i, 0).data()) + data = self._model.data(self._model.index(end + 1, 0, c.parent)) + self._debug(c.next, data) pass assert c.next == expected diff --git a/pytestqt/plugin.py b/pytestqt/plugin.py index bd144ae0..e61db0d8 100644 --- a/pytestqt/plugin.py +++ b/pytestqt/plugin.py @@ -589,11 +589,11 @@ def qtbot(qapp, request): @pytest.yield_fixture -def qtmodeltester(): +def qtmodeltester(request): """ Fixture used to create a ModelTester instance to test models. """ - tester = ModelTester() + tester = ModelTester(request.config) yield tester tester._cleanup() From 25c5785a64e58246b97de61097b8c87c8745f418 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 1 Sep 2015 06:42:53 +0200 Subject: [PATCH 06/60] Fix index in test_modeltest.py. --- pytestqt/_tests/test_modeltest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytestqt/_tests/test_modeltest.py b/pytestqt/_tests/test_modeltest.py index b4bd651f..5abf9c6d 100644 --- a/pytestqt/_tests/test_modeltest.py +++ b/pytestqt/_tests/test_modeltest.py @@ -14,6 +14,6 @@ def test_valid_model(qtmodeltester): model.setItem(0, 0, items[0]) model.setItem(0, 1, items[1]) model.setItem(1, 0, items[2]) - model.setItem(0, 1, items[3]) + model.setItem(1, 1, items[3]) qtmodeltester.check(model) From 7447a22d556793451282bb3ef52daed50e1906ca Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 7 Sep 2015 21:05:11 -0300 Subject: [PATCH 07/60] Add more tests for qtmodeltester --- pytestqt/modeltest.py | 18 ++++++++++-------- pytestqt/qt_compat.py | 20 ++++++++++++++++++++ tests/test_modeltest.py | 40 ++++++++++++++++++++++++++++++++++------ 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 5f78a27e..f93373b8 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -1,5 +1,6 @@ -# This file is based on the original C++ modeltest.cpp, licensed under -# the following terms: +# This file is based on the original C++ modeltest.cpp from: +# http://code.qt.io/cgit/qt/qtbase.git/tree/tests/auto/other/modeltest/modeltest.cpp +# Licensed under the following terms: # # Copyright (C) 2015 The Qt Company Ltd. # Contact: http://www.qt.io/licensing/ @@ -491,12 +492,13 @@ def _on_rows_inserted(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" c = self._insert.pop() assert c.parent == parent - # qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize - # << "parent=" << self._model.data(parent).toString() << "current rowcount of parent=" << self._model.rowCount(parent) - # - # for ii in range(start, end): - # qDebug() << "itemWasInserted:" << ii << self._model.data(self._model.index(ii, 0, parent)) - # qDebug() + self._debug("rowsInserted", "start=", start, "end=", end, "oldsize=", + c.oldSize, "parent=", self._model.data(parent), + "current rowcount of parent=", self._model.rowCount(parent)) + for ii in range(start, end): + self._debug("itemWasInserted:", ii, + self._model.data(self._model.index(ii, 0, parent))) + self._debug() last_data = self._model.data(self._model.index(start - 1, 0, parent)) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index d812c572..01229c58 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -94,6 +94,12 @@ def _import_module(module_name): QWidget = QtGui.QWidget qInstallMsgHandler = QtCore.qInstallMsgHandler + QStandardItem = QtGui.QStandardItem + QStandardItemModel = QtGui.QStandardItemModel + QFileSystemModel = QtGui.QFileSystemModel + QStringListModel = QtGui.QStringListModel + QSortFilterProxyModel = QtGui.QSortFilterProxyModel + def cast(obj, typ): """no cast operation is available in PySide""" return obj @@ -118,12 +124,26 @@ def get_versions(): QWidget = _QtWidgets.QWidget qInstallMessageHandler = QtCore.qInstallMessageHandler qt_api_name = 'PyQt5' + + QStandardItem = QtGui.QStandardItem + QStandardItemModel = QtGui.QStandardItemModel + QFileSystemModel = _QtWidgets.QFileSystemModel + QStringListModel = QtCore.QStringListModel + QSortFilterProxyModel = QtCore.QSortFilterProxyModel + else: QApplication = QtGui.QApplication QWidget = QtGui.QWidget qInstallMsgHandler = QtCore.qInstallMsgHandler qt_api_name = 'PyQt4' + QStandardItem = QtGui.QStandardItem + QStandardItemModel = QtGui.QStandardItemModel + QFileSystemModel = QtGui.QFileSystemModel + QStringListModel = QtGui.QStringListModel + QSortFilterProxyModel = QtGui.QSortFilterProxyModel + + def get_versions(): return VersionTuple(qt_api_name, QtCore.PYQT_VERSION_STR, QtCore.qVersion(), QtCore.QT_VERSION_STR) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 7f22f541..d3b55f39 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -1,15 +1,16 @@ import pytest -from pytestqt.qt_compat import QtGui +from pytestqt.qt_compat import QStandardItemModel, QStandardItem, \ + QFileSystemModel, QStringListModel, QSortFilterProxyModel, QT_API +pytestmark = pytest.mark.usefixtures('qtbot') -def test_valid_model(qtmodeltester): + +def test_standard_item_model(qtmodeltester): """ Basic test which uses qtmodeltester with a QStandardItemModel. """ - model = QtGui.QStandardItemModel() - - items = [QtGui.QStandardItem(str(i)) for i in range(5)] - + model = QStandardItemModel() + items = [QStandardItem(str(i)) for i in range(5)] items[0].setChild(0, items[4]) model.setItem(0, 0, items[0]) model.setItem(0, 1, items[1]) @@ -17,3 +18,30 @@ def test_valid_model(qtmodeltester): model.setItem(1, 1, items[3]) qtmodeltester.check(model) + + +def test_file_system_model(qtmodeltester, tmpdir): + tmpdir.ensure('directory', dir=1) + tmpdir.ensure('file1.txt', dir=1) + tmpdir.ensure('file2.py', dir=1) + model = QFileSystemModel() + model.setRootPath(str(tmpdir)) + qtmodeltester.check(model) + + +@pytest.mark.skipif(QT_API == 'pyside', reason='For some reason this fails in ' + 'PySide with a message about' + 'columnCount being private') +def test_string_list_model(qtmodeltester): + model = QStringListModel() + model.setStringList(['hello', 'world']) + qtmodeltester.check(model) + + +def test_sort_filter_proxy_model(qtmodeltester): + model = QStringListModel() + model.setStringList(['hello', 'world']) + proxy = QSortFilterProxyModel() + proxy.setSourceModel(model) + qtmodeltester.check(proxy) + From 768209c2a307c07b4c73d972e23b636e00cd480f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 7 Sep 2015 21:23:24 -0300 Subject: [PATCH 08/60] Add docs for qtmodeltester and CHANGELOG --- CHANGELOG.md | 4 ++++ docs/index.rst | 1 + docs/modeltester.rst | 45 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 docs/modeltester.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 27af8672..d767b738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 1.6.0.dev # +- New `qtmodeltester` fixture to test `QAbstractItemModel` subclasses. + Thanks @The-Compiler for the initiative and port of the original C++ code + for ModelTester (#63). + - Reduced verbosity when exceptions are captured in virtual methods (#77, thanks @The-Compiler). diff --git a/docs/index.rst b/docs/index.rst index 5f7cdec7..2dcce5a3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,6 +21,7 @@ pytest-qt logging signals virtual_methods + modeltester app_exit reference diff --git a/docs/modeltester.rst b/docs/modeltester.rst new file mode 100644 index 00000000..f2cb64c8 --- /dev/null +++ b/docs/modeltester.rst @@ -0,0 +1,45 @@ +Model Tester +============ + +.. versionadded:: 1.6 + +``pytest-qt`` includes a fixture that helps testing +`QAbstractItemModel`_ implementations. The implementation is copied +from the C++ code as described on the `Qt Wiki `_, +and it continuously checks a model as it changes, helping to verify the state +and catching many common errors the moment they show up. + +Some of the conditions caught include: + +* Verifying X number of rows have been inserted in the correct place after the signal rowsAboutToBeInserted() says X rows will be inserted. +* The parent of the first index of the first row is a QModelIndex() +* Calling index() twice in a row with the same values will return the same QModelIndex +* If rowCount() says there are X number of rows, model test will verify that is true. +* Many possible off by one bugs +* hasChildren() returns true if rowCount() is greater then zero. +* and many more... + +To use it, create a instance of your model implementation, fill it with some +items and call ``qtmodeltester.check``: + +.. code-block:: python + + def test_standard_item_model(qtmodeltester): + model = QStandardItemModel() + items = [QStandardItem(str(i)) for i in range(4)] + model.setItem(0, 0, items[0]) + model.setItem(0, 1, items[1]) + model.setItem(1, 0, items[2]) + model.setItem(1, 1, items[3]) + qtmodeltester.check(model) + +If the tester finds a problem the test will fail with an assert. + +The source code was ported from `modeltest.cpp`_ by `Florian Bruhin`_, many +thanks! + +.. _modeltest.cpp: http://code.qt.io/cgit/qt/qtbase.git/tree/tests/auto/other/modeltest/modeltest.cpp + +.. _Florian Bruhin: https://github.com/The-Compiler + +.. _QAbstractItemModel: http://doc.qt.io/qt-5/qabstractitemmodel.html \ No newline at end of file From 788212237fd317d3b34f1ae662132ebb55cfd68e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 7 Sep 2015 22:58:50 -0300 Subject: [PATCH 09/60] Pass USER and USERNAME in tox, required by pytest's master --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d45c84a2..cb1eb064 100644 --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ setenv= pyqt4: PYTEST_QT_API=pyqt4 pyqt5: PYTEST_QT_API=pyqt5 pyqt5: QT_QPA_PLATFORM_PLUGIN_PATH={envdir}/Lib/site-packages/PyQt5/plugins/platforms -passenv=DISPLAY XAUTHORITY +passenv=DISPLAY XAUTHORITY USER USERNAME [testenv:docs] basepython=python2.7 From c63301687a3be9690b93a1e0a11c503665ff4da2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 7 Sep 2015 23:19:31 -0300 Subject: [PATCH 10/60] Increase tolerance for timeout checking in test_wait_signal AppVeyor is failing sometimes by reaching the timeout by a few miliseconds :( --- tests/test_wait_signal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wait_signal.py b/tests/test_wait_signal.py index b6d891d1..4cdfda2e 100644 --- a/tests/test_wait_signal.py +++ b/tests/test_wait_signal.py @@ -234,7 +234,7 @@ def check(self, timeout, *delays): delays used to trigger a signal has passed. """ if timeout is None: - timeout = max(delays) * 1.30 # 30% tolerance + timeout = max(delays) * 1.35 # 35% tolerance max_wait_ms = max(delays + (timeout,)) elapsed_ms = (get_time() - self._start_time) * 1000.0 assert elapsed_ms < max_wait_ms From 9c7c22f73c06801d358a1dc930ef141616e12e7f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 7 Sep 2015 23:38:32 -0300 Subject: [PATCH 11/60] Change models after calling check() to improve coverage --- tests/test_modeltest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index d3b55f39..866b67d4 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -22,11 +22,13 @@ def test_standard_item_model(qtmodeltester): def test_file_system_model(qtmodeltester, tmpdir): tmpdir.ensure('directory', dir=1) - tmpdir.ensure('file1.txt', dir=1) - tmpdir.ensure('file2.py', dir=1) + tmpdir.ensure('file1.txt') + tmpdir.ensure('file2.py') model = QFileSystemModel() model.setRootPath(str(tmpdir)) qtmodeltester.check(model) + tmpdir.ensure('file3.py') + qtmodeltester.check(model) @pytest.mark.skipif(QT_API == 'pyside', reason='For some reason this fails in ' From 52e6477cd3d4ec769a35dde233cd1aef37942b2b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 8 Sep 2015 19:49:35 -0300 Subject: [PATCH 12/60] Add `` around method names in modeltester's documentation --- docs/modeltester.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/modeltester.rst b/docs/modeltester.rst index f2cb64c8..ddfa6417 100644 --- a/docs/modeltester.rst +++ b/docs/modeltester.rst @@ -11,12 +11,12 @@ and catching many common errors the moment they show up. Some of the conditions caught include: -* Verifying X number of rows have been inserted in the correct place after the signal rowsAboutToBeInserted() says X rows will be inserted. -* The parent of the first index of the first row is a QModelIndex() -* Calling index() twice in a row with the same values will return the same QModelIndex -* If rowCount() says there are X number of rows, model test will verify that is true. +* Verifying X number of rows have been inserted in the correct place after the signal ``rowsAboutToBeInserted()`` says X rows will be inserted. +* The parent of the first index of the first row is a ``QModelIndex()`` +* Calling ``index()`` twice in a row with the same values will return the same ``QModelIndex`` +* If ``rowCount()`` says there are X number of rows, model test will verify that is true. * Many possible off by one bugs -* hasChildren() returns true if rowCount() is greater then zero. +* ``hasChildren()`` returns true if ``rowCount()`` is greater then zero. * and many more... To use it, create a instance of your model implementation, fill it with some From 19c3411b570232c166fd903997a2a699e3b9ce0f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 8 Sep 2015 20:48:23 -0300 Subject: [PATCH 13/60] Add `test_broken_roles` as suggested by @The-Compiler --- pytestqt/qt_compat.py | 3 +++ tests/test_modeltest.py | 44 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 01229c58..44fe719f 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -99,6 +99,7 @@ def _import_module(module_name): QFileSystemModel = QtGui.QFileSystemModel QStringListModel = QtGui.QStringListModel QSortFilterProxyModel = QtGui.QSortFilterProxyModel + QAbstractListModel = QtCore.QAbstractListModel def cast(obj, typ): """no cast operation is available in PySide""" @@ -130,6 +131,7 @@ def get_versions(): QFileSystemModel = _QtWidgets.QFileSystemModel QStringListModel = QtCore.QStringListModel QSortFilterProxyModel = QtCore.QSortFilterProxyModel + QAbstractListModel = QtCore.QAbstractListModel else: QApplication = QtGui.QApplication @@ -142,6 +144,7 @@ def get_versions(): QFileSystemModel = QtGui.QFileSystemModel QStringListModel = QtGui.QStringListModel QSortFilterProxyModel = QtGui.QSortFilterProxyModel + QAbstractListModel = QtCore.QAbstractListModel def get_versions(): diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 866b67d4..fe12a7a2 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -1,6 +1,6 @@ import pytest from pytestqt.qt_compat import QStandardItemModel, QStandardItem, \ - QFileSystemModel, QStringListModel, QSortFilterProxyModel, QT_API + QFileSystemModel, QStringListModel, QSortFilterProxyModel, QT_API, QtCore pytestmark = pytest.mark.usefixtures('qtbot') @@ -47,3 +47,45 @@ def test_sort_filter_proxy_model(qtmodeltester): 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 +]) +def test_broken_types(testdir, broken_role): + """ + Check that qtmodeltester correctly captures data() returning invalid + values for various display roles. + """ + testdir.makepyfile(''' + from pytestqt.qt_compat import QAbstractListModel, QtCore + + invalid_obj = object() # This will fail the type check for any role + + class BrokenTypeModel(QAbstractListModel): + + def rowCount(self, parent=QtCore.QModelIndex()): + if parent == QtCore.QModelIndex(): + return 1 + else: + return 0 + + def data(self, index=QtCore.QModelIndex(), + role=QtCore.Qt.DisplayRole): + if role == {broken_role}: + return invalid_obj + else: + return None + + def test_broken_type(qtmodeltester): + model = BrokenTypeModel() + qtmodeltester.check(model) + + def test_passing(): + # Sanity test to make sure the imports etc. are right + pass + '''.format(broken_role=broken_role)) + res = testdir.inline_run() + res.assertoutcome(passed=1, failed=1) From b180176e27586c869c286468ecb0f5dfd67964d6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 8 Sep 2015 20:53:22 -0300 Subject: [PATCH 14/60] Refactor some common PyQt4 and PyQt5 imports --- pytestqt/qt_compat.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 44fe719f..1ad318e6 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -120,32 +120,30 @@ def get_versions(): Property = QtCore.pyqtProperty if QT_API == 'pyqt5': + qt_api_name = 'PyQt5' + _QtWidgets = _import_module('QtWidgets') QApplication = _QtWidgets.QApplication QWidget = _QtWidgets.QWidget qInstallMessageHandler = QtCore.qInstallMessageHandler - qt_api_name = 'PyQt5' - QStandardItem = QtGui.QStandardItem - QStandardItemModel = QtGui.QStandardItemModel QFileSystemModel = _QtWidgets.QFileSystemModel QStringListModel = QtCore.QStringListModel QSortFilterProxyModel = QtCore.QSortFilterProxyModel - QAbstractListModel = QtCore.QAbstractListModel - else: + qt_api_name = 'PyQt4' + QApplication = QtGui.QApplication QWidget = QtGui.QWidget qInstallMsgHandler = QtCore.qInstallMsgHandler - qt_api_name = 'PyQt4' - QStandardItem = QtGui.QStandardItem - QStandardItemModel = QtGui.QStandardItemModel QFileSystemModel = QtGui.QFileSystemModel QStringListModel = QtGui.QStringListModel QSortFilterProxyModel = QtGui.QSortFilterProxyModel - QAbstractListModel = QtCore.QAbstractListModel + QStandardItem = QtGui.QStandardItem + QStandardItemModel = QtGui.QStandardItemModel + QAbstractListModel = QtCore.QAbstractListModel def get_versions(): return VersionTuple(qt_api_name, QtCore.PYQT_VERSION_STR, From f60fbf9acfcbade268df1a8585136d197614b41a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 8 Sep 2015 21:29:23 -0300 Subject: [PATCH 15/60] Add check that data() doesn't return QVariant, unless the user explicitly sets data_may_return_qvariant to True --- pytestqt/modeltest.py | 8 +++++++- tests/test_modeltest.py | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index f93373b8..444f5382 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -51,6 +51,7 @@ def __init__(self, config): self._insert = None self._remove = None self._verbose = config.getoption('verbose') > 0 + self.data_may_return_qvariant = False def _debug(self, *args): if self._verbose: @@ -388,7 +389,12 @@ def _check_children(self, parent, currentDepth=0): # While you can technically return a QVariant usually this is a # sign of a bug in data(). Disable if this really is ok in # your model. - # assert self._model.data(index, QtCore.Qt.DisplayRole).isValid() + data = self._model.data(index, QtCore.Qt.DisplayRole) + is_qvariant = type(data).__name__ == 'QVariant' + if self.data_may_return_qvariant and is_qvariant: + assert data.isValid() + elif not self.data_may_return_qvariant: + assert not is_qvariant # If the next test fails here is some somewhat useful debug you # play with. diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index fe12a7a2..286de33c 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -5,6 +5,14 @@ pytestmark = pytest.mark.usefixtures('qtbot') +@pytest.fixture(autouse=True) +def default_model_implementations_return_qvariant(qtmodeltester): + """PyQt4 is the only implementation where the builtin model implementations + may return QVariant objects. + """ + qtmodeltester.data_may_return_qvariant = QT_API == 'pyqt4' + + def test_standard_item_model(qtmodeltester): """ Basic test which uses qtmodeltester with a QStandardItemModel. From 803f228524c46e7ff5c9eb22eda31cd1eaa04ea2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 8 Sep 2015 21:53:04 -0300 Subject: [PATCH 16/60] Test qvariant related functions from qt_compat across all Qt bindings --- pytestqt/qt_compat.py | 31 +++++++++++++++++++++++++++---- tests/test_basics.py | 17 ++++++++++++++++- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 1ad318e6..df1577df 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -106,9 +106,13 @@ def cast(obj, typ): return obj def extract_from_variant(variant): - """returns python object from the given QVariant""" + """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__) @@ -130,6 +134,12 @@ def get_versions(): QFileSystemModel = _QtWidgets.QFileSystemModel QStringListModel = QtCore.QStringListModel QSortFilterProxyModel = QtCore.QSortFilterProxyModel + + def extract_from_variant(variant): + """returns python object from the given QVariant""" + if isinstance(variant, QtCore.QVariant): + return variant.value() + return variant else: qt_api_name = 'PyQt4' @@ -141,6 +151,12 @@ def get_versions(): QStringListModel = QtGui.QStringListModel 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 + QStandardItem = QtGui.QStandardItem QStandardItemModel = QtGui.QStandardItemModel QAbstractListModel = QtCore.QAbstractListModel @@ -153,9 +169,16 @@ def cast(obj, typ): """casts from a subclass to a parent class""" return sip.cast(obj, typ) - def extract_from_variant(variant): - """returns python object from the given QVariant""" - return variant.toPyObject() if variant is not None else None + def make_variant(value=None): + """Return a QVariant object from the given Python builtin""" + import sys + # PyQt4 on py3 doesn't allow one to instantiate any QVariant at + # all: + # QVariant represents a mapped type and cannot be instantiated + # --' + if QT_API == 'pyqt4' and sys.version_info[0] == 3: + return value + return QtCore.QVariant(value) else: # pragma: no cover USING_PYSIDE = True diff --git a/tests/test_basics.py b/tests/test_basics.py index 7603c7fd..cc7d4241 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -1,7 +1,7 @@ import weakref import pytest from pytestqt.qt_compat import QtGui, Qt, QEvent, QtCore, QApplication, \ - QWidget + QWidget, make_variant, extract_from_variant def test_basics(qtbot): @@ -171,6 +171,21 @@ def test_public_api_backward_compatibility(): assert pytestqt.plugin.Record +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()) + + 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 + + class EventRecorder(QWidget): """ From f6b44bc538a807ffda83119430b953b6e59cdbee Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 8 Sep 2015 23:02:55 -0300 Subject: [PATCH 17/60] Add model test for Alignment role in data() method --- tests/test_modeltest.py | 53 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 286de33c..eec3456c 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -4,6 +4,12 @@ pytestmark = pytest.mark.usefixtures('qtbot') +skip_due_to_pyside_private_methods = pytest.mark.skipif( + QT_API == 'pyside', reason='Skip tests that work with QAbstractItemModel ' + 'subclasses on PySide because methods that ' + 'should be public are private. See' + 'PySide/PySide#127.') + @pytest.fixture(autouse=True) def default_model_implementations_return_qvariant(qtmodeltester): @@ -39,9 +45,7 @@ def test_file_system_model(qtmodeltester, tmpdir): qtmodeltester.check(model) -@pytest.mark.skipif(QT_API == 'pyside', reason='For some reason this fails in ' - 'PySide with a message about' - 'columnCount being private') +@skip_due_to_pyside_private_methods def test_string_list_model(qtmodeltester): model = QStringListModel() model.setStringList(['hello', 'world']) @@ -62,6 +66,7 @@ def test_sort_filter_proxy_model(qtmodeltester): QtCore.Qt.TextColorRole, QtCore.Qt.TextAlignmentRole, QtCore.Qt.CheckStateRole ]) +@skip_due_to_pyside_private_methods def test_broken_types(testdir, broken_role): """ Check that qtmodeltester correctly captures data() returning invalid @@ -97,3 +102,45 @@ def test_passing(): '''.format(broken_role=broken_role)) res = testdir.inline_run() res.assertoutcome(passed=1, failed=1) + + +@pytest.mark.parametrize('role, should_pass', [ + (QtCore.Qt.AlignLeft, True), + (QtCore.Qt.AlignRight, True), + (0xFFFFFF, False), +]) +@skip_due_to_pyside_private_methods +def test_data_alignment(testdir, role, should_pass): + """Test a custom model which returns a good and alignments from data(). + qtmodeltest should capture this problem and fail when that happens. + """ + testdir.makepyfile(''' + from pytestqt.qt_compat import QAbstractListModel, QtCore + + invalid_obj = object() # This will fail the type check for any role + + class MyModel(QAbstractListModel): + + def rowCount(self, parent=QtCore.QModelIndex()): + return 1 if parent == QtCore.QModelIndex() else 0 + + def columnCount(self, parent=QtCore.QModelIndex()): + return 1 if parent == QtCore.QModelIndex() else 0 + + def data(self, index=QtCore.QModelIndex(), + role=QtCore.Qt.DisplayRole): + if role == QtCore.Qt.TextAlignmentRole: + return {role} + else: + if role == QtCore.Qt.DisplayRole and index == \ + self.index(0, 0): + return 'Hello' + return None + + def test_broken_alignment(qtmodeltester): + model = MyModel() + qtmodeltester.data_may_return_qvariant = True + qtmodeltester.check(model) + '''.format(role=role)) + res = testdir.inline_run() + res.assertoutcome(passed=int(should_pass), failed=int(not should_pass)) From 38fa79cafaaccd4c653783c4407fc9fc327bf71c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 9 Sep 2015 21:40:17 -0300 Subject: [PATCH 18/60] Implement workarounds for private model methods in PySide --- pytestqt/modeltest.py | 86 ++++++++++++++++++++++++++++------------- pytestqt/qt_compat.py | 2 + tests/test_modeltest.py | 26 +++++-------- 3 files changed, 72 insertions(+), 42 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 444f5382..5d590327 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -34,7 +34,8 @@ from __future__ import print_function import collections -from pytestqt.qt_compat import QtCore, QtGui, cast, extract_from_variant +from pytestqt.qt_compat import QtCore, QtGui, cast, extract_from_variant, \ + QAbstractListModel, QAbstractTableModel, USING_PYSIDE _Changing = collections.namedtuple('_Changing', 'parent, oldSize, last, next') @@ -156,7 +157,7 @@ def _test_basic(self): """ assert self._model.buddy(QtCore.QModelIndex()) == QtCore.QModelIndex() self._model.canFetchMore(QtCore.QModelIndex()) - assert self._model.columnCount(QtCore.QModelIndex()) >= 0 + assert self._column_count(QtCore.QModelIndex()) >= 0 display_data = self._model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole) @@ -169,7 +170,7 @@ def _test_basic(self): self._fetching_more = False flags = self._model.flags(QtCore.QModelIndex()) assert flags == QtCore.Qt.ItemIsDropEnabled or not flags - self._model.hasChildren(QtCore.QModelIndex()) + self._has_children(QtCore.QModelIndex()) self._model.hasIndex(0, 0) self._model.headerData(0, QtCore.Qt.Horizontal) self._model.index(0, 0) @@ -177,7 +178,7 @@ def _test_basic(self): cache = None self._model.match(QtCore.QModelIndex(), -1, cache) self._model.mimeTypes() - assert self._model.parent(QtCore.QModelIndex()) == QtCore.QModelIndex() + assert self._parent(QtCore.QModelIndex()) == QtCore.QModelIndex() assert self._model.rowCount() >= 0 self._model.setData(QtCore.QModelIndex(), None, -1) self._model.setHeaderData(-1, QtCore.Qt.Horizontal, None) @@ -196,7 +197,7 @@ def _test_row_count(self): rows = self._model.rowCount(topIndex) assert rows >= 0 if rows > 0: - assert self._model.hasChildren(topIndex) + assert self._has_children(topIndex) secondLevelIndex = self._model.index(0, 0, topIndex) if secondLevelIndex.isValid(): # not the top level @@ -204,7 +205,7 @@ def _test_row_count(self): rows = self._model.rowCount(secondLevelIndex) assert rows >= 0 if rows > 0: - assert self._model.hasChildren(secondLevelIndex) + assert self._has_children(secondLevelIndex) # The models rowCount() is tested more extensively in # _check_children(), but this catches the big mistakes @@ -214,12 +215,12 @@ def _test_column_count(self): # check top row topIndex = self._model.index(0, 0, QtCore.QModelIndex()) - assert self._model.columnCount(topIndex) >= 0 + assert self._column_count(topIndex) >= 0 # check a column count where parent is valid childIndex = self._model.index(0, 0, topIndex) if childIndex.isValid(): - assert self._model.columnCount(childIndex) >= 0 + assert self._column_count(childIndex) >= 0 # columnCount() is tested more extensively in _check_children(), # but this catches the big mistakes @@ -232,7 +233,7 @@ def _test_has_index(self): assert not self._model.hasIndex(0, -2) rows = self._model.rowCount() - columns = self._model.columnCount() + columns = self._column_count() # check out of bounds assert not self._model.hasIndex(rows, columns) @@ -252,7 +253,7 @@ def _test_index(self): assert self._model.index(0, -2) == QtCore.QModelIndex() rows = self._model.rowCount() - columns = self._model.columnCount() + columns = self._column_count() if rows == 0: return @@ -273,7 +274,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._model.parent(QtCore.QModelIndex()) == QtCore.QModelIndex() + assert self._parent(QtCore.QModelIndex()) == QtCore.QModelIndex() if self._model.rowCount() == 0: return @@ -286,13 +287,13 @@ def _test_parent(self): # Common error test #1, make sure that a top level index has a parent # that is a invalid QModelIndex. topIndex = self._model.index(0, 0, QtCore.QModelIndex()) - assert self._model.parent(topIndex) == QtCore.QModelIndex() + assert self._parent(topIndex) == QtCore.QModelIndex() # Common error test #2, make sure that a second level index has a # parent that is the first level index. if self._model.rowCount(topIndex) > 0: childIndex = self._model.index(0, 0, topIndex) - assert self._model.parent(childIndex) == topIndex + assert self._parent(childIndex) == topIndex # Common error test #3, the second column should NOT have the same # children as the first column in a row. @@ -336,19 +337,20 @@ def _check_children(self, parent, currentDepth=0): self._fetching_more = False rows = self._model.rowCount(parent) - columns = self._model.columnCount(parent) + columns = self._column_count(parent) if rows > 0: - assert self._model.hasChildren(parent) + assert self._has_children(parent) # Some further testing against rows(), columns(), and hasChildren() assert rows >= 0 assert columns >= 0 if rows > 0: - assert self._model.hasChildren(parent) + assert self._has_children(parent) - self._debug("parent:", self._model.data(parent), "rows:", rows, - "columns:", columns, "parent column:", parent.column()) + self._debug("parent:", self._model.data(parent, QtCore.Qt.DisplayRole), + "rows:", rows, "columns:", columns, "parent column:", + parent.column()) topLeftChild = self._model.index(0, 0, parent) @@ -399,11 +401,11 @@ def _check_children(self, parent, currentDepth=0): # If the next test fails here is some somewhat useful debug you # play with. - if self._model.parent(index) != parent: + if self._parent(index) != parent: # FIXME self._debug(r, c, currentDepth, self._model.data(index), self._model.data(parent)) - self._debug(index, parent, self._model.parent(index)) + self._debug(index, parent, self._parent(index)) # And a view that you can even use to show the model. # QTreeView view # view.setModel(self._model) @@ -411,10 +413,10 @@ def _check_children(self, parent, currentDepth=0): pass # Check that we can get back our real parent. - assert self._model.parent(index) == parent + assert self._parent(index) == parent # recursively go down the children - if self._model.hasChildren(index) and currentDepth < 10: + if self._has_children(index) and currentDepth < 10: self._debug(r, c, "has children", self._model.rowCount(index)) self._check_children(index, currentDepth + 1) @@ -427,11 +429,11 @@ def _check_children(self, parent, currentDepth=0): newerIndex = self._model.index(r, c, parent) assert index == newerIndex - def _test_data(self): """Test model's implementation of data()""" # Invalid index should return an invalid qvariant - assert self._model.data(QtCore.QModelIndex()) == None + value = self._model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole) + assert extract_from_variant(value) is None if self._model.rowCount() == 0: return @@ -564,7 +566,7 @@ def _on_data_changed(self, topLeft, bottomRight): assert topLeft.row() <= bottomRight.row() assert topLeft.column() <= bottomRight.column() rowCount = self._model.rowCount(commonParent) - columnCount = self._model.columnCount(commonParent) + columnCount = self._column_count(commonParent) assert bottomRight.row() < rowCount assert bottomRight.column() < columnCount @@ -576,6 +578,38 @@ def _on_header_data_changed(self, orientation, start, end): if orientation == QtCore.Qt.Vertical: itemCount = self._model.rowCount() else: - itemCount = self._model.columnCount() + itemCount = self._column_count() assert start < itemCount assert end < itemCount + + def _column_count(self, parent=QtCore.QModelIndex()): + """ + Workaround for the fact that ``columnCount`` is a private method in + QAbstractListModel/QAbstractTableModel subclasses and PySide does not + provide any way to work around that limitation. PyQt lets us "cast" + back to the super class to access these private methods. + """ + if isinstance(self._orig_model, QAbstractListModel) and USING_PYSIDE: + return 1 if parent == QtCore.QModelIndex() else 0 + else: + return self._model.columnCount(parent) + + def _parent(self, index): + """ + .. see:: ``_column_count`` + """ + model_types = (QAbstractListModel, QAbstractTableModel) + if isinstance(self._orig_model, model_types) and USING_PYSIDE: + return QtCore.QModelIndex() + else: + return self._model.parent(index) + + def _has_children(self, parent=QtCore.QModelIndex()): + """ + .. see:: ``_column_count`` + """ + model_types = (QAbstractListModel, QAbstractTableModel) + if isinstance(self._orig_model, model_types) and USING_PYSIDE: + return parent == QtCore.QModelIndex() and self._model.rowCount() > 0 + else: + return self._model.hasChildren(parent) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index df1577df..4e5f4101 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -100,6 +100,7 @@ def _import_module(module_name): QStringListModel = QtGui.QStringListModel QSortFilterProxyModel = QtGui.QSortFilterProxyModel QAbstractListModel = QtCore.QAbstractListModel + QAbstractTableModel = QtCore.QAbstractTableModel def cast(obj, typ): """no cast operation is available in PySide""" @@ -160,6 +161,7 @@ def extract_from_variant(variant): QStandardItem = QtGui.QStandardItem QStandardItemModel = QtGui.QStandardItemModel QAbstractListModel = QtCore.QAbstractListModel + QAbstractTableModel = QtCore.QAbstractTableModel def get_versions(): return VersionTuple(qt_api_name, QtCore.PYQT_VERSION_STR, diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index eec3456c..9f4f108b 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -1,15 +1,10 @@ import pytest + from pytestqt.qt_compat import QStandardItemModel, QStandardItem, \ - QFileSystemModel, QStringListModel, QSortFilterProxyModel, QT_API, QtCore + QFileSystemModel, QStringListModel, QSortFilterProxyModel, QT_API pytestmark = pytest.mark.usefixtures('qtbot') -skip_due_to_pyside_private_methods = pytest.mark.skipif( - QT_API == 'pyside', reason='Skip tests that work with QAbstractItemModel ' - 'subclasses on PySide because methods that ' - 'should be public are private. See' - 'PySide/PySide#127.') - @pytest.fixture(autouse=True) def default_model_implementations_return_qvariant(qtmodeltester): @@ -45,7 +40,6 @@ def test_file_system_model(qtmodeltester, tmpdir): qtmodeltester.check(model) -@skip_due_to_pyside_private_methods def test_string_list_model(qtmodeltester): model = QStringListModel() model.setStringList(['hello', 'world']) @@ -61,12 +55,13 @@ def test_sort_filter_proxy_model(qtmodeltester): @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 + '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', ]) -@skip_due_to_pyside_private_methods def test_broken_types(testdir, broken_role): """ Check that qtmodeltester correctly captures data() returning invalid @@ -105,11 +100,10 @@ def test_passing(): @pytest.mark.parametrize('role, should_pass', [ - (QtCore.Qt.AlignLeft, True), - (QtCore.Qt.AlignRight, True), + ('QtCore.Qt.AlignLeft', True), + ('QtCore.Qt.AlignRight', True), (0xFFFFFF, False), ]) -@skip_due_to_pyside_private_methods def test_data_alignment(testdir, role, should_pass): """Test a custom model which returns a good and alignments from data(). qtmodeltest should capture this problem and fail when that happens. From 7620b8475f8e4f6ea5f9c9dac7242faed7a5497d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 9 Sep 2015 21:58:19 -0300 Subject: [PATCH 19/60] Refactor custom model tests to use inline models instead of `testdir` fixture sub-tests This facilitates code coverage and debugging --- pytestqt/modeltest.py | 5 +- tests/test_modeltest.py | 120 ++++++++++++++++++---------------------- 2 files changed, 59 insertions(+), 66 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 5d590327..d0bd4fba 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -466,7 +466,10 @@ def _test_data(self): QtCore.Qt.TextAlignmentRole) alignment = extract_from_variant(alignment) if alignment is not None: - alignment = int(alignment) + try: + alignment = int(alignment) + except TypeError: + assert 0, '%r should be a TextAlignmentRole enum' % alignment mask = int(QtCore.Qt.AlignHorizontal_Mask | QtCore.Qt.AlignVertical_Mask) assert alignment == alignment & mask diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 9f4f108b..13e44cc2 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -1,7 +1,8 @@ import pytest from pytestqt.qt_compat import QStandardItemModel, QStandardItem, \ - QFileSystemModel, QStringListModel, QSortFilterProxyModel, QT_API + QFileSystemModel, QStringListModel, QSortFilterProxyModel, QT_API, \ + QAbstractListModel, QtCore pytestmark = pytest.mark.usefixtures('qtbot') @@ -9,7 +10,7 @@ @pytest.fixture(autouse=True) def default_model_implementations_return_qvariant(qtmodeltester): """PyQt4 is the only implementation where the builtin model implementations - may return QVariant objects. + return QVariant objects. """ qtmodeltester.data_may_return_qvariant = QT_API == 'pyqt4' @@ -55,86 +56,75 @@ def test_sort_filter_proxy_model(qtmodeltester): @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', + 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, ]) -def test_broken_types(testdir, broken_role): +def test_broken_types(check_model, broken_role): """ Check that qtmodeltester correctly captures data() returning invalid values for various display roles. """ - testdir.makepyfile(''' - from pytestqt.qt_compat import QAbstractListModel, QtCore - - invalid_obj = object() # This will fail the type check for any role - - class BrokenTypeModel(QAbstractListModel): - - def rowCount(self, parent=QtCore.QModelIndex()): - if parent == QtCore.QModelIndex(): - return 1 - else: - return 0 - - def data(self, index=QtCore.QModelIndex(), - role=QtCore.Qt.DisplayRole): - if role == {broken_role}: - return invalid_obj - else: - return None - - def test_broken_type(qtmodeltester): - model = BrokenTypeModel() - qtmodeltester.check(model) + class BrokenTypeModel(QAbstractListModel): + + def rowCount(self, parent=QtCore.QModelIndex()): + if parent == QtCore.QModelIndex(): + return 1 + else: + return 0 + + def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): + if role == broken_role: + return object() # This will fail the type check for any role + else: + return None - def test_passing(): - # Sanity test to make sure the imports etc. are right - pass - '''.format(broken_role=broken_role)) - res = testdir.inline_run() - res.assertoutcome(passed=1, failed=1) + check_model(BrokenTypeModel(), should_pass=False) -@pytest.mark.parametrize('role, should_pass', [ - ('QtCore.Qt.AlignLeft', True), - ('QtCore.Qt.AlignRight', True), +@pytest.mark.parametrize('role_value, should_pass', [ + (QtCore.Qt.AlignLeft, True), + (QtCore.Qt.AlignRight, True), (0xFFFFFF, False), ]) -def test_data_alignment(testdir, role, should_pass): +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. """ - testdir.makepyfile(''' - from pytestqt.qt_compat import QAbstractListModel, QtCore + class MyModel(QAbstractListModel): - invalid_obj = object() # This will fail the type check for any role + def rowCount(self, parent=QtCore.QModelIndex()): + return 1 if parent == QtCore.QModelIndex() else 0 - class MyModel(QAbstractListModel): + def columnCount(self, parent=QtCore.QModelIndex()): + return 1 if parent == QtCore.QModelIndex() else 0 - def rowCount(self, parent=QtCore.QModelIndex()): - return 1 if parent == QtCore.QModelIndex() else 0 + def data(self, index=QtCore.QModelIndex(), + role=QtCore.Qt.DisplayRole): + if role == QtCore.Qt.TextAlignmentRole: + return role_value + elif role == QtCore.Qt.DisplayRole: + if index == self.index(0, 0): + return 'Hello' + return None - def columnCount(self, parent=QtCore.QModelIndex()): - return 1 if parent == QtCore.QModelIndex() else 0 + check_model(MyModel(), should_pass=should_pass) - def data(self, index=QtCore.QModelIndex(), - role=QtCore.Qt.DisplayRole): - if role == QtCore.Qt.TextAlignmentRole: - return {role} - else: - if role == QtCore.Qt.DisplayRole and index == \ - self.index(0, 0): - return 'Hello' - return None - def test_broken_alignment(qtmodeltester): - model = MyModel() - qtmodeltester.data_may_return_qvariant = True +@pytest.fixture +def check_model(qtmodeltester): + """ + Return a check_model(model, should_pass=True) function that uses + qtmodeltester to check if the model is OK or not according to the + ``should_pass`` parameter. + """ + def check(model, should_pass=True): + if should_pass: qtmodeltester.check(model) - '''.format(role=role)) - res = testdir.inline_run() - res.assertoutcome(passed=int(should_pass), failed=int(not should_pass)) + else: + with pytest.raises(AssertionError): + qtmodeltester.check(model) + return check From 77a9cf29818ce198355907b7d906ac13a0d86b0d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 9 Sep 2015 22:44:28 -0300 Subject: [PATCH 20/60] Small code coverage adjustments - Remove debug code from coverage (as it won't fail the test anyway) - Remove some dead code --- pytestqt/modeltest.py | 18 ++++-------------- tests/test_modeltest.py | 3 --- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index d0bd4fba..a65cf1a1 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -51,6 +51,7 @@ def __init__(self, config): self._fetching_more = None self._insert = None self._remove = None + self._changing = None self._verbose = config.getoption('verbose') > 0 self.data_may_return_qvariant = False @@ -103,9 +104,6 @@ def check(self, model, verbose=False): self._run(verbose=verbose) def _cleanup(self): - if self._model is None: - return - self._model.columnsAboutToBeInserted.disconnect(self._run) self._model.columnsAboutToBeRemoved.disconnect(self._run) self._model.columnsInserted.disconnect(self._run) @@ -161,10 +159,7 @@ def _test_basic(self): display_data = self._model.data(QtCore.QModelIndex(), QtCore.Qt.DisplayRole) - # note: compare against None using "==" on purpose, as depending - # on the Qt API this will be a QVariant object which compares using - # "==" correctly against other Python types, including None - assert display_data == None + assert extract_from_variant(display_data) is None self._fetching_more = True self._model.fetchMore(QtCore.QModelIndex()) self._fetching_more = False @@ -401,8 +396,7 @@ def _check_children(self, parent, currentDepth=0): # If the next test fails here is some somewhat useful debug you # play with. - if self._parent(index) != parent: - # FIXME + if self._parent(index) != parent: # pragma: no cover self._debug(r, c, currentDepth, self._model.data(index), self._model.data(parent)) self._debug(index, parent, self._parent(index)) @@ -410,7 +404,6 @@ def _check_children(self, parent, currentDepth=0): # QTreeView view # view.setModel(self._model) # view.show() - pass # Check that we can get back our real parent. assert self._parent(index) == parent @@ -485,8 +478,6 @@ def _on_rows_about_to_be_inserted(self, parent, start, end): This gets stored to make sure it actually happens in rowsInserted. """ - - # Q_UNUSED(end) self._debug("rowsAboutToBeInserted", "start=", start, "end=", end, "parent=", self._model.data(parent), "current count of parent=", self._model.rowCount(parent), "display of last=", @@ -518,14 +509,13 @@ def _on_rows_inserted(self, parent, start, end): expected = self._model.data(self._model.index(end + 1, 0, c.parent)) - if c.next != expected: + if c.next != expected: # pragma: no cover # FIXME self._debug(start, end) for i in xrange(self._model.rowCount()): self._debug(self._model.index(i, 0).data()) data = self._model.data(self._model.index(end + 1, 0, c.parent)) self._debug(c.next, data) - pass assert c.next == expected diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 13e44cc2..b1176308 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -99,9 +99,6 @@ class MyModel(QAbstractListModel): def rowCount(self, parent=QtCore.QModelIndex()): return 1 if parent == QtCore.QModelIndex() else 0 - def columnCount(self, parent=QtCore.QModelIndex()): - return 1 if parent == QtCore.QModelIndex() else 0 - def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): if role == QtCore.Qt.TextAlignmentRole: From c10ce046627ac440d314f31ff67894e8efae774c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 9 Sep 2015 23:00:21 -0300 Subject: [PATCH 21/60] Replace data_may_return_qvariant by data_display_may_return_none After clarification from @The-Compiler, now implements the originally intended check --- docs/modeltester.rst | 12 +++++++++++- pytestqt/modeltest.py | 19 +++++++++---------- tests/test_modeltest.py | 8 -------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/modeltester.rst b/docs/modeltester.rst index ddfa6417..6eec65eb 100644 --- a/docs/modeltester.rst +++ b/docs/modeltester.rst @@ -33,7 +33,17 @@ items and call ``qtmodeltester.check``: model.setItem(1, 1, items[3]) qtmodeltester.check(model) -If the tester finds a problem the test will fail with an assert. +If the tester finds a problem the test will fail with an assert pinpointing +the issue. + +The following attribute that may influence the outcome of the check depending +on your model implementation: + +* ``data_display_may_return_none`` (default: ``False``): While you can + technically return ``None`` (or an invalid ``QVariant``) from ``data()`` + for ``QtCore.Qt.DisplayRole``, this usually is a sign of + a bug in your implementation. Set this variable to ``True`` if this really + is OK in your model. The source code was ported from `modeltest.cpp`_ by `Florian Bruhin`_, many thanks! diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index a65cf1a1..b3418028 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -43,7 +43,11 @@ class ModelTester: - """A tester for Qt's QAbstractItemModel's.""" + """A tester for Qt's QAbstractItemModel's. + + :ivar bool data_display_may_return_none: if the model implementation is + allowed to return None from data() for DisplayRole. + """ def __init__(self, config): self._model = None @@ -53,7 +57,7 @@ def __init__(self, config): self._remove = None self._changing = None self._verbose = config.getoption('verbose') > 0 - self.data_may_return_qvariant = False + self.data_display_may_return_none = False def _debug(self, *args): if self._verbose: @@ -383,15 +387,10 @@ def _check_children(self, parent, currentDepth=0): assert index.model() == self._orig_model assert index.row() == r assert index.column() == c - # While you can technically return a QVariant usually this is a - # sign of a bug in data(). Disable if this really is ok in - # your model. + data = self._model.data(index, QtCore.Qt.DisplayRole) - is_qvariant = type(data).__name__ == 'QVariant' - if self.data_may_return_qvariant and is_qvariant: - assert data.isValid() - elif not self.data_may_return_qvariant: - assert not is_qvariant + if not self.data_display_may_return_none: + assert extract_from_variant(data) is not None # If the next test fails here is some somewhat useful debug you # play with. diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index b1176308..d841bd24 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -7,14 +7,6 @@ pytestmark = pytest.mark.usefixtures('qtbot') -@pytest.fixture(autouse=True) -def default_model_implementations_return_qvariant(qtmodeltester): - """PyQt4 is the only implementation where the builtin model implementations - return QVariant objects. - """ - qtmodeltester.data_may_return_qvariant = QT_API == 'pyqt4' - - def test_standard_item_model(qtmodeltester): """ Basic test which uses qtmodeltester with a QStandardItemModel. From b4b876489c131c74f9023cfca086ad6e7249e544 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 9 Sep 2015 23:15:28 -0300 Subject: [PATCH 22/60] Cover header changes in models --- tests/test_modeltest.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index d841bd24..9eb73aec 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -91,8 +91,7 @@ class MyModel(QAbstractListModel): def rowCount(self, parent=QtCore.QModelIndex()): return 1 if parent == QtCore.QModelIndex() else 0 - def data(self, index=QtCore.QModelIndex(), - role=QtCore.Qt.DisplayRole): + def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): if role == QtCore.Qt.TextAlignmentRole: return role_value elif role == QtCore.Qt.DisplayRole: @@ -103,6 +102,32 @@ def data(self, index=QtCore.QModelIndex(), check_model(MyModel(), should_pass=should_pass) +def test_header_handling(check_model): + + class MyModel(QAbstractListModel): + + def rowCount(self, parent=QtCore.QModelIndex()): + return 1 if parent == 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) + + def headerData(self, section, orientation, role=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): + return 'Contents' + return None + + model = MyModel() + model.set_header_text('Start Header') + check_model(model, should_pass=1) + model.set_header_text('New Header') + + @pytest.fixture def check_model(qtmodeltester): """ From 4f96e18e07cf06c26fc570a2714250ec8edf606a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 19:15:45 +0200 Subject: [PATCH 23/60] Run dos2unix over all files --- pytestqt/__init__.py | 60 +++--- pytestqt/qt_compat.py | 452 +++++++++++++++++++++--------------------- setup.py | 128 ++++++------ 3 files changed, 320 insertions(+), 320 deletions(-) diff --git a/pytestqt/__init__.py b/pytestqt/__init__.py index 83dcda70..b04f0fa8 100644 --- a/pytestqt/__init__.py +++ b/pytestqt/__init__.py @@ -1,30 +1,30 @@ -''' -`pytest-qt` is a pytest_ plugin that provides fixtures to help programmers write tests for -PySide_ and PyQt_. - -The main usage is to use the ``qtbot`` fixture, which provides methods to simulate user -interaction, like key presses and mouse clicks:: - - def test_hello(qtbot): - widget = HelloWidget() - qtbot.addWidget(widget) - - # click in the Greet button and make sure it updates the appropriate label - qtbot.mouseClick(window.button_greet, QtCore.Qt.LeftButton) - - assert window.greet_label.text() == 'Hello!' - - -.. .. literalinclude:: ../src/pytestqt/_tests/test_basics.py -.. :language: python -.. :start-after: # create test widget - - -.. _pytest: http://www.pytest.org -.. _PySide: https://pypi.python.org/pypi/PySide -.. _PyQt: http://www.riverbankcomputing.com/software/pyqt - -''' - -version = '1.5.1' -__version__ = version +''' +`pytest-qt` is a pytest_ plugin that provides fixtures to help programmers write tests for +PySide_ and PyQt_. + +The main usage is to use the ``qtbot`` fixture, which provides methods to simulate user +interaction, like key presses and mouse clicks:: + + def test_hello(qtbot): + widget = HelloWidget() + qtbot.addWidget(widget) + + # click in the Greet button and make sure it updates the appropriate label + qtbot.mouseClick(window.button_greet, QtCore.Qt.LeftButton) + + assert window.greet_label.text() == 'Hello!' + + +.. .. literalinclude:: ../src/pytestqt/_tests/test_basics.py +.. :language: python +.. :start-after: # create test widget + + +.. _pytest: http://www.pytest.org +.. _PySide: https://pypi.python.org/pypi/PySide +.. _PyQt: http://www.riverbankcomputing.com/software/pyqt + +''' + +version = '1.5.1' +__version__ = version diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 4e5f4101..9807e4c6 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -1,226 +1,226 @@ -""" -Provide a common way to import Qt classes used by pytest-qt in a unique manner, -abstracting API differences between PyQt4, PyQt5 and PySide. - -.. 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 collections import namedtuple -import os - -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - -if not on_rtd: # pragma: no cover - - def _try_import(name): - try: - __import__(name) - return True - except ImportError: - return False - - def _guess_qt_api(): - if _try_import('PySide'): - return 'pyside' - elif _try_import('PyQt4'): - return 'pyqt4' - elif _try_import('PyQt5'): - return 'pyqt5' - else: - msg = 'pytest-qt requires either PySide, PyQt4 or PyQt5 to be installed' - raise ImportError(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', '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', - 'pyqt5': 'PyQt5', - } - _root_module = _root_modules[QT_API] - - 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 - QFileSystemModel = QtGui.QFileSystemModel - QStringListModel = QtGui.QStringListModel - QSortFilterProxyModel = QtGui.QSortFilterProxyModel - QAbstractListModel = QtCore.QAbstractListModel - QAbstractTableModel = QtCore.QAbstractTableModel - - def cast(obj, typ): - """no cast operation is available in PySide""" - return obj - - 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', 'pyqt5'): - import sip - Signal = QtCore.pyqtSignal - Slot = QtCore.pyqtSlot - Property = QtCore.pyqtProperty - - if QT_API == 'pyqt5': - qt_api_name = 'PyQt5' - - _QtWidgets = _import_module('QtWidgets') - QApplication = _QtWidgets.QApplication - QWidget = _QtWidgets.QWidget - qInstallMessageHandler = QtCore.qInstallMessageHandler - - QFileSystemModel = _QtWidgets.QFileSystemModel - QStringListModel = QtCore.QStringListModel - QSortFilterProxyModel = QtCore.QSortFilterProxyModel - - def extract_from_variant(variant): - """returns python object from the given QVariant""" - if isinstance(variant, QtCore.QVariant): - return variant.value() - return variant - else: - qt_api_name = 'PyQt4' - - QApplication = QtGui.QApplication - QWidget = QtGui.QWidget - qInstallMsgHandler = QtCore.qInstallMsgHandler - - QFileSystemModel = QtGui.QFileSystemModel - QStringListModel = QtGui.QStringListModel - 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 - - QStandardItem = QtGui.QStandardItem - QStandardItemModel = QtGui.QStandardItemModel - QAbstractListModel = QtCore.QAbstractListModel - QAbstractTableModel = QtCore.QAbstractTableModel - - def get_versions(): - return VersionTuple(qt_api_name, QtCore.PYQT_VERSION_STR, - QtCore.qVersion(), QtCore.QT_VERSION_STR) - - def cast(obj, typ): - """casts from a subclass to a parent class""" - return sip.cast(obj, typ) - - def make_variant(value=None): - """Return a QVariant object from the given Python builtin""" - import sys - # PyQt4 on py3 doesn't allow one to instantiate any QVariant at - # all: - # QVariant represents a mapped type and cannot be instantiated - # --' - if QT_API == 'pyqt4' and sys.version_info[0] == 3: - 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 = '' - cast = Mock() - extract_from_variant = Mock() +""" +Provide a common way to import Qt classes used by pytest-qt in a unique manner, +abstracting API differences between PyQt4, PyQt5 and PySide. + +.. 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 collections import namedtuple +import os + +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +if not on_rtd: # pragma: no cover + + def _try_import(name): + try: + __import__(name) + return True + except ImportError: + return False + + def _guess_qt_api(): + if _try_import('PySide'): + return 'pyside' + elif _try_import('PyQt4'): + return 'pyqt4' + elif _try_import('PyQt5'): + return 'pyqt5' + else: + msg = 'pytest-qt requires either PySide, PyQt4 or PyQt5 to be installed' + raise ImportError(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', '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', + 'pyqt5': 'PyQt5', + } + _root_module = _root_modules[QT_API] + + 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 + QFileSystemModel = QtGui.QFileSystemModel + QStringListModel = QtGui.QStringListModel + QSortFilterProxyModel = QtGui.QSortFilterProxyModel + QAbstractListModel = QtCore.QAbstractListModel + QAbstractTableModel = QtCore.QAbstractTableModel + + def cast(obj, typ): + """no cast operation is available in PySide""" + return obj + + 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', 'pyqt5'): + import sip + Signal = QtCore.pyqtSignal + Slot = QtCore.pyqtSlot + Property = QtCore.pyqtProperty + + if QT_API == 'pyqt5': + qt_api_name = 'PyQt5' + + _QtWidgets = _import_module('QtWidgets') + QApplication = _QtWidgets.QApplication + QWidget = _QtWidgets.QWidget + qInstallMessageHandler = QtCore.qInstallMessageHandler + + QFileSystemModel = _QtWidgets.QFileSystemModel + QStringListModel = QtCore.QStringListModel + QSortFilterProxyModel = QtCore.QSortFilterProxyModel + + def extract_from_variant(variant): + """returns python object from the given QVariant""" + if isinstance(variant, QtCore.QVariant): + return variant.value() + return variant + else: + qt_api_name = 'PyQt4' + + QApplication = QtGui.QApplication + QWidget = QtGui.QWidget + qInstallMsgHandler = QtCore.qInstallMsgHandler + + QFileSystemModel = QtGui.QFileSystemModel + QStringListModel = QtGui.QStringListModel + 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 + + QStandardItem = QtGui.QStandardItem + QStandardItemModel = QtGui.QStandardItemModel + QAbstractListModel = QtCore.QAbstractListModel + QAbstractTableModel = QtCore.QAbstractTableModel + + def get_versions(): + return VersionTuple(qt_api_name, QtCore.PYQT_VERSION_STR, + QtCore.qVersion(), QtCore.QT_VERSION_STR) + + def cast(obj, typ): + """casts from a subclass to a parent class""" + return sip.cast(obj, typ) + + def make_variant(value=None): + """Return a QVariant object from the given Python builtin""" + import sys + # PyQt4 on py3 doesn't allow one to instantiate any QVariant at + # all: + # QVariant represents a mapped type and cannot be instantiated + # --' + if QT_API == 'pyqt4' and sys.version_info[0] == 3: + 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 = '' + cast = Mock() + extract_from_variant = Mock() diff --git a/setup.py b/setup.py index e0a10c47..a6ff5bf1 100644 --- a/setup.py +++ b/setup.py @@ -1,64 +1,64 @@ -import sys -import re - -from setuptools import setup -from setuptools.command.test import test as TestCommand - - -class PyTest(TestCommand): - """ - Overrides setup "test" command, taken from here: - http://pytest.org/latest/goodpractises.html - """ - - def finalize_options(self): - TestCommand.finalize_options(self) - self.test_args = [] - self.test_suite = True - - def run_tests(self): - # import here, cause outside the eggs aren't loaded - import pytest - - errno = pytest.main([]) - sys.exit(errno) - - -with open('pytestqt/__init__.py') as f: - m = re.search("version = '(.*)'", f.read()) - assert m is not None - version = m.group(1) - -setup( - name="pytest-qt", - version=version, - packages=['pytestqt'], - entry_points={ - 'pytest11': ['pytest-qt = pytestqt.plugin'], - }, - install_requires=['pytest>=2.7.0'], - - # metadata for upload to PyPI - author="Bruno Oliveira", - author_email="nicoddemus@gmail.com", - description='pytest support for PyQt and PySide applications', - long_description=open('README.rst').read(), - license="LGPL", - keywords="pytest qt test unittest", - url="http://github.com/pytest-dev/pytest-qt", - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Topic :: Desktop Environment :: Window Managers', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: User Interfaces', - ], - tests_requires=['pytest'], - cmdclass={'test': PyTest}, -) +import sys +import re + +from setuptools import setup +from setuptools.command.test import test as TestCommand + + +class PyTest(TestCommand): + """ + Overrides setup "test" command, taken from here: + http://pytest.org/latest/goodpractises.html + """ + + def finalize_options(self): + TestCommand.finalize_options(self) + self.test_args = [] + self.test_suite = True + + def run_tests(self): + # import here, cause outside the eggs aren't loaded + import pytest + + errno = pytest.main([]) + sys.exit(errno) + + +with open('pytestqt/__init__.py') as f: + m = re.search("version = '(.*)'", f.read()) + assert m is not None + version = m.group(1) + +setup( + name="pytest-qt", + version=version, + packages=['pytestqt'], + entry_points={ + 'pytest11': ['pytest-qt = pytestqt.plugin'], + }, + install_requires=['pytest>=2.7.0'], + + # metadata for upload to PyPI + author="Bruno Oliveira", + author_email="nicoddemus@gmail.com", + description='pytest support for PyQt and PySide applications', + long_description=open('README.rst').read(), + license="LGPL", + keywords="pytest qt test unittest", + url="http://github.com/pytest-dev/pytest-qt", + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Topic :: Desktop Environment :: Window Managers', + 'Topic :: Software Development :: Quality Assurance', + 'Topic :: Software Development :: Testing', + 'Topic :: Software Development :: User Interfaces', + ], + tests_requires=['pytest'], + cmdclass={'test': PyTest}, +) From 24008bb0948a9348e7eb46ed071c402415686f57 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 19:31:27 +0200 Subject: [PATCH 24/60] Remove unnecessary imports They somehow slipped in with the master-merge --- pytestqt/plugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pytestqt/plugin.py b/pytestqt/plugin.py index 12c52dbb..c17e0baa 100644 --- a/pytestqt/plugin.py +++ b/pytestqt/plugin.py @@ -52,7 +52,6 @@ def qtbot(qapp, request): @pytest.fixture def qtlog(request): - from pytestqt.logging import _QtMessageCapture """Fixture that can access messages captured during testing""" if hasattr(request._pyfuncitem, 'qt_log_capture'): return request._pyfuncitem.qt_log_capture @@ -72,7 +71,6 @@ def qtmodeltester(request): def pytest_addoption(parser): - from pytestqt.qt_compat import QT_API parser.addini('qt_no_exception_capture', 'disable automatic exception capture') parser.addini('qt_wait_signal_raising', @@ -150,7 +148,6 @@ def _process_events(): """Calls app.processEvents() while taking care of capturing exceptions or not based on the given item's configuration. """ - from pytestqt.qt_compat import QApplication app = QApplication.instance() if app is not None: app.processEvents() From a860d4a5337a1deeff4fe8a2723010429f6d7db7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 25 Jun 2016 17:47:07 +0200 Subject: [PATCH 25/60] Add comment for _cleanup --- pytestqt/modeltest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index b3418028..fa39a3f6 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -108,6 +108,7 @@ def check(self, model, verbose=False): self._run(verbose=verbose) def _cleanup(self): + """Not API intended for users, but called from the fixture function.""" self._model.columnsAboutToBeInserted.disconnect(self._run) self._model.columnsAboutToBeRemoved.disconnect(self._run) self._model.columnsInserted.disconnect(self._run) From 777a1589211e83fc4a57931d69e52cbbd8ebe46e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 25 Jun 2016 17:52:14 +0200 Subject: [PATCH 26/60] Cherry-pick tests from modeltest branch --- tests/test_modeltest.py | 99 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 9eb73aec..c315f0bc 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -1,3 +1,5 @@ +import os + import pytest from pytestqt.qt_compat import QStandardItemModel, QStandardItem, \ @@ -7,6 +9,24 @@ pytestmark = pytest.mark.usefixtures('qtbot') +class BasicModel(QtCore.QAbstractItemModel): + + def data(self, index, role=QtCore.Qt.DisplayRole): + return None + + def rowCount(self, parent=QtCore.QModelIndex()): + return 0 + + def columnCount(self, parent=QtCore.QModelIndex()): + return 0 + + def index(self, row, column, parent=QtCore.QModelIndex()): + return QtCore.QModelIndex() + + def parent(self, index): + return QtCore.QModelIndex() + + def test_standard_item_model(qtmodeltester): """ Basic test which uses qtmodeltester with a QStandardItemModel. @@ -142,3 +162,82 @@ def check(model, should_pass=True): with pytest.raises(AssertionError): qtmodeltester.check(model) return check + + +def test_invalid_column_count(qtmodeltester): + """Basic check with an invalid model.""" + class Model(BasicModel): + def columnCount(self, parent=QtCore.QModelIndex()): + return -1 + + model = Model() + + with pytest.raises(AssertionError): + qtmodeltester.check(model) + + +def test_changing_model_insert(qtmodeltester): + model = QStandardItemModel() + item = QStandardItem('foo') + qtmodeltester.check(model) + model.insertRow(0, item) + + +def test_changing_model_remove(qtmodeltester): + model = QStandardItemModel() + item = 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.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]) +def test_changing_model_header_data(qtmodeltester, orientation): + model = QStandardItemModel() + item = QStandardItem('foo') + model.setItem(0, 0, item) + qtmodeltester.check(model) + model.setHeaderData(0, orientation, 'blah') + + +def test_changing_model_sort(qtmodeltester): + """Sorting emits layoutChanged""" + model = QStandardItemModel() + item = QStandardItem('foo') + model.setItem(0, 0, item) + qtmodeltester.check(model) + model.sort(0) + + +def test_nop(qtmodeltester): + """We should not get a crash on cleanup with no model.""" + pass + + +def test_fetch_more(qtmodeltester): + class Model(BasicModel): + + def rowCount(self, parent=None): + if parent is None: + 1/0 + return 1 + else: + return 0 + + def canFetchMore(self, parent): + return True + + def fetchMore(self, parent): + pass + + model = Model() + qtmodeltester.check(model) From 062e3e362e3590cb08b614787d83e5aa62270c68 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 19:45:33 +0200 Subject: [PATCH 27/60] Re-add None-check in ModelTester._cleanup This was removed in 77a9cf29818ce198355907b7d906ac13a0d86b0d but is actually needed to avoid an exception as triggered by a test using the qtmodeltester fixture without calling .check(). While that's not a good thing to do, we should still not fail. --- pytestqt/modeltest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index fa39a3f6..9f3e2360 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -109,6 +109,9 @@ def check(self, model, verbose=False): def _cleanup(self): """Not API intended for users, but called from the fixture function.""" + if self._model is None: + return + self._model.columnsAboutToBeInserted.disconnect(self._run) self._model.columnsAboutToBeRemoved.disconnect(self._run) self._model.columnsInserted.disconnect(self._run) From 5561cc491d756b23056f353b866af4822b841178 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 19:52:19 +0200 Subject: [PATCH 28/60] Remove casting of models While casting the given model to a QAbstractItemModel gave us access to the private columnCount/rowCount methods (for a QAbstract{List,Table}Model), it also means it would not ever run overridden methods of the model, instead the QAbstractItemModel implementations would always run. We now never cast anything and instead use the same workaround we use for PySide as well (which doesn't have sip.cast). For some reason this makes test_file_system_model hang, which is why it gets marked as @xfail(run=False) for now. --- pytestqt/modeltest.py | 21 ++++++++------------- pytestqt/qt_compat.py | 10 ---------- tests/test_modeltest.py | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 9f3e2360..eb03555d 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -34,8 +34,8 @@ from __future__ import print_function import collections -from pytestqt.qt_compat import QtCore, QtGui, cast, extract_from_variant, \ - QAbstractListModel, QAbstractTableModel, USING_PYSIDE +from pytestqt.qt_compat import QtCore, QtGui, extract_from_variant, \ + QAbstractListModel, QAbstractTableModel _Changing = collections.namedtuple('_Changing', 'parent, oldSize, last, next') @@ -51,7 +51,6 @@ class ModelTester: def __init__(self, config): self._model = None - self._orig_model = None self._fetching_more = None self._insert = None self._remove = None @@ -71,8 +70,7 @@ def check(self, model, verbose=False): Whenever anything happens recheck everything. """ assert model is not None - self._model = cast(model, QtCore.QAbstractItemModel) - self._orig_model = model + self._model = model self._fetching_more = False self._insert = [] self._remove = [] @@ -139,7 +137,6 @@ def _cleanup(self): self._model.headerDataChanged.disconnect(self._on_header_data_changed) self._model = None - self._orig_model = None def _run(self, verbose=False): self._verbose = verbose @@ -388,7 +385,7 @@ def _check_children(self, parent, currentDepth=0): assert index == sibling # Some basic checking on the index that is returned - assert index.model() == self._orig_model + assert index.model() == self._model assert index.row() == r assert index.column() == c @@ -581,11 +578,9 @@ def _on_header_data_changed(self, orientation, start, end): def _column_count(self, parent=QtCore.QModelIndex()): """ Workaround for the fact that ``columnCount`` is a private method in - QAbstractListModel/QAbstractTableModel subclasses and PySide does not - provide any way to work around that limitation. PyQt lets us "cast" - back to the super class to access these private methods. + QAbstractListModel/QAbstractTableModel subclasses. """ - if isinstance(self._orig_model, QAbstractListModel) and USING_PYSIDE: + if isinstance(self._model, QAbstractListModel): return 1 if parent == QtCore.QModelIndex() else 0 else: return self._model.columnCount(parent) @@ -595,7 +590,7 @@ def _parent(self, index): .. see:: ``_column_count`` """ model_types = (QAbstractListModel, QAbstractTableModel) - if isinstance(self._orig_model, model_types) and USING_PYSIDE: + if isinstance(self._model, model_types): return QtCore.QModelIndex() else: return self._model.parent(index) @@ -605,7 +600,7 @@ def _has_children(self, parent=QtCore.QModelIndex()): .. see:: ``_column_count`` """ model_types = (QAbstractListModel, QAbstractTableModel) - if isinstance(self._orig_model, model_types) and USING_PYSIDE: + if isinstance(self._model, model_types): return parent == QtCore.QModelIndex() and self._model.rowCount() > 0 else: return self._model.hasChildren(parent) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 3be6fa9a..07430687 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -115,10 +115,6 @@ def _import_module(module_name): QAbstractListModel = QtCore.QAbstractListModel QAbstractTableModel = QtCore.QAbstractTableModel - def cast(obj, typ): - """no cast operation is available in PySide""" - return obj - def extract_from_variant(variant): """PySide does not expose QVariant API""" return variant @@ -132,7 +128,6 @@ def get_versions(): QtCore.__version__) elif QT_API in ('pyqt4', 'pyqt4v2', 'pyqt5'): - import sip Signal = QtCore.pyqtSignal Slot = QtCore.pyqtSlot Property = QtCore.pyqtProperty @@ -180,10 +175,6 @@ def get_versions(): return VersionTuple(qt_api_name, QtCore.PYQT_VERSION_STR, QtCore.qVersion(), QtCore.QT_VERSION_STR) - def cast(obj, typ): - """casts from a subclass to a parent class""" - return sip.cast(obj, typ) - def make_variant(value=None): """Return a QVariant object from the given Python builtin""" import sys @@ -235,5 +226,4 @@ def __getattr__(cls, name): QtCriticalMsg = Mock() QtFatalMsg = Mock() QT_API = '' - cast = Mock() extract_from_variant = Mock() diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index c315f0bc..524ed4b2 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -42,6 +42,7 @@ def test_standard_item_model(qtmodeltester): qtmodeltester.check(model) +@pytest.mark.xfail(run=False, reason='Makes pytest hang') def test_file_system_model(qtmodeltester, tmpdir): tmpdir.ensure('directory', dir=1) tmpdir.ensure('file1.txt') @@ -223,6 +224,28 @@ def test_nop(qtmodeltester): pass +def test_overridden_methods(qtmodeltester): + """Make sure overriden methods of a model are actually run. + + With a previous implementation of the modeltester using sip.cast, the custom + implementations did never actually run. + """ + class Model(BasicModel): + + def __init__(self, parent=None): + super().__init__(parent) + self.row_count_did_run = False + + def rowCount(self, parent=None): + self.row_count_did_run = True + return 0 + + model = Model() + assert not model.row_count_did_run + qtmodeltester.check(model) + assert model.row_count_did_run + + def test_fetch_more(qtmodeltester): class Model(BasicModel): From 3881b510d2d38f5e4e0d359eda731e7c6e2b4fd7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 20:24:28 +0200 Subject: [PATCH 29/60] Fix test_fetch_more --- tests/test_modeltest.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 524ed4b2..b7f3edd8 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -247,20 +247,16 @@ def rowCount(self, parent=None): def test_fetch_more(qtmodeltester): - class Model(BasicModel): - - def rowCount(self, parent=None): - if parent is None: - 1/0 - return 1 - else: - return 0 + class Model(QStandardItemModel): def canFetchMore(self, parent): return True def fetchMore(self, parent): - pass + """Force a re-check while fetching more.""" + self.setData(self.index(0, 0), 'bar') model = Model() + item = QStandardItem('foo') + model.setItem(0, 0, item) qtmodeltester.check(model) From b8b3599adf440b511fa8a00d3e222fd6e8870d01 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 21:45:25 +0200 Subject: [PATCH 30/60] Fix parent handling in _on_rows_removed/inserted With Qt4, we get invalid parents (as a root element changed) for both 'parent' (rows{Inserted,Removed} argument) and 'c.parent' (rowsAboutToBe{Inserted,Removed} argument). Both represent the same thing, but they don't compare equal using ==. --- pytestqt/modeltest.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index eb03555d..eadbddb7 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -493,10 +493,16 @@ def _on_rows_about_to_be_inserted(self, parent, start, end): def _on_rows_inserted(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" c = self._insert.pop() - assert c.parent == parent self._debug("rowsInserted", "start=", start, "end=", end, "oldsize=", c.oldSize, "parent=", self._model.data(parent), "current rowcount of parent=", self._model.rowCount(parent)) + + if c.parent.isValid(): + assert parent.isValid() + assert c.parent == parent + else: + assert not parent.isValid() + for ii in range(start, end): self._debug("itemWasInserted:", ii, self._model.data(self._model.index(ii, 0, parent))) @@ -546,7 +552,12 @@ def _on_rows_removed(self, parent, start, end): last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) next_data = self._model.data(self._model.index(start, 0, c.parent)) - assert c.parent == parent + if c.parent.isValid(): + assert parent.isValid() + assert c.parent == parent + else: + assert not parent.isValid() + assert c.oldSize - (end - start + 1) == self._model.rowCount(parent) assert c.last == last_data assert c.next == next_data From 5d36a9daecff234f32b1397ed1d73d7d5da40c23 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 22:11:21 +0200 Subject: [PATCH 31/60] Fix on_layout_changed for Python 2 Python 2 doesn't have list.clear() --- pytestqt/modeltest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index eadbddb7..fe2d69a8 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -54,7 +54,7 @@ def __init__(self, config): self._fetching_more = None self._insert = None self._remove = None - self._changing = None + self._changing = [] self._verbose = config.getoption('verbose') > 0 self.data_display_may_return_none = False @@ -533,7 +533,7 @@ def _on_layout_about_to_be_changed(self): def _on_layout_changed(self): for p in self._changing: assert p == self._model.index(p.row(), p.column(), p.parent()) - self._changing.clear() + self._changing = [] def _on_rows_about_to_be_removed(self, parent, start, end): """Store what is about to be removed to make sure it actually happens. From 26544253ce11afd8fb962b75823dcae74dd08854 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 22:12:27 +0200 Subject: [PATCH 32/60] Fix super call for Python 2 --- tests/test_modeltest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index b7f3edd8..4c6b8b06 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -233,7 +233,7 @@ def test_overridden_methods(qtmodeltester): class Model(BasicModel): def __init__(self, parent=None): - super().__init__(parent) + super(Model, self).__init__(parent) self.row_count_did_run = False def rowCount(self, parent=None): From a830c7a13d3416c5193ff311f2addb870f46c12a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 28 Jun 2016 23:14:31 +0200 Subject: [PATCH 33/60] Fix qt_compat.make_variant with pyqt4v2 --- pytestqt/qt_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 07430687..74c45c84 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -182,7 +182,7 @@ def make_variant(value=None): # all: # QVariant represents a mapped type and cannot be instantiated # --' - if QT_API == 'pyqt4' and sys.version_info[0] == 3: + if QT_API in ['pyqt4', 'pyqt4v2'] and sys.version_info[0] == 3: return value return QtCore.QVariant(value) From 08c2177493daa2a96e166b256b4ac18a4e7e6bca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 10:01:26 +0200 Subject: [PATCH 34/60] Revert "Fix parent handling in _on_rows_removed/inserted" This reverts commit b8b3599adf440b511fa8a00d3e222fd6e8870d01. --- pytestqt/modeltest.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index fe2d69a8..5e881d6a 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -493,16 +493,10 @@ def _on_rows_about_to_be_inserted(self, parent, start, end): def _on_rows_inserted(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" c = self._insert.pop() + assert c.parent == parent self._debug("rowsInserted", "start=", start, "end=", end, "oldsize=", c.oldSize, "parent=", self._model.data(parent), "current rowcount of parent=", self._model.rowCount(parent)) - - if c.parent.isValid(): - assert parent.isValid() - assert c.parent == parent - else: - assert not parent.isValid() - for ii in range(start, end): self._debug("itemWasInserted:", ii, self._model.data(self._model.index(ii, 0, parent))) @@ -552,12 +546,7 @@ def _on_rows_removed(self, parent, start, end): last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) next_data = self._model.data(self._model.index(start, 0, c.parent)) - if c.parent.isValid(): - assert parent.isValid() - assert c.parent == parent - else: - assert not parent.isValid() - + assert c.parent == parent assert c.oldSize - (end - start + 1) == self._model.rowCount(parent) assert c.last == last_data assert c.next == next_data From 874dccb61846ed06f6b8ee4c54530223e5fe42d9 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 10:15:08 +0200 Subject: [PATCH 35/60] Improve handling of verbose - _run doesn't take a 'verbose' argument anymore as it's called automatically from signals, so a re-run would automatically set the verbosity to False anymore. - If check() is called without a verbosity argument, pytest's --verbose flag is honoured (set in __init__) --- pytestqt/modeltest.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 5e881d6a..040c61a5 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -62,7 +62,7 @@ def _debug(self, *args): if self._verbose: print(*args) - def check(self, model, verbose=False): + def check(self, model, verbose=None): """Runs a series of checks in the given model. Connect to all of the models signals. @@ -76,6 +76,9 @@ def check(self, model, verbose=False): self._remove = [] self._changing = [] + if verbose is not None: + self._verbose = verbose + self._model.columnsAboutToBeInserted.connect(self._run) self._model.columnsAboutToBeRemoved.connect(self._run) self._model.columnsInserted.connect(self._run) @@ -103,7 +106,7 @@ def check(self, model, verbose=False): self._model.dataChanged.connect(self._on_data_changed) self._model.headerDataChanged.connect(self._on_header_data_changed) - self._run(verbose=verbose) + self._run() def _cleanup(self): """Not API intended for users, but called from the fixture function.""" @@ -138,8 +141,7 @@ def _cleanup(self): self._model = None - def _run(self, verbose=False): - self._verbose = verbose + def _run(self): assert self._model is not None if self._fetching_more: return From e74be4e5fd4311cb4592fa596a822599cc6f732d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:11:19 +0200 Subject: [PATCH 36/60] Improve debugging output --- pytestqt/modeltest.py | 119 ++++++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 40 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 040c61a5..f14b0e36 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -58,9 +58,18 @@ def __init__(self, config): self._verbose = config.getoption('verbose') > 0 self.data_display_may_return_none = False - def _debug(self, *args): + def _debug(self, text): if self._verbose: - print(*args) + print('modeltest: ' + text) + + def _modelindex_debug(self, index): + """Get a string for debug output for a QModelIndex.""" + if not index.isValid(): + return ' (0x{:x})'.format(id(index)) + else: + data = self._model.data(index, QtCore.Qt.DisplayRole) + return '{}/{} {!r} (0x{:x})'.format( + index.row(), index.column(), data, id(index)) def check(self, model, verbose=None): """Runs a series of checks in the given model. @@ -349,10 +358,10 @@ def _check_children(self, parent, currentDepth=0): assert columns >= 0 if rows > 0: assert self._has_children(parent) - - self._debug("parent:", self._model.data(parent, QtCore.Qt.DisplayRole), - "rows:", rows, "columns:", columns, "parent column:", - parent.column()) + self._debug("Checking children of {} with depth {} " + "({} rows, {} columns)".format( + self._modelindex_debug(parent), currentDepth, + rows, columns)) topLeftChild = self._model.index(0, 0, parent) @@ -369,6 +378,10 @@ def _check_children(self, parent, currentDepth=0): # rowCount() and columnCount() said that it existed... assert index.isValid() + # sanity checks + assert index.column() == c + assert index.row() == r + # index() should always return the same index when called twice # in a row modifiedIndex = self._model.index(r, c, parent) @@ -399,9 +412,14 @@ def _check_children(self, parent, currentDepth=0): # play with. if self._parent(index) != parent: # pragma: no cover - self._debug(r, c, currentDepth, self._model.data(index), - self._model.data(parent)) - self._debug(index, parent, self._parent(index)) + self._debug( + "parent-check failed for index {}:\n" + " parent {} != expected {}".format( + self._modelindex_debug(index), + self._modelindex_debug(self._parent(index)), + self._modelindex_debug(parent) + ) + ) # And a view that you can even use to show the model. # QTreeView view # view.setModel(self._model) @@ -412,8 +430,10 @@ def _check_children(self, parent, currentDepth=0): # recursively go down the children if self._has_children(index) and currentDepth < 10: - self._debug(r, c, "has children", - self._model.rowCount(index)) + self._debug("{} has {} children".format( + self._modelindex_debug(index), + self._model.rowCount(index) + )) self._check_children(index, currentDepth + 1) # elif currentDepth >= 10: # print("checked 10 deep") @@ -423,6 +443,7 @@ def _check_children(self, parent, currentDepth=0): # doesn't change. newerIndex = self._model.index(r, c, parent) assert index == newerIndex + self._debug("Children check for {} done".format(self._modelindex_debug(parent))) def _test_data(self): """Test model's implementation of data()""" @@ -480,46 +501,64 @@ def _on_rows_about_to_be_inserted(self, parent, start, end): This gets stored to make sure it actually happens in rowsInserted. """ - self._debug("rowsAboutToBeInserted", "start=", start, "end=", end, "parent=", - self._model.data(parent), "current count of parent=", - self._model.rowCount(parent), "display of last=", - self._model.data(self._model.index(start-1, 0, parent))) - self._debug(self._model.index(start-1, 0, parent), self._model.data(self._model.index(start-1, 0, parent))) - - last_data = self._model.data(self._model.index(start - 1, 0, parent)) - next_data = self._model.data(self._model.index(start, 0, parent)) - c = _Changing(parent=parent, oldSize=self._model.rowCount(parent), + last_index = self._model.index(start - 1, 0, parent) + next_index = self._model.index(start, 0, parent) + parent_rowcount = self._model.rowCount(parent) + + self._debug("rows about to be inserted: start {}, end {}, parent {}, " + "parent row count {}, last item {}, next item {}".format( + start, end, + self._modelindex_debug(parent), + parent_rowcount, + self._modelindex_debug(last_index), + self._modelindex_debug(next_index), + ) + ) + + last_data = self._model.data(last_index) + next_data = self._model.data(next_index) + c = _Changing(parent=parent, oldSize=parent_rowcount, last=last_data, next=next_data) self._insert.append(c) def _on_rows_inserted(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" + current_size = self._model.rowCount(parent) c = self._insert.pop() - assert c.parent == parent - self._debug("rowsInserted", "start=", start, "end=", end, "oldsize=", - c.oldSize, "parent=", self._model.data(parent), - "current rowcount of parent=", self._model.rowCount(parent)) - for ii in range(start, end): - self._debug("itemWasInserted:", ii, - self._model.data(self._model.index(ii, 0, parent))) - self._debug() last_data = self._model.data(self._model.index(start - 1, 0, parent)) + next_data = self._model.data(self._model.index(end + 1, 0, c.parent)) + expected_size = c.oldSize + (end - start + 1) + + self._debug("rows inserted: start {}, end {}".format(start, end)) + self._debug(" from rowsAboutToBeInserted: parent {}, " + "size {} (-> {} expected), " + "next data {!r}, last data {!r}".format( + self._modelindex_debug(c.parent), + c.oldSize, expected_size, + c.next, c.last + ) + ) + + self._debug(" now in rowsInserted: parent {}, size {}, " + "next data {!r}, last data {!r}".format( + self._modelindex_debug(parent), + current_size, + next_data, last_data + ) + ) - assert c.oldSize + (end - start + 1) == self._model.rowCount(parent) - assert c.last == last_data - - expected = self._model.data(self._model.index(end + 1, 0, c.parent)) + assert c.parent == parent - if c.next != expected: # pragma: no cover - # FIXME - self._debug(start, end) - for i in xrange(self._model.rowCount()): - self._debug(self._model.index(i, 0).data()) - data = self._model.data(self._model.index(end + 1, 0, c.parent)) - self._debug(c.next, data) + for ii in range(start, end): + idx = self._model.index(ii, 0, parent) + self._debug(" item {} inserted: {}".format(ii, + self._modelindex_debug(idx))) + self._debug('') - assert c.next == expected + assert current_size == expected_size + assert c.last == last_data + assert c.next == next_data def _on_layout_about_to_be_changed(self): for i in range(max(self._model.rowCount(), 100)): From ee0a45021766d68a5051020d287ace93e751c4e4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:12:56 +0200 Subject: [PATCH 37/60] Turn on verbose in test_modeltest --- tests/test_modeltest.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 4c6b8b06..51334eba 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -39,7 +39,7 @@ def test_standard_item_model(qtmodeltester): model.setItem(1, 0, items[2]) model.setItem(1, 1, items[3]) - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) @pytest.mark.xfail(run=False, reason='Makes pytest hang') @@ -49,15 +49,15 @@ def test_file_system_model(qtmodeltester, tmpdir): tmpdir.ensure('file2.py') model = QFileSystemModel() model.setRootPath(str(tmpdir)) - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) tmpdir.ensure('file3.py') - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) def test_string_list_model(qtmodeltester): model = QStringListModel() model.setStringList(['hello', 'world']) - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) def test_sort_filter_proxy_model(qtmodeltester): @@ -65,7 +65,7 @@ def test_sort_filter_proxy_model(qtmodeltester): model.setStringList(['hello', 'world']) proxy = QSortFilterProxyModel() proxy.setSourceModel(model) - qtmodeltester.check(proxy) + qtmodeltester.check(proxy, verbose=True) @pytest.mark.parametrize('broken_role', [ @@ -158,10 +158,10 @@ def check_model(qtmodeltester): """ def check(model, should_pass=True): if should_pass: - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) else: with pytest.raises(AssertionError): - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) return check @@ -174,13 +174,13 @@ def columnCount(self, parent=QtCore.QModelIndex()): model = Model() with pytest.raises(AssertionError): - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) def test_changing_model_insert(qtmodeltester): model = QStandardItemModel() item = QStandardItem('foo') - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) model.insertRow(0, item) @@ -188,7 +188,7 @@ def test_changing_model_remove(qtmodeltester): model = QStandardItemModel() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) model.removeRow(0) @@ -196,7 +196,7 @@ def test_changing_model_data(qtmodeltester): model = QStandardItemModel() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) model.setData(model.index(0, 0), 'hello world') @@ -206,7 +206,7 @@ def test_changing_model_header_data(qtmodeltester, orientation): model = QStandardItemModel() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) model.setHeaderData(0, orientation, 'blah') @@ -215,7 +215,7 @@ def test_changing_model_sort(qtmodeltester): model = QStandardItemModel() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) model.sort(0) @@ -242,7 +242,7 @@ def rowCount(self, parent=None): model = Model() assert not model.row_count_did_run - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) assert model.row_count_did_run @@ -259,4 +259,4 @@ def fetchMore(self, parent): model = Model() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model) + qtmodeltester.check(model, verbose=True) From ffd985c7fee5545a71ef9f55a77b5e990523c630 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:14:34 +0200 Subject: [PATCH 38/60] Show message without verbose --- pytestqt/modeltest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index f14b0e36..b1c48be5 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -88,6 +88,11 @@ def check(self, model, verbose=None): if verbose is not None: self._verbose = verbose + if not self._verbose: + print("model check running non-verbose, run pytest with -v or use " + "qtmodeltester.check(model, verbose=True) for more " + "information") + self._model.columnsAboutToBeInserted.connect(self._run) self._model.columnsAboutToBeRemoved.connect(self._run) self._model.columnsInserted.connect(self._run) From 1518ce588f2814f20935354fc1fa264b8cb6a21c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:16:15 +0200 Subject: [PATCH 39/60] Revert "Revert "Fix parent handling in _on_rows_removed/inserted"" This reverts commit 08c2177493daa2a96e166b256b4ac18a4e7e6bca. --- pytestqt/modeltest.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index b1c48be5..73997213 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -553,7 +553,11 @@ def _on_rows_inserted(self, parent, start, end): ) ) - assert c.parent == parent + if c.parent.isValid(): + assert parent.isValid() + assert c.parent == parent + else: + assert not parent.isValid() for ii in range(start, end): idx = self._model.index(ii, 0, parent) @@ -592,7 +596,12 @@ def _on_rows_removed(self, parent, start, end): last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) next_data = self._model.data(self._model.index(start, 0, c.parent)) - assert c.parent == parent + if c.parent.isValid(): + assert parent.isValid() + assert c.parent == parent + else: + assert not parent.isValid() + assert c.oldSize - (end - start + 1) == self._model.rowCount(parent) assert c.last == last_data assert c.next == next_data From 8f6e8cf527165028efdb9dc1843b369128cb5aa1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:21:45 +0200 Subject: [PATCH 40/60] Improve data logging for QVariant Otherwise we get simple QVariant repr's in the logging on PyQt4 which doesn't make much sense. --- pytestqt/modeltest.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 73997213..7c9d3ea0 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -69,7 +69,9 @@ def _modelindex_debug(self, index): else: data = self._model.data(index, QtCore.Qt.DisplayRole) return '{}/{} {!r} (0x{:x})'.format( - index.row(), index.column(), data, id(index)) + index.row(), index.column(), + qt_compat.extract_from_variant(data), + id(index)) def check(self, model, verbose=None): """Runs a series of checks in the given model. @@ -541,7 +543,8 @@ def _on_rows_inserted(self, parent, start, end): "next data {!r}, last data {!r}".format( self._modelindex_debug(c.parent), c.oldSize, expected_size, - c.next, c.last + qt_compat.extract_from_variant(c.next), + qt_compat.extract_from_variant(c.last) ) ) @@ -549,7 +552,8 @@ def _on_rows_inserted(self, parent, start, end): "next data {!r}, last data {!r}".format( self._modelindex_debug(parent), current_size, - next_data, last_data + qt_compat.extract_from_variant(next_data), + qt_compat.extract_from_variant(last_data) ) ) From 7f27cb150eb755772a7ae6c72453fbe1dd98d54b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:25:28 +0200 Subject: [PATCH 41/60] Disable make_variant on Python 2 as well Seems like QVariant can't be instantiated there (on Travis) either. --- pytestqt/qt_compat.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index 74c45c84..c5934422 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -177,12 +177,9 @@ def get_versions(): def make_variant(value=None): """Return a QVariant object from the given Python builtin""" - import sys - # PyQt4 on py3 doesn't allow one to instantiate any QVariant at - # all: + # 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'] and sys.version_info[0] == 3: + if QT_API in ['pyqt4', 'pyqt4v2']: return value return QtCore.QVariant(value) From b7095ae41cbe666da2acbf0201588ad9c569b31f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:28:35 +0200 Subject: [PATCH 42/60] Fix extract_from_variant calls --- pytestqt/modeltest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 7c9d3ea0..914bc300 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -70,7 +70,7 @@ def _modelindex_debug(self, index): data = self._model.data(index, QtCore.Qt.DisplayRole) return '{}/{} {!r} (0x{:x})'.format( index.row(), index.column(), - qt_compat.extract_from_variant(data), + extract_from_variant(data), id(index)) def check(self, model, verbose=None): @@ -543,8 +543,8 @@ def _on_rows_inserted(self, parent, start, end): "next data {!r}, last data {!r}".format( self._modelindex_debug(c.parent), c.oldSize, expected_size, - qt_compat.extract_from_variant(c.next), - qt_compat.extract_from_variant(c.last) + extract_from_variant(c.next), + extract_from_variant(c.last) ) ) @@ -552,8 +552,8 @@ def _on_rows_inserted(self, parent, start, end): "next data {!r}, last data {!r}".format( self._modelindex_debug(parent), current_size, - qt_compat.extract_from_variant(next_data), - qt_compat.extract_from_variant(last_data) + extract_from_variant(next_data), + extract_from_variant(last_data) ) ) From 95ef07d7c423e9f79d7ffb848328eaa4c6467f2f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:40:39 +0200 Subject: [PATCH 43/60] Skip modeltest parent checks on Qt4 --- pytestqt/modeltest.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 914bc300..05e4859a 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -557,11 +557,16 @@ def _on_rows_inserted(self, parent, start, end): ) ) - if c.parent.isValid(): - assert parent.isValid() + if QtCore.QT_VERSION >= 0x050000: + # Skipping this on Qt4 as the parent changes for some reason: + # modeltest: rows about to be inserted: [...] + # parent (0x7f8f540eacf8), [...] + # [...] + # modeltest: from rowsAboutToBeInserted: + # parent 0/0 None (0x7f8f540eacf8), [...] + # modeltest: now in rowsInserted: + # parent (0x7f8f60a96cf8) [...] assert c.parent == parent - else: - assert not parent.isValid() for ii in range(start, end): idx = self._model.index(ii, 0, parent) @@ -600,11 +605,10 @@ def _on_rows_removed(self, parent, start, end): last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) next_data = self._model.data(self._model.index(start, 0, c.parent)) - if c.parent.isValid(): - assert parent.isValid() + if QtCore.QT_VERSION >= 0x050000: + # Skipping this on Qt4 as the parent changes for some reason + # see _on_rows_inserted for details assert c.parent == parent - else: - assert not parent.isValid() assert c.oldSize - (end - start + 1) == self._model.rowCount(parent) assert c.last == last_data From bd5b3c7d4fa96646db21bf68994ba932e27f928c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:47:46 +0200 Subject: [PATCH 44/60] Make version checks work with PySide --- pytestqt/modeltest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 05e4859a..3751504e 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -557,7 +557,7 @@ def _on_rows_inserted(self, parent, start, end): ) ) - if QtCore.QT_VERSION >= 0x050000: + if not QtCore.qVersion().startswith('4.') # Skipping this on Qt4 as the parent changes for some reason: # modeltest: rows about to be inserted: [...] # parent (0x7f8f540eacf8), [...] @@ -605,7 +605,7 @@ def _on_rows_removed(self, parent, start, end): last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) next_data = self._model.data(self._model.index(start, 0, c.parent)) - if QtCore.QT_VERSION >= 0x050000: + if not 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 From b7604ef7c25743e41ec55e9b3248de2196858ac8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:50:01 +0200 Subject: [PATCH 45/60] Version fixup --- pytestqt/modeltest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 3751504e..0ab548a3 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -557,7 +557,7 @@ def _on_rows_inserted(self, parent, start, end): ) ) - if not QtCore.qVersion().startswith('4.') + if not QtCore.qVersion().startswith('4.'): # Skipping this on Qt4 as the parent changes for some reason: # modeltest: rows about to be inserted: [...] # parent (0x7f8f540eacf8), [...] @@ -605,7 +605,7 @@ def _on_rows_removed(self, parent, start, end): last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) next_data = self._model.data(self._model.index(start, 0, c.parent)) - if not QtCore.qVersion().startswith('4.') + if not 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 From 5375847341cadc7a0e835bb2b88d01fd24e50884 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 11:52:53 +0200 Subject: [PATCH 46/60] Spelling fix --- pytestqt/modeltest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 0ab548a3..4a9fcafb 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -43,7 +43,7 @@ class ModelTester: - """A tester for Qt's QAbstractItemModel's. + """A tester for Qt's QAbstractItemModels. :ivar bool data_display_may_return_none: if the model implementation is allowed to return None from data() for DisplayRole. From 5eae373002de3c1af1fa46957a8f9f90710d864d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 12:20:46 +0200 Subject: [PATCH 47/60] Add debugging output for rowsRemoved --- pytestqt/modeltest.py | 47 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 4a9fcafb..2715b865 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -530,12 +530,11 @@ def _on_rows_about_to_be_inserted(self, parent, start, end): def _on_rows_inserted(self, parent, start, end): """Confirm that what was said was going to happen actually did.""" - current_size = self._model.rowCount(parent) c = self._insert.pop() - last_data = self._model.data(self._model.index(start - 1, 0, parent)) next_data = self._model.data(self._model.index(end + 1, 0, c.parent)) expected_size = c.oldSize + (end - start + 1) + current_size = self._model.rowCount(parent) self._debug("rows inserted: start {}, end {}".format(start, end)) self._debug(" from rowsAboutToBeInserted: parent {}, " @@ -593,9 +592,23 @@ def _on_rows_about_to_be_removed(self, parent, start, end): This gets stored to make sure it actually happens in rowsRemoved. """ - last_data = self._model.data(self._model.index(start - 1, 0, parent)) - next_data = self._model.data(self._model.index(end + 1, 0, parent)) - c = _Changing(parent=parent, oldSize=self._model.rowCount(parent), + last_index = self._model.index(start - 1, 0, parent) + next_index = self._model.index(end + 1, 0, parent) + parent_rowcount = self._model.rowCount(parent) + + self._debug("rows about to be removed: start {}, end {}, parent {}, " + "parent row count {}, last item {}, next item {}".format( + start, end, + self._modelindex_debug(parent), + parent_rowcount, + self._modelindex_debug(last_index), + self._modelindex_debug(next_index), + ) + ) + + last_data = self._model.data(last_index) + next_data = self._model.data(next_index) + c = _Changing(parent=parent, oldSize=parent_rowcount, last=last_data, next=next_data) self._remove.append(c) @@ -604,13 +617,35 @@ def _on_rows_removed(self, parent, start, end): c = self._remove.pop() last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) next_data = self._model.data(self._model.index(start, 0, c.parent)) + current_size = self._model.rowCount(parent) + expected_size = c.oldSize - (end - start + 1) + + self._debug("rows removed: start {}, end {}".format(start, end)) + self._debug(" from rowsAboutToBeRemoved: parent {}, " + "size {} (-> {} expected), " + "next data {!r}, last data {!r}".format( + self._modelindex_debug(c.parent), + c.oldSize, expected_size, + extract_from_variant(c.next), + extract_from_variant(c.last) + ) + ) + + self._debug(" now in rowsRemoved: parent {}, size {}, " + "next data {!r}, last data {!r}".format( + self._modelindex_debug(parent), + current_size, + extract_from_variant(next_data), + extract_from_variant(last_data) + ) + ) if not 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 - assert c.oldSize - (end - start + 1) == self._model.rowCount(parent) + assert current_size == expected_size assert c.last == last_data assert c.next == next_data From e05bb2b985b71e77271267cd5e93cbe588252f01 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 12:28:17 +0200 Subject: [PATCH 48/60] pep8-ify some identifiers --- pytestqt/modeltest.py | 120 +++++++++++++++++++++--------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 2715b865..cb86ed27 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -38,7 +38,7 @@ QAbstractListModel, QAbstractTableModel -_Changing = collections.namedtuple('_Changing', 'parent, oldSize, last, next') +_Changing = collections.namedtuple('_Changing', 'parent, old_size, last, next') class ModelTester: @@ -211,19 +211,19 @@ def _test_row_count(self): Models that are dynamically populated are not as fully tested here. """ # check top row - topIndex = self._model.index(0, 0, QtCore.QModelIndex()) - rows = self._model.rowCount(topIndex) + top_index = self._model.index(0, 0, QtCore.QModelIndex()) + rows = self._model.rowCount(top_index) assert rows >= 0 if rows > 0: - assert self._has_children(topIndex) + assert self._has_children(top_index) - secondLevelIndex = self._model.index(0, 0, topIndex) - if secondLevelIndex.isValid(): # not the top level + second_level_index = self._model.index(0, 0, top_index) + if second_level_index.isValid(): # not the top level # check a row count where parent is valid - rows = self._model.rowCount(secondLevelIndex) + rows = self._model.rowCount(second_level_index) assert rows >= 0 if rows > 0: - assert self._has_children(secondLevelIndex) + assert self._has_children(second_level_index) # The models rowCount() is tested more extensively in # _check_children(), but this catches the big mistakes @@ -232,13 +232,13 @@ def _test_column_count(self): """Test model's implementation of columnCount() and hasChildren().""" # check top row - topIndex = self._model.index(0, 0, QtCore.QModelIndex()) - assert self._column_count(topIndex) >= 0 + top_index = self._model.index(0, 0, QtCore.QModelIndex()) + assert self._column_count(top_index) >= 0 # check a column count where parent is valid - childIndex = self._model.index(0, 0, topIndex) - if childIndex.isValid(): - assert self._column_count(childIndex) >= 0 + child_index = self._model.index(0, 0, top_index) + if child_index.isValid(): + assert self._column_count(child_index) >= 0 # columnCount() is tested more extensively in _check_children(), # but this catches the big mistakes @@ -297,36 +297,36 @@ def _test_parent(self): if self._model.rowCount() == 0: return - # Column 0 | Column 1 | - # QModelIndex() | | - # \- topIndex | topIndex1 | - # \- childIndex | childIndex1 | + # Column 0 | Column 1 | + # QModelIndex() | | + # \- top_index | top_index_1 | + # \- child_index | child_index_1 | # Common error test #1, make sure that a top level index has a parent # that is a invalid QModelIndex. - topIndex = self._model.index(0, 0, QtCore.QModelIndex()) - assert self._parent(topIndex) == QtCore.QModelIndex() + top_index = self._model.index(0, 0, QtCore.QModelIndex()) + assert self._parent(top_index) == QtCore.QModelIndex() # Common error test #2, make sure that a second level index has a # parent that is the first level index. - if self._model.rowCount(topIndex) > 0: - childIndex = self._model.index(0, 0, topIndex) - assert self._parent(childIndex) == topIndex + if self._model.rowCount(top_index) > 0: + child_index = self._model.index(0, 0, top_index) + assert self._parent(child_index) == top_index # 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. - topIndex1 = self._model.index(0, 1, QtCore.QModelIndex()) - if self._model.rowCount(topIndex1) > 0: - childIndex = self._model.index(0, 0, topIndex) - childIndex1 = self._model.index(0, 0, topIndex1) - assert childIndex != childIndex1 + top_index_1 = self._model.index(0, 1, 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) + assert child_index != child_index_1 # 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()) - def _check_children(self, parent, currentDepth=0): + def _check_children(self, parent, current_depth=0): """Check parent/children relationships. Called from the parent() test. @@ -367,10 +367,10 @@ def _check_children(self, parent, currentDepth=0): assert self._has_children(parent) self._debug("Checking children of {} with depth {} " "({} rows, {} columns)".format( - self._modelindex_debug(parent), currentDepth, + self._modelindex_debug(parent), current_depth, rows, columns)) - topLeftChild = self._model.index(0, 0, parent) + top_left_child = self._model.index(0, 0, parent) assert not self._model.hasIndex(rows + 1, 0, parent) for r in range(rows): @@ -391,8 +391,8 @@ def _check_children(self, parent, currentDepth=0): # index() should always return the same index when called twice # in a row - modifiedIndex = self._model.index(r, c, parent) - assert index == modifiedIndex + modified_index = self._model.index(r, c, parent) + assert index == modified_index # Make sure we get the same index if we request it twice in a # row @@ -400,10 +400,10 @@ def _check_children(self, parent, currentDepth=0): b = self._model.index(r, c, parent) assert a == b - sibling = self._model.sibling(r, c, topLeftChild) + sibling = self._model.sibling(r, c, top_left_child) assert index == sibling - sibling = topLeftChild.sibling(r, c) + sibling = top_left_child.sibling(r, c) assert index == sibling # Some basic checking on the index that is returned @@ -436,20 +436,20 @@ def _check_children(self, parent, currentDepth=0): assert self._parent(index) == parent # recursively go down the children - if self._has_children(index) and currentDepth < 10: + if self._has_children(index) and current_depth < 10: self._debug("{} has {} children".format( self._modelindex_debug(index), self._model.rowCount(index) )) - self._check_children(index, currentDepth + 1) - # elif currentDepth >= 10: + self._check_children(index, current_depth + 1) + # elif current_depth >= 10: # print("checked 10 deep") # FIXME # make sure that after testing the children that the index # doesn't change. - newerIndex = self._model.index(r, c, parent) - assert index == newerIndex + newer_index = self._model.index(r, c, parent) + assert index == newer_index self._debug("Children check for {} done".format(self._modelindex_debug(parent))) def _test_data(self): @@ -524,7 +524,7 @@ def _on_rows_about_to_be_inserted(self, parent, start, end): last_data = self._model.data(last_index) next_data = self._model.data(next_index) - c = _Changing(parent=parent, oldSize=parent_rowcount, + c = _Changing(parent=parent, old_size=parent_rowcount, last=last_data, next=next_data) self._insert.append(c) @@ -533,7 +533,7 @@ def _on_rows_inserted(self, parent, start, end): c = self._insert.pop() last_data = self._model.data(self._model.index(start - 1, 0, parent)) next_data = self._model.data(self._model.index(end + 1, 0, c.parent)) - expected_size = c.oldSize + (end - start + 1) + expected_size = c.old_size + (end - start + 1) current_size = self._model.rowCount(parent) self._debug("rows inserted: start {}, end {}".format(start, end)) @@ -541,7 +541,7 @@ def _on_rows_inserted(self, parent, start, end): "size {} (-> {} expected), " "next data {!r}, last data {!r}".format( self._modelindex_debug(c.parent), - c.oldSize, expected_size, + c.old_size, expected_size, extract_from_variant(c.next), extract_from_variant(c.last) ) @@ -608,7 +608,7 @@ def _on_rows_about_to_be_removed(self, parent, start, end): last_data = self._model.data(last_index) next_data = self._model.data(next_index) - c = _Changing(parent=parent, oldSize=parent_rowcount, + c = _Changing(parent=parent, old_size=parent_rowcount, last=last_data, next=next_data) self._remove.append(c) @@ -618,14 +618,14 @@ def _on_rows_removed(self, parent, start, end): last_data = self._model.data(self._model.index(start - 1, 0, c.parent)) next_data = self._model.data(self._model.index(start, 0, c.parent)) current_size = self._model.rowCount(parent) - expected_size = c.oldSize - (end - start + 1) + expected_size = c.old_size - (end - start + 1) self._debug("rows removed: start {}, end {}".format(start, end)) self._debug(" from rowsAboutToBeRemoved: parent {}, " "size {} (-> {} expected), " "next data {!r}, last data {!r}".format( self._modelindex_debug(c.parent), - c.oldSize, expected_size, + c.old_size, expected_size, extract_from_variant(c.next), extract_from_variant(c.last) ) @@ -649,17 +649,17 @@ def _on_rows_removed(self, parent, start, end): assert c.last == last_data assert c.next == next_data - def _on_data_changed(self, topLeft, bottomRight): - assert topLeft.isValid() - assert bottomRight.isValid() - commonParent = bottomRight.parent() - assert topLeft.parent() == commonParent - assert topLeft.row() <= bottomRight.row() - assert topLeft.column() <= bottomRight.column() - rowCount = self._model.rowCount(commonParent) - columnCount = self._column_count(commonParent) - assert bottomRight.row() < rowCount - assert bottomRight.column() < columnCount + def _on_data_changed(self, top_left, bottom_right): + assert top_left.isValid() + assert bottom_right.isValid() + common_parent = bottom_right.parent() + assert top_left.parent() == common_parent + assert top_left.row() <= bottom_right.row() + assert top_left.column() <= bottom_right.column() + row_count = self._model.rowCount(common_parent) + column_count = self._column_count(common_parent) + assert bottom_right.row() < row_count + assert bottom_right.column() < column_count def _on_header_data_changed(self, orientation, start, end): assert orientation in [QtCore.Qt.Horizontal, QtCore.Qt.Vertical] @@ -667,11 +667,11 @@ def _on_header_data_changed(self, orientation, start, end): assert end >= 0 assert start <= end if orientation == QtCore.Qt.Vertical: - itemCount = self._model.rowCount() + item_count = self._model.rowCount() else: - itemCount = self._column_count() - assert start < itemCount - assert end < itemCount + item_count = self._column_count() + assert start < item_count + assert end < item_count def _column_count(self, parent=QtCore.QModelIndex()): """ From 0590fbac538657f7249d93fff652728ffb0f418f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 12:46:54 +0200 Subject: [PATCH 49/60] Improve test coverage --- tests/test_modeltest.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 51334eba..ec9c42e8 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -32,13 +32,15 @@ def test_standard_item_model(qtmodeltester): Basic test which uses qtmodeltester with a QStandardItemModel. """ model = QStandardItemModel() - items = [QStandardItem(str(i)) for i in range(5)] - items[0].setChild(0, items[4]) + items = [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]) model.setItem(1, 1, items[3]) + items[0].setChild(0, items[4]) + items[4].setChild(0, items[5]) + qtmodeltester.check(model, verbose=True) @@ -260,3 +262,24 @@ def fetchMore(self, parent): item = QStandardItem('foo') model.setItem(0, 0, item) qtmodeltester.check(model, verbose=True) + + +@pytest.mark.parametrize('verbose', [True, False]) +def test_verbosity(testdir, verbose): + testdir.makepyfile(""" + from pytestqt.qt_compat import QStandardItemModel + + def test_foo(qtmodeltester): + model = QStandardItemModel() + qtmodeltester.check(model) + assert False + """) + + if verbose: + res = testdir.runpytest('-v') + assert 'model check running non-verbose' not in res.stdout.str() + else: + res = testdir.runpytest() + res.stdout.fnmatch_lines([ + 'model check running non-verbose, *' + ]) From ecc34f94694a7ae42940be79279ab485d5944fd1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 12:47:01 +0200 Subject: [PATCH 50/60] Fix off-by-one error --- pytestqt/modeltest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index cb86ed27..11a780eb 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -567,7 +567,7 @@ def _on_rows_inserted(self, parent, start, end): # parent (0x7f8f60a96cf8) [...] assert c.parent == parent - for ii in range(start, end): + for ii in range(start, end + 1): idx = self._model.index(ii, 0, parent) self._debug(" item {} inserted: {}".format(ii, self._modelindex_debug(idx))) From 1c31124bc9731f28c41546513118cad229371f7e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 12:50:31 +0200 Subject: [PATCH 51/60] Fix checking for alignment roles --- pytestqt/modeltest.py | 2 +- tests/test_modeltest.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 11a780eb..a7cd2085 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -491,7 +491,7 @@ def _test_data(self): if alignment is not None: try: alignment = int(alignment) - except TypeError: + except (TypeError, ValueError): assert 0, '%r should be a TextAlignmentRole enum' % alignment mask = int(QtCore.Qt.AlignHorizontal_Mask | QtCore.Qt.AlignVertical_Mask) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index ec9c42e8..4bdbde34 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -104,6 +104,8 @@ def data(self, index=QtCore.QModelIndex(), role=QtCore.Qt.DisplayRole): (QtCore.Qt.AlignLeft, True), (QtCore.Qt.AlignRight, True), (0xFFFFFF, False), + ('foo', False), + (object(), False), ]) def test_data_alignment(role_value, should_pass, check_model): """Test a custom model which returns a good and alignments from data(). From 5e158dad24360389bc6a497d39ddf380265b4d1a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 13:56:48 +0200 Subject: [PATCH 52/60] Add a test for a model returning an invalid parent --- pytestqt/modeltest.py | 2 +- tests/test_modeltest.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index a7cd2085..d57ff67a 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -418,7 +418,7 @@ def _check_children(self, parent, current_depth=0): # If the next test fails here is some somewhat useful debug you # play with. - if self._parent(index) != parent: # pragma: no cover + if self._parent(index) != parent: self._debug( "parent-check failed for index {}:\n" " parent {} != expected {}".format( diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 4bdbde34..3fa3e3e3 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -266,6 +266,28 @@ def fetchMore(self, parent): qtmodeltester.check(model, verbose=True) +def test_invalid_parent(qtmodeltester): + + class Model(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() + + model = Model() + item = QStandardItem('foo') + item2 = QStandardItem('bar') + item3 = QStandardItem('bar') + model.setItem(0, 0, item) + item.setChild(0, item2) + item2.setChild(0, item3) + + with pytest.raises(AssertionError): + qtmodeltester.check(model, verbose=True) + + @pytest.mark.parametrize('verbose', [True, False]) def test_verbosity(testdir, verbose): testdir.makepyfile(""" From 161c9f07370ec84a4816081938a81c4147c61ed4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 13:57:26 +0200 Subject: [PATCH 53/60] Fix indentation --- tests/test_modeltest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 3fa3e3e3..3a548909 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -306,4 +306,4 @@ def test_foo(qtmodeltester): res = testdir.runpytest() res.stdout.fnmatch_lines([ 'model check running non-verbose, *' - ]) + ]) From 3e5669eaf72028a7a05357f2b4a31850d1c3d3f8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 13:58:20 +0200 Subject: [PATCH 54/60] Remove some commented-out code --- pytestqt/modeltest.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index d57ff67a..bf448c6e 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -427,10 +427,6 @@ def _check_children(self, parent, current_depth=0): self._modelindex_debug(parent) ) ) - # And a view that you can even use to show the model. - # QTreeView view - # view.setModel(self._model) - # view.show() # Check that we can get back our real parent. assert self._parent(index) == parent @@ -442,9 +438,6 @@ def _check_children(self, parent, current_depth=0): self._model.rowCount(index) )) self._check_children(index, current_depth + 1) - # elif current_depth >= 10: - # print("checked 10 deep") - # FIXME # make sure that after testing the children that the index # doesn't change. From 8a3466b634bbdd4be84f19e7108954af07d30b47 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 14:23:40 +0200 Subject: [PATCH 55/60] Comment/docstring style fixes --- pytestqt/modeltest.py | 50 ++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index bf448c6e..39814c09 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -170,11 +170,10 @@ def _run(self): self._test_data() def _test_basic(self): - """ - Try to call a number of the basic functions (not all). + """Try to call a number of the basic functions (not all). Make sure the model doesn't outright segfault, testing the functions - that makes sense. + which make sense. """ assert self._model.buddy(QtCore.QModelIndex()) == QtCore.QModelIndex() self._model.canFetchMore(QtCore.QModelIndex()) @@ -209,6 +208,9 @@ def _test_row_count(self): """Test model's implementation of rowCount() and hasChildren(). Models that are dynamically populated are not as fully tested here. + + The models rowCount() is tested more extensively in _check_children(), + but this catches the big mistakes. """ # check top row top_index = self._model.index(0, 0, QtCore.QModelIndex()) @@ -225,12 +227,12 @@ def _test_row_count(self): if rows > 0: assert self._has_children(second_level_index) - # The models rowCount() is tested more extensively in - # _check_children(), but this catches the big mistakes - def _test_column_count(self): - """Test model's implementation of columnCount() and hasChildren().""" + """Test model's implementation of columnCount() and hasChildren(). + columnCount() is tested more extensively in _check_children(), + but this catches the big mistakes. + """ # check top row top_index = self._model.index(0, 0, QtCore.QModelIndex()) assert self._column_count(top_index) >= 0 @@ -240,12 +242,13 @@ def _test_column_count(self): if child_index.isValid(): assert self._column_count(child_index) >= 0 - # columnCount() is tested more extensively in _check_children(), - # but this catches the big mistakes - def _test_has_index(self): - """Test model's implementation of hasIndex().""" - # Make sure that invalid values returns an invalid index + """Test model's implementation of hasIndex(). + + hasIndex() is tested more extensively in _check_children(), + but this catches the big mistakes. + """ + # Make sure that invalid values return an invalid index assert not self._model.hasIndex(-2, -2) assert not self._model.hasIndex(-2, 0) assert not self._model.hasIndex(0, -2) @@ -260,12 +263,13 @@ def _test_has_index(self): if rows > 0: assert self._model.hasIndex(0, 0) - # hasIndex() is tested more extensively in _check_children(), - # but this catches the big mistakes - def _test_index(self): - """Test model's implementation of index()""" - # Make sure that invalid values returns an invalid index + """Test model's implementation of index(). + + index() is tested more extensively in _check_children(), + 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() @@ -285,11 +289,8 @@ def _test_index(self): b = self._model.index(0, 0) assert a == b - # index() is tested more extensively in _check_children(), - # but this catches the big mistakes - def _test_parent(self): - """Tests model's implementation of QAbstractItemModel::parent()""" + """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() @@ -342,7 +343,6 @@ def _check_children(self, parent, current_depth=0): tests should have already found the basic bugs because it is easier to figure out the problem in those tests then this one. """ - # First just try walking back up the tree. p = parent while p.isValid(): @@ -385,10 +385,6 @@ def _check_children(self, parent, current_depth=0): # rowCount() and columnCount() said that it existed... assert index.isValid() - # sanity checks - assert index.column() == c - assert index.row() == r - # index() should always return the same index when called twice # in a row modified_index = self._model.index(r, c, parent) @@ -472,7 +468,7 @@ def _test_data(self): (QtCore.Qt.TextColorRole, QtGui.QColor), ] - # General Purpose roles that should return a QString + # General purpose roles with a fixed expected type for role, typ in types: data = self._model.data(self._model.index(0, 0), role) assert data == None or isinstance(data, typ), role From 25376518d68cab08c13cb43a607637eb78f434a2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 14:25:59 +0200 Subject: [PATCH 56/60] Clean up handling of fetchMore --- pytestqt/modeltest.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index 39814c09..a0008ed8 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -159,6 +159,7 @@ def _cleanup(self): def _run(self): assert self._model is not None + assert self._fetching_more is not None if self._fetching_more: return self._test_basic() @@ -182,9 +183,7 @@ def _test_basic(self): QtCore.Qt.DisplayRole) assert extract_from_variant(display_data) is None - self._fetching_more = True - self._model.fetchMore(QtCore.QModelIndex()) - self._fetching_more = False + self._fetch_more(QtCore.QModelIndex()) flags = self._model.flags(QtCore.QModelIndex()) assert flags == QtCore.Qt.ItemIsDropEnabled or not flags self._has_children(QtCore.QModelIndex()) @@ -350,9 +349,7 @@ def _check_children(self, parent, current_depth=0): # For models that are dynamically populated if self._model.canFetchMore(parent): - self._fetching_more = True - self._model.fetchMore(parent) - self._fetching_more = False + self._fetch_more(parent) rows = self._model.rowCount(parent) columns = self._column_count(parent) @@ -375,9 +372,7 @@ def _check_children(self, parent, current_depth=0): assert not self._model.hasIndex(rows + 1, 0, parent) for r in range(rows): if self._model.canFetchMore(parent): - self._fetching_more = True - self._model.fetchMore(parent) - self._fetching_more = False + self._fetch_more(parent) assert not self._model.hasIndex(r, columns + 1, parent) for c in range(columns): assert self._model.hasIndex(r, c, parent) @@ -691,3 +686,9 @@ def _has_children(self, parent=QtCore.QModelIndex()): return parent == QtCore.QModelIndex() and self._model.rowCount() > 0 else: return self._model.hasChildren(parent) + + def _fetch_more(self, parent): + """Call ``fetchMore`` on the model and set ``self._fetching_more``.""" + self._fetching_more = True + self._model.fetchMore(parent) + self._fetching_more = False From 340435173e012a96be54459d9342e368a2c5dcfe Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 14:52:40 +0200 Subject: [PATCH 57/60] Adjust versionadded --- docs/modeltester.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modeltester.rst b/docs/modeltester.rst index 6eec65eb..003ca8ae 100644 --- a/docs/modeltester.rst +++ b/docs/modeltester.rst @@ -1,7 +1,7 @@ Model Tester ============ -.. versionadded:: 1.6 +.. versionadded:: 2.0 ``pytest-qt`` includes a fixture that helps testing `QAbstractItemModel`_ implementations. The implementation is copied @@ -52,4 +52,4 @@ thanks! .. _Florian Bruhin: https://github.com/The-Compiler -.. _QAbstractItemModel: http://doc.qt.io/qt-5/qabstractitemmodel.html \ No newline at end of file +.. _QAbstractItemModel: http://doc.qt.io/qt-5/qabstractitemmodel.html From 5c758c5034aaf3ca3bebece02234ecb069df7f69 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 16:04:44 +0200 Subject: [PATCH 58/60] Get rid of 'verbose' argument The output will only be shown on errors anyways, and it's very useful if there's a problem. --- pytestqt/modeltest.py | 14 ++--------- tests/test_modeltest.py | 53 +++++++++++++---------------------------- 2 files changed, 18 insertions(+), 49 deletions(-) diff --git a/pytestqt/modeltest.py b/pytestqt/modeltest.py index a0008ed8..1fd2d7ec 100644 --- a/pytestqt/modeltest.py +++ b/pytestqt/modeltest.py @@ -55,12 +55,10 @@ def __init__(self, config): self._insert = None self._remove = None self._changing = [] - self._verbose = config.getoption('verbose') > 0 self.data_display_may_return_none = False def _debug(self, text): - if self._verbose: - print('modeltest: ' + text) + print('modeltest: ' + text) def _modelindex_debug(self, index): """Get a string for debug output for a QModelIndex.""" @@ -73,7 +71,7 @@ def _modelindex_debug(self, index): extract_from_variant(data), id(index)) - def check(self, model, verbose=None): + def check(self, model): """Runs a series of checks in the given model. Connect to all of the models signals. @@ -87,14 +85,6 @@ def check(self, model, verbose=None): self._remove = [] self._changing = [] - if verbose is not None: - self._verbose = verbose - - if not self._verbose: - print("model check running non-verbose, run pytest with -v or use " - "qtmodeltester.check(model, verbose=True) for more " - "information") - self._model.columnsAboutToBeInserted.connect(self._run) self._model.columnsAboutToBeRemoved.connect(self._run) self._model.columnsInserted.connect(self._run) diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 3a548909..7f239e32 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -41,7 +41,7 @@ def test_standard_item_model(qtmodeltester): items[0].setChild(0, items[4]) items[4].setChild(0, items[5]) - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) @pytest.mark.xfail(run=False, reason='Makes pytest hang') @@ -51,15 +51,15 @@ def test_file_system_model(qtmodeltester, tmpdir): tmpdir.ensure('file2.py') model = QFileSystemModel() model.setRootPath(str(tmpdir)) - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) tmpdir.ensure('file3.py') - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) def test_string_list_model(qtmodeltester): model = QStringListModel() model.setStringList(['hello', 'world']) - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) def test_sort_filter_proxy_model(qtmodeltester): @@ -67,7 +67,7 @@ def test_sort_filter_proxy_model(qtmodeltester): model.setStringList(['hello', 'world']) proxy = QSortFilterProxyModel() proxy.setSourceModel(model) - qtmodeltester.check(proxy, verbose=True) + qtmodeltester.check(proxy) @pytest.mark.parametrize('broken_role', [ @@ -162,10 +162,10 @@ def check_model(qtmodeltester): """ def check(model, should_pass=True): if should_pass: - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) else: with pytest.raises(AssertionError): - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) return check @@ -178,13 +178,13 @@ def columnCount(self, parent=QtCore.QModelIndex()): model = Model() with pytest.raises(AssertionError): - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) def test_changing_model_insert(qtmodeltester): model = QStandardItemModel() item = QStandardItem('foo') - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) model.insertRow(0, item) @@ -192,7 +192,7 @@ def test_changing_model_remove(qtmodeltester): model = QStandardItemModel() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) model.removeRow(0) @@ -200,7 +200,7 @@ def test_changing_model_data(qtmodeltester): model = QStandardItemModel() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) model.setData(model.index(0, 0), 'hello world') @@ -210,7 +210,7 @@ def test_changing_model_header_data(qtmodeltester, orientation): model = QStandardItemModel() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) model.setHeaderData(0, orientation, 'blah') @@ -219,7 +219,7 @@ def test_changing_model_sort(qtmodeltester): model = QStandardItemModel() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) model.sort(0) @@ -246,7 +246,7 @@ def rowCount(self, parent=None): model = Model() assert not model.row_count_did_run - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) assert model.row_count_did_run @@ -263,7 +263,7 @@ def fetchMore(self, parent): model = Model() item = QStandardItem('foo') model.setItem(0, 0, item) - qtmodeltester.check(model, verbose=True) + qtmodeltester.check(model) def test_invalid_parent(qtmodeltester): @@ -285,25 +285,4 @@ def parent(self, index): item2.setChild(0, item3) with pytest.raises(AssertionError): - qtmodeltester.check(model, verbose=True) - - -@pytest.mark.parametrize('verbose', [True, False]) -def test_verbosity(testdir, verbose): - testdir.makepyfile(""" - from pytestqt.qt_compat import QStandardItemModel - - def test_foo(qtmodeltester): - model = QStandardItemModel() - qtmodeltester.check(model) - assert False - """) - - if verbose: - res = testdir.runpytest('-v') - assert 'model check running non-verbose' not in res.stdout.str() - else: - res = testdir.runpytest() - res.stdout.fnmatch_lines([ - 'model check running non-verbose, *' - ]) + qtmodeltester.check(model) From 0f3ce427929724971aed9d21348bec884ac67d5d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 18:17:44 +0200 Subject: [PATCH 59/60] Fix typos in docs [ci skip] --- docs/modeltester.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modeltester.rst b/docs/modeltester.rst index 003ca8ae..f56e0ed6 100644 --- a/docs/modeltester.rst +++ b/docs/modeltester.rst @@ -19,7 +19,7 @@ Some of the conditions caught include: * ``hasChildren()`` returns true if ``rowCount()`` is greater then zero. * and many more... -To use it, create a instance of your model implementation, fill it with some +To use it, create an instance of your model implementation, fill it with some items and call ``qtmodeltester.check``: .. code-block:: python @@ -36,7 +36,7 @@ items and call ``qtmodeltester.check``: If the tester finds a problem the test will fail with an assert pinpointing the issue. -The following attribute that may influence the outcome of the check depending +The following attribute may influence the outcome of the check depending on your model implementation: * ``data_display_may_return_none`` (default: ``False``): While you can From 444c95865db1b8ce6d25014019313a3439cb7c06 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 29 Jun 2016 20:02:23 +0200 Subject: [PATCH 60/60] Remove test_file_system_model setRootPath doesn't actually change the data represented by the model, i.e. the test will still end up iterating over the root filesystem. Due to symlinks (?) or other funny stuff this could take a long time or do other funny things, so let's get rid of it. --- pytestqt/qt_compat.py | 3 --- tests/test_modeltest.py | 16 ++-------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/pytestqt/qt_compat.py b/pytestqt/qt_compat.py index c5934422..5a988c7c 100644 --- a/pytestqt/qt_compat.py +++ b/pytestqt/qt_compat.py @@ -109,7 +109,6 @@ def _import_module(module_name): QStandardItem = QtGui.QStandardItem QStandardItemModel = QtGui.QStandardItemModel - QFileSystemModel = QtGui.QFileSystemModel QStringListModel = QtGui.QStringListModel QSortFilterProxyModel = QtGui.QSortFilterProxyModel QAbstractListModel = QtCore.QAbstractListModel @@ -138,7 +137,6 @@ def get_versions(): QWidget = _QtWidgets.QWidget qInstallMessageHandler = QtCore.qInstallMessageHandler - QFileSystemModel = _QtWidgets.QFileSystemModel QStringListModel = QtCore.QStringListModel QSortFilterProxyModel = QtCore.QSortFilterProxyModel @@ -154,7 +152,6 @@ def extract_from_variant(variant): QWidget = QtGui.QWidget qInstallMsgHandler = QtCore.qInstallMsgHandler - QFileSystemModel = QtGui.QFileSystemModel QStringListModel = QtGui.QStringListModel QSortFilterProxyModel = QtGui.QSortFilterProxyModel diff --git a/tests/test_modeltest.py b/tests/test_modeltest.py index 7f239e32..474e05fb 100644 --- a/tests/test_modeltest.py +++ b/tests/test_modeltest.py @@ -3,8 +3,8 @@ import pytest from pytestqt.qt_compat import QStandardItemModel, QStandardItem, \ - QFileSystemModel, QStringListModel, QSortFilterProxyModel, QT_API, \ - QAbstractListModel, QtCore + QStringListModel, QSortFilterProxyModel, QT_API, QAbstractListModel, \ + QtCore pytestmark = pytest.mark.usefixtures('qtbot') @@ -44,18 +44,6 @@ def test_standard_item_model(qtmodeltester): qtmodeltester.check(model) -@pytest.mark.xfail(run=False, reason='Makes pytest hang') -def test_file_system_model(qtmodeltester, tmpdir): - tmpdir.ensure('directory', dir=1) - tmpdir.ensure('file1.txt') - tmpdir.ensure('file2.py') - model = QFileSystemModel() - model.setRootPath(str(tmpdir)) - qtmodeltester.check(model) - tmpdir.ensure('file3.py') - qtmodeltester.check(model) - - def test_string_list_model(qtmodeltester): model = QStringListModel() model.setStringList(['hello', 'world'])