Skip to content

Commit

Permalink
Merge pull request #5 from astrofrog/patch-qcombobox
Browse files Browse the repository at this point in the history
Patch QComboBox and re-organize tests
  • Loading branch information
astrofrog committed Jan 19, 2016
2 parents 1af13e7 + 9d72955 commit 8102c34
Show file tree
Hide file tree
Showing 6 changed files with 333 additions and 183 deletions.
10 changes: 6 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ env:
- PIP_DEPENDENCIES='pytest-cov coveralls'
- CONDA_CHANNELS='astrofrog'
- PYTHON_VERSION=2.7
- TEST_FILES='test_qt_helpers.py'
matrix:
- QT_VER="QT4" CONDA_DEPENDENCIES='mock pytest pip coverage pyyaml requests pyqt pyside'
- QT_VER="QT5" CONDA_DEPENDENCIES='mock pytest pip coverage pyyaml requests pyqt5'
- CONDA_DEPENDENCIES='mock pytest pip coverage pyyaml requests pyside'
- CONDA_DEPENDENCIES='mock pytest pip coverage pyyaml requests pyqt'
- CONDA_DEPENDENCIES='mock pytest pip coverage pyyaml requests pyqt5'
- CONDA_DEPENDENCIES='mock pytest pip coverage pyyaml requests pyqt pyside' TEST_FILES='test_qt_helpers.py test_switch.py'

before_install:

Expand All @@ -25,8 +28,7 @@ before_install:
- sh -e /etc/init.d/xvfb start

script:
- if [ $QT_VER == QT4 ]; then py.test --cov qt_helpers.py test_qt_helpers.py; fi
- if [ $QT_VER == QT5 ]; then py.test --cov qt_helpers.py test_qt_helpers_qt5.py; fi
- py.test --cov qt_helpers.py $TEST_FILES

after_success:
- coveralls
11 changes: 8 additions & 3 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@ environment:
PYTHON_ARCH: "64" # needs to be set for CMD_IN_ENV to succeed. If a mix
# of 32 bit and 64 bit builds are needed, move this
# to the matrix section.
CONDA_DEPENDENCIES: "mock pytest pyqt pyside"
PYTHON_VERSION: "2.7"
TEST_FILES: "test_qt_helpers.py"

matrix:
- PYTHON_VERSION: "2.7"
- CONDA_DEPENDENCIES: "mock pytest pyside"
- CONDA_DEPENDENCIES: "mock pytest pyqt"
- CONDA_DEPENDENCIES: "mock pytest pyqt pyside"
TEST_FILES: "test_qt_helpers.py test_switch.py"


platform:
-x64
Expand All @@ -32,5 +37,5 @@ install:
build: false

test_script:
- "%CMD_IN_ENV% py.test test_qt_helpers.py"
- "%CMD_IN_ENV% py.test %TEST_FILES%"

69 changes: 68 additions & 1 deletion qt_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ def reload_qt():
" Encountered the following errors: %s" %
'\n'.join(msgs))

# We patch this only now, once QtCore and QtGui are defined
if is_pyside() or is_pyqt4():
patch_qcombobox()


def load_ui(path, parent=None, custom_widgets=None):
if is_pyside():
Expand Down Expand Up @@ -305,10 +309,73 @@ def get_qapp(icon_path=None):
# Make sure we use high resolution icons with PyQt5 for HDPI
# displays. TODO: check impact on non-HDPI displays.
if is_pyqt5():
qapp.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps);
qapp.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps)

return qapp


def patch_qcombobox():

# In PySide, using Python objects as userData in QComboBox causes
# Segmentation faults under certain conditions. Even in cases where it
# doesn't, findData does not work correctly. Likewise, findData also
# does not work correctly with Python objects when using PyQt4. On the
# other hand, PyQt5 deals with this case correctly. We therefore patch
# QComboBox when using PyQt4 and PySide to avoid issues.

class userDataWrapper(QtCore.QObject):
def __init__(self, data, parent=None):
super(userDataWrapper, self).__init__(parent)
self.data = data

_addItem = QtGui.QComboBox.addItem

def addItem(self, *args, **kwargs):
if len(args) == 3 or (not isinstance(args[0], QtGui.QIcon)
and len(args) == 2):
args, kwargs['userData'] = args[:-1], args[-1]
if 'userData' in kwargs:
kwargs['userData'] = userDataWrapper(kwargs['userData'],
parent=self)
_addItem(self, *args, **kwargs)

_insertItem = QtGui.QComboBox.insertItem

def insertItem(self, *args, **kwargs):
if len(args) == 4 or (not isinstance(args[1], QtGui.QIcon)
and len(args) == 3):
args, kwargs['userData'] = args[:-1], args[-1]
if 'userData' in kwargs:
kwargs['userData'] = userDataWrapper(kwargs['userData'],
parent=self)
_insertItem(self, *args, **kwargs)

_setItemData = QtGui.QComboBox.setItemData

def setItemData(self, index, value, role=QtCore.Qt.UserRole):
value = userDataWrapper(value, parent=self)
_setItemData(self, index, value, role=role)

_itemData = QtGui.QComboBox.itemData

def itemData(self, index, role=QtCore.Qt.UserRole):
userData = _itemData(self, index, role=role)
if isinstance(userData, userDataWrapper):
userData = userData.data
return userData

def findData(self, value):
for i in range(self.count()):
if self.itemData(i) == value:
return i
return -1

QtGui.QComboBox.addItem = addItem
QtGui.QComboBox.insertItem = insertItem
QtGui.QComboBox.setItemData = setItemData
QtGui.QComboBox.itemData = itemData
QtGui.QComboBox.findData = findData


# Now load default Qt
reload_qt()
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"Development Status :: 4 - Beta",
"Programming Language :: Python",
"License :: OSI Approved :: BSD License",
]
)
]
)
249 changes: 76 additions & 173 deletions test_qt_helpers.py
Original file line number Diff line number Diff line change
@@ -1,174 +1,77 @@
from __future__ import absolute_import, division, print_function

import os
import sys
import time

import qt_helpers as qt

import pytest
from mock import MagicMock


class TestQT(object):

def teardown_class(cls):
for m in sys.modules.keys():
if m.startswith('PyQt4') or m.startswith('PySide'):
sys.modules.pop(m)

def setup_method(self, method):
qt.deny_module(None)
if 'QT_API' in os.environ:
os.environ.pop('QT_API')

def test_defaults_to_qt4(self):
qt.reload_qt()
assert qt.QT_API == qt.QT_API_PYQT4

def _load_qt4(self):
os.environ['QT_API'] = qt.QT_API_PYQT4
qt.reload_qt()

def _load_pyside(self):
os.environ['QT_API'] = qt.QT_API_PYSIDE
qt.reload_qt()

def test_overridden_with_env(self):
os.environ['QT_API'] = qt.QT_API_PYSIDE
qt.reload_qt()
assert qt.QT_API == qt.QT_API_PYSIDE

def test_main_import(self):
self._load_qt4()
from qt_helpers import QtCore
from qt_helpers import QtGui

from PyQt4 import QtCore as core, QtGui as gui
assert QtCore is core
assert QtGui is gui

self._load_pyside()
from qt_helpers import QtCore
from qt_helpers import QtGui

from PySide import QtCore as core, QtGui as gui
assert QtCore is core
assert QtGui is gui

def test_load_ui_qt4(self):
self._load_qt4()
from qt_helpers import load_ui, get_qapp
app = get_qapp()
load_ui('test.ui')
app.quit()
del app

def test_load_ui_pyside(self):
self._load_pyside()
from qt_helpers import load_ui, get_qapp
app = get_qapp()
load_ui('test.ui')
app.exit()
app.quit()
del app

def test_submodule_import(self):
self._load_qt4()
from qt_helpers.QtGui import QMessageBox
from qt_helpers.QtCore import Qt
from PyQt4.QtGui import QMessageBox as qmb
from PyQt4.QtCore import Qt as _qt
assert qmb is QMessageBox
assert _qt is Qt

self._load_pyside()
from qt_helpers.QtGui import QMessageBox
from qt_helpers.QtCore import Qt

from PySide.QtGui import QMessageBox as qmb
from PySide.QtCore import Qt as _qt
assert qmb is QMessageBox
assert _qt is Qt

def test_signal_slot_property(self):
self._load_qt4()
from qt_helpers.QtCore import Signal, Slot, Property

def test_qt4_unavailable(self):
import PyQt4
try:
sys.modules['PyQt4'] = None
self._load_qt4()
assert qt.QT_API == qt.QT_API_PYSIDE
finally:
sys.modules['PyQt4'] = PyQt4

def test_pyside_unavailable(self):
import PySide
try:
sys.modules['PySide'] = None
self._load_pyside()
assert qt.QT_API == qt.QT_API_PYQT4
finally:
sys.modules['PySide'] = PySide

def test_both_unavailable(self):
import PySide
import PyQt4
try:
sys.modules['PySide'] = None
sys.modules['PyQt4'] = None
with pytest.raises(ImportError) as e:
qt.reload_qt()
finally:
sys.modules['PySide'] = PySide
sys.modules['PyQt4'] = PyQt4

def test_launch_after_reload(self):

os.environ['QT_API'] = qt.QT_API_PYSIDE
qt.reload_qt()

from qt_helpers import QtCore
from qt_helpers import QtGui

app = qt.get_qapp()
widget = QtGui.QMessageBox()
widget.show()
app.flush()
time.sleep(0.1)
app.quit()

del app

os.environ['QT_API'] = qt.QT_API_PYQT4
qt.reload_qt()

from qt_helpers import QtCore
from qt_helpers import QtGui

app = qt.get_qapp()
widget = QtGui.QMessageBox()
widget.show()
app.flush()
time.sleep(0.1)
app.quit()

del app

os.environ['QT_API'] = qt.QT_API_PYSIDE
qt.reload_qt()

from qt_helpers import QtCore
from qt_helpers import QtGui

app = qt.get_qapp()
widget = QtGui.QMessageBox()
widget.show()
app.flush()
time.sleep(0.1)
app.quit()

del app
# Tests common to all Qt packages

import qt_helpers


def test_patched_qcombobox():

# This test ensures that patch_qcombobox works correctly. See the comments
# in that function to understand the issue.

# The __getitem__ is needed in order to reproduce the Segmentation Fault
class Data(object):
def __getitem__(self, item):
raise ValueError("Failing")

data1 = Data()
data2 = Data()
data3 = Data()
data4 = Data()
data5 = Data()
data6 = Data()

icon1 = qt_helpers.QtGui.QIcon()
icon2 = qt_helpers.QtGui.QIcon()

app = qt_helpers.get_qapp()

widget = qt_helpers.QtGui.QComboBox()
widget.addItem('a', data1)
widget.insertItem(0, 'b', data2)
widget.addItem('c', data1)
widget.setItemData(2, data3)
widget.addItem(icon1, 'd', data4)
widget.insertItem(3, icon2, 'e', data5)
widget.addItem(icon1, 'f')
widget.insertItem(5, icon2, 'g')

widget.show()

assert widget.findData(data1) == 1
assert widget.findData(data2) == 0
assert widget.findData(data3) == 2
assert widget.findData(data4) == 4
assert widget.findData(data5) == 3
assert widget.findData(data6) == -1

assert widget.itemData(0) == data2
assert widget.itemData(1) == data1
assert widget.itemData(2) == data3
assert widget.itemData(3) == data5
assert widget.itemData(4) == data4
assert widget.itemData(5) is None
assert widget.itemData(6) is None

assert widget.itemText(0) == 'b'
assert widget.itemText(1) == 'a'
assert widget.itemText(2) == 'c'
assert widget.itemText(3) == 'e'
assert widget.itemText(4) == 'd'
assert widget.itemText(5) == 'g'
assert widget.itemText(6) == 'f'


def test_main_import():
from qt_helpers import QtCore
from qt_helpers import QtGui


def test_submodule_import():
from qt_helpers.QtGui import QMessageBox
from qt_helpers.QtCore import Qt


def test_load_ui():
from qt_helpers import load_ui, get_qapp
qpp = get_qapp()
load_ui('test.ui')
Loading

0 comments on commit 8102c34

Please sign in to comment.