diff --git a/Dockerfile b/Dockerfile index 7b8c470e..a34cbe68 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ FROM ubuntu:14.04 RUN apt-get update && apt-get install -y \ python-qt4 \ python-pyside \ - python-pip + python-pip \ + xvfb # Nose is the Python test-runner RUN pip install nose nosepipe @@ -11,9 +12,14 @@ RUN pip install nose nosepipe # Enable additional output from Qt.py ENV QT_VERBOSE true +# Xvfb +ENV DISPLAY :99 + WORKDIR /workspace/Qt.py ENTRYPOINT cp -r /Qt.py /workspace && \ python build_caveats_tests.py && \ + Xvfb :99 -screen 0 1024x768x16 2>/dev/null & \ + sleep 3 && \ nosetests \ --verbose \ --with-process-isolation \ diff --git a/Qt.py b/Qt.py index 17257c36..01adaba8 100644 --- a/Qt.py +++ b/Qt.py @@ -26,6 +26,7 @@ def _pyqt5(): import PyQt5.Qt + from PyQt5 import uic # Remap PyQt5.QtCore.Signal = PyQt5.QtCore.pyqtSignal @@ -37,7 +38,7 @@ def _pyqt5(): PyQt5.__binding__ = "PyQt5" PyQt5.__binding_version__ = PyQt5.QtCore.PYQT_VERSION_STR PyQt5.__qt_version__ = PyQt5.QtCore.QT_VERSION_STR - PyQt5.load_ui = pyqt5_load_ui + PyQt5.load_ui = _pyqt_load_ui_factory(uic) return PyQt5 @@ -61,6 +62,7 @@ def _pyqt4(): raise ImportError import PyQt4.Qt + from PyQt4 import uic # Remap PyQt4.QtWidgets = PyQt4.QtGui @@ -75,14 +77,14 @@ def _pyqt4(): PyQt4.__binding__ = "PyQt4" PyQt4.__binding_version__ = PyQt4.QtCore.PYQT_VERSION_STR PyQt4.__qt_version__ = PyQt4.QtCore.QT_VERSION_STR - PyQt4.load_ui = pyqt4_load_ui + PyQt4.load_ui = _pyqt_load_ui_factory(uic) return PyQt4 def _pyside2(): import PySide2 - from PySide2 import QtGui, QtCore + from PySide2 import QtGui, QtCore, QtUiTools # Remap QtCore.QStringListModel = QtGui.QStringListModel @@ -92,14 +94,14 @@ def _pyside2(): PySide2.__binding__ = "PySide2" PySide2.__binding_version__ = PySide2.__version__ PySide2.__qt_version__ = PySide2.QtCore.qVersion() - PySide2.load_ui = pyside2_load_ui + PySide2.load_ui = _pyside_load_ui_factory(QtUiTools.QUiLoader) return PySide2 def _pyside(): import PySide - from PySide import QtGui, QtCore + from PySide import QtGui, QtCore, QtUiTools QtCore, QtGui # bypass linter warnings # Remap @@ -114,65 +116,84 @@ def _pyside(): PySide.__binding__ = "PySide" PySide.__binding_version__ = PySide.__version__ PySide.__qt_version__ = PySide.QtCore.qVersion() - PySide.load_ui = pyside_load_ui + PySide.load_ui = _pyside_load_ui_factory(QtUiTools.QUiLoader) return PySide -def pyside_load_ui(fname): - """Read Qt Designer .ui `fname` - - Args: - fname (str): Absolute path to .ui file +def _pyside_load_ui_factory(superclass): + """load_ui factory function for PySide and PySide2 - Usage: - >> from Qt import load_ui - >> class MyWindow(QtWidgets.QWidget): - .. fname = 'my_ui.ui' - .. self.ui = load_ui(fname) - .. - >> window = MyWindow() + Produce a load_ui function using the provided superclass """ - from PySide import QtUiTools - return QtUiTools.QUiLoader().load(fname) + def load_ui(fname, base_instance=None): + """Read Qt Designer .ui `fname` + Args: + fname (str): Absolute path to .ui file + base_instance (widget, optional): Instance of the Qt base class. -def pyside2_load_ui(fname): - """Read Qt Designer .ui `fname` + Usage: + import sys + from Qt import QtWidgets, load_ui + class MyWindow(QtWidgets.QMainWindow): + def __init__(self, parent=None): + super(MyWindow, self).__init__(parent) + fname = 'my_ui.ui' + load_ui(fname, self) + app = QtWidgets.QApplication(sys.argv) + window = MyWindow() + window.show() + app.exec_() - Args: - fname (str): Absolute path to .ui file + """ - """ + if base_instance: + ui = superclass().load(fname) + for member in dir(ui): + if not member.startswith('__'): + setattr(base_instance, member, getattr(ui, member)) + return ui + else: + return superclass().load(fname) - from PySide2 import QtUiTools - return QtUiTools.QUiLoader().load(fname) + return load_ui -def pyqt4_load_ui(fname): - """Read Qt Designer .ui `fname` +def _pyqt_load_ui_factory(uic): + """load_ui factory function for PyQt4 and PyQt5 - Args: - fname (str): Absolute path to .ui file + Produce a load_ui function using the provided module """ - from PyQt4 import uic - return uic.loadUi(fname) + def load_ui(fname, base_instance=None): + """Read Qt Designer .ui `fname` + Args: + fname (str): Absolute path to .ui file + base_instance (widget, optional): Instance of the Qt base class. -def pyqt5_load_ui(fname): - """Read Qt Designer .ui `fname` + Usage: + import sys + from Qt import QtWidgets, load_ui + class MyWindow(QtWidgets.QMainWindow): + def __init__(self, parent=None): + super(MyWindow, self).__init__(parent) + fname = 'my_ui.ui' + load_ui(fname, self) + app = QtWidgets.QApplication(sys.argv) + window = MyWindow() + window.show() + app.exec_() - Args: - fname (str): Absolute path to .ui file + """ - """ + return uic.loadUi(fname, base_instance) - from PyQt5 import uic - return uic.loadUi(fname) + return load_ui def _log(text, verbose): @@ -190,18 +211,18 @@ def _init(): this has executed. """ - + preferred = os.getenv("QT_PREFERRED_BINDING") verbose = os.getenv("QT_VERBOSE") is not None bindings = (_pyside2, _pyqt5, _pyside, _pyqt4) if preferred: - + # Internal flag (used in installer) if preferred == "None": sys.modules[__name__].__wrapper_version__ = __version__ return - + preferred = preferred.split(os.pathsep) available = { "PySide2": _pyside2, diff --git a/README.md b/README.md index 06e119b9..de033824 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,22 @@ ui.show() app.exec_() ``` -Please note, for maximum compatibility, only pass the argument of the filename to the `load_ui` function. +This `load_ui` function accepts a second argument; the base instance into which the UI is loaded: + +```python +import sys +from Qt import QtWidgets, load_ui + +class MyWindow(QtWidgets.QMainWindow): + def __init__(self, parent=None): + super(MyWindow, self).__init__(parent) + load_ui("my.ui", self) + +app = QtWidgets.QApplication(sys.argv) +window = MyWindow() +window.show() +app.exec_() +``` ##### sip API v2 diff --git a/appveyor.yml b/appveyor.yml index 224a2e9f..454d9968 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,7 +22,7 @@ install: # the registry key. - REG ADD HKCU\Software\Python\PythonCore\2.7\InstallPath /f /ve /t REG_SZ /d %PYTHON% - - ps: (new-object net.webclient).DownloadFile($env:PYQT, "C:\install-PyQt4.exe") + - ps: do{sleep 5;(New-Object Net.WebClient).DownloadFile($env:PYQT, "C:\install-PyQt4.exe")}while(!$?) - ps: Start-Process -FilePath C:\install-PyQt4.exe -ArgumentList "/S" -Wait -Passthru # Test install diff --git a/tests.py b/tests.py index 6c3ca8fd..2c6bf674 100644 --- a/tests.py +++ b/tests.py @@ -6,6 +6,7 @@ """ import os +import io import sys import imp import shutil @@ -25,6 +26,206 @@ def setup(): self.tempdir = tempfile.mkdtemp() + self.ui_qmainwindow = os.path.join(self.tempdir, "qmainwindow.ui") + source_qmainwindow = u"""\ + + + MainWindow + + + + 0 + 0 + 216 + 149 + + + + MainWindow + + + + + + + + + + + + 0 + 0 + 216 + 22 + + + + + + + + + +""" + with io.open(self.ui_qmainwindow, "w", encoding="utf-8") as f: + f.write(source_qmainwindow) + + self.ui_qwidget = os.path.join(self.tempdir, "qwidget.ui") + source_qwidget = u"""\ + + + Form + + + + 0 + 0 + 235 + 149 + + + + Form + + + + + + + + + + + +""" + with io.open(self.ui_qwidget, "w", encoding="utf-8") as f: + f.write(source_qwidget) + + self.ui_qdialog = os.path.join(self.tempdir, "qdialog.ui") + source_qdialog = u"""\ + + + Dialog + + + + 0 + 0 + 201 + 176 + + + + Dialog + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + + +""" + with io.open(self.ui_qdialog, "w", encoding="utf-8") as f: + f.write(source_qdialog) + + self.ui_custom_pyqt = os.path.join(self.tempdir, "custom_widget.ui") + source_custom_pyqt = u"""\ + + + MainWindow + + + MainWindow + + + + + + 10 + 10 + 113 + 32 + + + + PushButton + + + + + + + 0 + 0 + 125 + 22 + + + + + + + + MyCustomWidget + QPushButton +
MyCustomClasses
+
+
+ + +
+ +""" + with io.open(self.ui_custom_pyqt, "w", encoding="utf-8") as f: + f.write(source_custom_pyqt) + def teardown(): shutil.rmtree(self.tempdir) @@ -179,6 +380,262 @@ def test_vendoring(): ) == 0 +def test_load_ui_into_self_qmainwindow_pyside(): + """load_ui: Load UI into self (QMainWindow) using PySide""" + + with pyside(): + from Qt import QtWidgets, load_ui + + class MainWindow(QtWidgets.QMainWindow): + def __init__(self, parent=None): + QtWidgets.QMainWindow.__init__(self, parent) + load_ui(sys.modules[__name__].ui_qmainwindow, self) + + app = QtWidgets.QApplication(sys.argv) + window = MainWindow() + + # Inherited from .ui file + assert hasattr(window, "lineEdit") + assert isinstance(window.__class__, type(QtWidgets.QMainWindow)) + assert isinstance(window.parent(), type(None)) + assert isinstance(window.lineEdit.__class__, type(QtWidgets.QWidget)) + assert window.lineEdit.text() == '' + window.lineEdit.setText('Hello') + assert window.lineEdit.text() == 'Hello' + + app.exit() + + +def test_load_ui_into_self_qwidget_pyside(): + """load_ui: Load UI into self (QWidget) using PySide""" + + with pyside(): + from Qt import QtWidgets, load_ui + + class MainWindow(QtWidgets.QWidget): + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent) + load_ui(sys.modules[__name__].ui_qwidget, self) + + app = QtWidgets.QApplication(sys.argv) + window = MainWindow() + + # Inherited from .ui file + assert hasattr(window, "lineEdit") + assert isinstance(window.__class__, type(QtWidgets.QWidget)) + assert isinstance(window.parent(), type(None)) + assert isinstance(window.lineEdit.__class__, type(QtWidgets.QWidget)) + assert window.lineEdit.text() == '' + window.lineEdit.setText('Hello') + assert window.lineEdit.text() == 'Hello' + + app.exit() + + +def test_load_ui_into_self_qdialog_pyside(): + """load_ui: Load UI into self (QDialog) using PySide""" + + with pyside(): + from Qt import QtWidgets, load_ui + + class MainWindow(QtWidgets.QDialog): + def __init__(self, parent=None): + QtWidgets.QDialog.__init__(self, parent) + load_ui(sys.modules[__name__].ui_qdialog, self) + + app = QtWidgets.QApplication(sys.argv) + window = MainWindow() + + # Inherited from .ui file + assert hasattr(window, "lineEdit") + assert isinstance(window.__class__, type(QtWidgets.QDialog)) + assert isinstance(window.parent(), type(None)) + assert isinstance(window.lineEdit.__class__, type(QtWidgets.QWidget)) + assert window.lineEdit.text() == '' + window.lineEdit.setText('Hello') + assert window.lineEdit.text() == 'Hello' + + app.exit() + + +def test_load_ui_into_self_qmainwindow_pyqt4(): + """load_ui: Load UI into self (QMainWindow) using PyQt4""" + + with pyqt4(): + from Qt import QtWidgets, load_ui + + class MainWindow(QtWidgets.QMainWindow): + def __init__(self, parent=None): + QtWidgets.QMainWindow.__init__(self, parent) + load_ui(sys.modules[__name__].ui_qmainwindow, self) + + app = QtWidgets.QApplication(sys.argv) + window = MainWindow() + + # From .ui file + assert hasattr(window, "lineEdit") + assert isinstance(window.__class__, type(QtWidgets.QMainWindow)) + assert isinstance(window.parent(), type(None)) + assert isinstance(window.lineEdit.__class__, type(QtWidgets.QWidget)) + assert window.lineEdit.text() == '' + window.lineEdit.setText('Hello') + assert window.lineEdit.text() == 'Hello' + + app.exit() + + +def test_load_ui_into_self_qwidget_pyqt4(): + """load_ui: Load UI into self (QWidget) using PyQt4""" + + with pyqt4(): + from Qt import QtWidgets, load_ui + + class MainWindow(QtWidgets.QWidget): + def __init__(self, parent=None): + QtWidgets.QWidget.__init__(self, parent) + load_ui(sys.modules[__name__].ui_qwidget, self) + + app = QtWidgets.QApplication(sys.argv) + window = MainWindow() + + # From .ui file + assert hasattr(window, "lineEdit") + assert isinstance(window.__class__, type(QtWidgets.QWidget)) + assert isinstance(window.parent(), type(None)) + assert isinstance(window.lineEdit.__class__, type(QtWidgets.QWidget)) + assert window.lineEdit.text() == '' + window.lineEdit.setText('Hello') + assert window.lineEdit.text() == 'Hello' + + app.exit() + + +def test_load_ui_into_self_qdialog_pyqt4(): + """load_ui: Load UI into self (QWidget) using PyQt4""" + + with pyqt4(): + from Qt import QtWidgets, load_ui + + class MainWindow(QtWidgets.QDialog): + def __init__(self, parent=None): + QtWidgets.QDialog.__init__(self, parent) + load_ui(sys.modules[__name__].ui_qdialog, self) + + app = QtWidgets.QApplication(sys.argv) + window = MainWindow() + + # From .ui file + assert hasattr(window, "lineEdit") + assert isinstance(window.__class__, type(QtWidgets.QDialog)) + assert isinstance(window.parent(), type(None)) + assert isinstance(window.lineEdit.__class__, type(QtWidgets.QWidget)) + assert window.lineEdit.text() == '' + window.lineEdit.setText('Hello') + assert window.lineEdit.text() == 'Hello' + + app.exit() + + +def test_load_ui_into_custom_pyside(): + """load_ui: Load UI into widget (QMainWindow) using PySide""" + + with pyside(): + from Qt import QtWidgets, load_ui + + app = QtWidgets.QApplication(sys.argv) + widget = load_ui(sys.modules[__name__].ui_qmainwindow) + + # From .ui file + assert hasattr(widget, "lineEdit") + assert isinstance(widget.__class__, type(QtWidgets.QMainWindow)) + assert isinstance(widget.parent(), type(None)) + assert isinstance(widget.lineEdit.__class__, type(QtWidgets.QWidget)) + assert widget.lineEdit.text() == '' + widget.lineEdit.setText('Hello') + assert widget.lineEdit.text() == 'Hello' + + app.exit() + + +def test_load_ui_into_custom_pyqt4(): + """load_ui: Load UI into widget (QMainWindow) using PyQt4""" + + with pyqt4(): + from Qt import QtWidgets, load_ui + + app = QtWidgets.QApplication(sys.argv) + widget = load_ui(sys.modules[__name__].ui_qmainwindow) + + # From .ui file + assert hasattr(widget, "lineEdit") + assert isinstance(widget.__class__, type(QtWidgets.QMainWindow)) + assert isinstance(widget.parent(), type(None)) + assert isinstance(widget.lineEdit.__class__, type(QtWidgets.QWidget)) + assert widget.lineEdit.text() == '' + widget.lineEdit.setText('Hello') + assert widget.lineEdit.text() == 'Hello' + + app.exit() + + +def test_load_ui_connection_pyside(): + """load_ui: Load UI into self, set up signals in PySide""" + + with pyside(): + import sys + from Qt import QtWidgets, load_ui + + def setup_ui(base_instance=None): + return load_ui(sys.modules[__name__].ui_qmainwindow, base_instance) + + app = QtWidgets.QApplication(sys.argv) + Ui = type(setup_ui()) # Get the un-instantiated class + + class MyWidget(Ui): # Inherit from it. + def __init__(self, parent=None): + super(MyWidget, self).__init__(parent) + setup_ui(self) + self.x = False + self.lineEdit.textChanged.connect(self.some_method) + self.lineEdit.setText('hello') + assert self.x is True + + def some_method(self): + self.x = True + + my_widget = MyWidget() + my_widget.show() + + +def test_load_ui_connection_pyqt4(): + """load_ui: Load UI into self, set up signals in PySide""" + + with pyqt4(): + import sys + from Qt import QtWidgets, load_ui + + def setup_ui(base_instance=None): + return load_ui(sys.modules[__name__].ui_qmainwindow, base_instance) + + app = QtWidgets.QApplication(sys.argv) + Ui = type(setup_ui()) # Get the un-instantiated class + + class MyWidget(Ui): # Inherit from it. + def __init__(self, parent=None): + super(MyWidget, self).__init__(parent) + setup_ui(self) + self.x = False + self.lineEdit.textChanged.connect(self.some_method) + self.lineEdit.setText('hello') + assert self.x is True + + def some_method(self): + self.x = True + + my_widget = MyWidget() + my_widget.show() + + if PYTHON == 2: def test_sip_api_already_set(): """Raise ImportError if sip API v1 was already set (Python 2.x only)"""