Skip to content

Commit 0fb6f47

Browse files
authored
Merge pull request #196 from dgovil/baseinstance
Baseinstance support for all bindings
2 parents 227f4df + f48b203 commit 0fb6f47

File tree

5 files changed

+347
-58
lines changed

5 files changed

+347
-58
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ test_caveats.py
66

77
# Development
88
.vscode
9+
.idea

.hound.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
python:
2+
enabled: true
3+
4+
fail_on_violations: true

Qt.py

Lines changed: 209 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,14 @@
1-
"""The MIT License (MIT)
1+
"""Minimal Python 2 & 3 shim around all Qt bindings
22
3-
Copyright (c) 2016-2017 Marcus Ottosson
3+
DOCUMENTATION
4+
Qt.py was born in the film and visual effects industry to address
5+
the growing need for the development of software capable of running
6+
with more than one flavour of the Qt bindings for Python - PySide,
7+
PySide2, PyQt4 and PyQt5.
48
5-
Permission is hereby granted, free of charge, to any person obtaining a copy
6-
of this software and associated documentation files (the "Software"), to deal
7-
in the Software without restriction, including without limitation the rights
8-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9-
copies of the Software, and to permit persons to whom the Software is
10-
furnished to do so, subject to the following conditions:
11-
12-
The above copyright notice and this permission notice shall be included in all
13-
copies or substantial portions of the Software.
14-
15-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
22-
23-
Documentation
24-
25-
Map all bindings to PySide2
26-
27-
Project goals:
28-
Qt.py was born in the film and visual effects industry to address
29-
the growing need for the development of software capable of running
30-
with more than one flavour of the Qt bindings for Python - PySide,
31-
PySide2, PyQt4 and PyQt5.
32-
33-
1. Build for one, run with all
34-
2. Explicit is better than implicit
35-
3. Support co-existence
9+
1. Build for one, run with all
10+
2. Explicit is better than implicit
11+
3. Support co-existence
3612
3713
Default resolution order:
3814
- PySide2
@@ -55,6 +31,10 @@
5531
5632
For more details, visit https://github.com/mottosso/Qt.py
5733
34+
LICENSE
35+
36+
See end of file for license (MIT, BSD) information.
37+
5838
"""
5939

6040
import os
@@ -63,7 +43,7 @@
6343
import shutil
6444
import importlib
6545

66-
__version__ = "1.0.0.b3"
46+
__version__ = "1.0.0.b4"
6747

6848
# Enable support for `from Qt import *`
6949
__all__ = []
@@ -661,8 +641,7 @@ def _pyside2():
661641
Qt.__binding_version__ = module.__version__
662642

663643
if hasattr(Qt, "_QtUiTools"):
664-
Qt.QtCompat.loadUi = lambda fname: \
665-
Qt._QtUiTools.QUiLoader().load(fname)
644+
Qt.QtCompat.loadUi = _loadUi
666645

667646
if hasattr(Qt, "_QtGui") and hasattr(Qt, "_QtCore"):
668647
Qt.QtCore.QStringListModel = Qt._QtGui.QStringListModel
@@ -695,8 +674,7 @@ def _pyside():
695674
Qt.__binding_version__ = module.__version__
696675

697676
if hasattr(Qt, "_QtUiTools"):
698-
Qt.QtCompat.loadUi = lambda fname: \
699-
Qt._QtUiTools.QUiLoader().load(fname)
677+
Qt.QtCompat.loadUi = _loadUi
700678

701679
if hasattr(Qt, "_QtGui"):
702680
setattr(Qt, "QtWidgets", _new_module("QtWidgets"))
@@ -739,7 +717,7 @@ def _pyqt5():
739717
_setup(module, ["uic"])
740718

741719
if hasattr(Qt, "_uic"):
742-
Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname)
720+
Qt.QtCompat.loadUi = _loadUi
743721

744722
if hasattr(Qt, "_QtWidgets"):
745723
Qt.QtCompat.setSectionResizeMode = \
@@ -804,7 +782,7 @@ def _pyqt4():
804782
_setup(module, ["uic"])
805783

806784
if hasattr(Qt, "_uic"):
807-
Qt.QtCompat.loadUi = lambda fname: Qt._uic.loadUi(fname)
785+
Qt.QtCompat.loadUi = _loadUi
808786

809787
if hasattr(Qt, "_QtGui"):
810788
setattr(Qt, "QtWidgets", _new_module("QtWidgets"))
@@ -849,7 +827,7 @@ def _none():
849827
Qt.__binding__ = "None"
850828
Qt.__qt_version__ = "0.0.0"
851829
Qt.__binding_version__ = "0.0.0"
852-
Qt.QtCompat.loadUi = lambda fname: None
830+
Qt.QtCompat.loadUi = lambda uifile, baseinstance=None: None
853831
Qt.QtCompat.setSectionResizeMode = lambda *args, **kwargs: None
854832

855833
for submodule in _common_members.keys():
@@ -862,6 +840,103 @@ def _log(text):
862840
sys.stdout.write(text + "\n")
863841

864842

843+
def _loadUi(uifile, baseinstance=None):
844+
"""Dynamically load a user interface from the given `uifile`
845+
846+
This function calls `uic.loadUi` if using PyQt bindings,
847+
else it implements a comparable binding for PySide.
848+
849+
Documentation:
850+
http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi
851+
852+
Arguments:
853+
uifile (str): Absolute path to Qt Designer file.
854+
baseinstance (QWidget): Instantiated QWidget or subclass thereof
855+
856+
Return:
857+
baseinstance if `baseinstance` is not `None`. Otherwise
858+
return the newly created instance of the user interface.
859+
860+
"""
861+
if hasattr(baseinstance, "layout") and baseinstance.layout():
862+
message = ("QLayout: Attempting to add Layout to %s which "
863+
"already has a layout")
864+
raise RuntimeError(message % (baseinstance))
865+
866+
if hasattr(Qt, "_uic"):
867+
return Qt._uic.loadUi(uifile, baseinstance)
868+
869+
elif hasattr(Qt, "_QtUiTools"):
870+
# Implement `PyQt5.uic.loadUi` for PySide(2)
871+
872+
class _UiLoader(Qt._QtUiTools.QUiLoader):
873+
"""Create the user interface in a base instance.
874+
875+
Unlike `Qt._QtUiTools.QUiLoader` itself this class does not
876+
create a new instance of the top-level widget, but creates the user
877+
interface in an existing instance of the top-level class if needed.
878+
879+
This mimics the behaviour of `PyQt5.uic.loadUi`.
880+
881+
"""
882+
883+
def __init__(self, baseinstance):
884+
super(_UiLoader, self).__init__(baseinstance)
885+
self.baseinstance = baseinstance
886+
887+
def load(self, uifile, *args, **kwargs):
888+
from xml.etree.ElementTree import ElementTree
889+
890+
# For whatever reason, if this doesn't happen then
891+
# reading an invalid or non-existing .ui file throws
892+
# a RuntimeError.
893+
etree = ElementTree()
894+
etree.parse(uifile)
895+
896+
return Qt._QtUiTools.QUiLoader.load(
897+
self, uifile, *args, **kwargs)
898+
899+
def createWidget(self, class_name, parent=None, name=""):
900+
"""Called for each widget defined in ui file
901+
902+
Overridden here to populate `baseinstance` instead.
903+
904+
"""
905+
906+
if parent is None and self.baseinstance:
907+
# Supposed to create the top-level widget,
908+
# return the base instance instead
909+
return self.baseinstance
910+
911+
# For some reason, Line is not in the list of available
912+
# widgets, but works fine, so we have to special case it here.
913+
if class_name in self.availableWidgets() + ["Line"]:
914+
# Create a new widget for child widgets
915+
widget = Qt._QtUiTools.QUiLoader.createWidget(self,
916+
class_name,
917+
parent,
918+
name)
919+
920+
else:
921+
raise Exception("Custom widget '%s' not supported"
922+
% class_name)
923+
924+
if self.baseinstance:
925+
# Set an attribute for the new child widget on the base
926+
# instance, just like PyQt5.uic.loadUi does.
927+
setattr(self.baseinstance, name, widget)
928+
929+
return widget
930+
931+
widget = _UiLoader(baseinstance).load(uifile)
932+
Qt.QtCore.QMetaObject.connectSlotsByName(widget)
933+
934+
return widget
935+
936+
else:
937+
raise NotImplementedError("No implementation available for loadUi")
938+
939+
865940
def _convert(lines):
866941
"""Convert compiled .ui file from PySide2 to Qt.py
867942
@@ -1028,3 +1103,96 @@ def _install():
10281103
# Enable command-line interface
10291104
if __name__ == "__main__":
10301105
_cli(sys.argv[1:])
1106+
1107+
1108+
# The MIT License (MIT)
1109+
#
1110+
# Copyright (c) 2016-2017 Marcus Ottosson
1111+
#
1112+
# Permission is hereby granted, free of charge, to any person obtaining a copy
1113+
# of this software and associated documentation files (the "Software"), to deal
1114+
# in the Software without restriction, including without limitation the rights
1115+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1116+
# copies of the Software, and to permit persons to whom the Software is
1117+
# furnished to do so, subject to the following conditions:
1118+
#
1119+
# The above copyright notice and this permission notice shall be included in
1120+
# all copies or substantial portions of the Software.
1121+
#
1122+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1123+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1124+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1125+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1126+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1127+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1128+
# SOFTWARE.
1129+
#
1130+
# In PySide(2), loadUi does not exist, so we implement it
1131+
#
1132+
# `_UiLoader` is adapted from the qtpy project, which was further influenced
1133+
# by qt-helpers which was released under a 3-clause BSD license which in turn
1134+
# is based on a solution at:
1135+
#
1136+
# - https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
1137+
#
1138+
# The License for this code is as follows:
1139+
#
1140+
# qt-helpers - a common front-end to various Qt modules
1141+
#
1142+
# Copyright (c) 2015, Chris Beaumont and Thomas Robitaille
1143+
#
1144+
# All rights reserved.
1145+
#
1146+
# Redistribution and use in source and binary forms, with or without
1147+
# modification, are permitted provided that the following conditions are
1148+
# met:
1149+
#
1150+
# * Redistributions of source code must retain the above copyright
1151+
# notice, this list of conditions and the following disclaimer.
1152+
# * Redistributions in binary form must reproduce the above copyright
1153+
# notice, this list of conditions and the following disclaimer in the
1154+
# documentation and/or other materials provided with the
1155+
# distribution.
1156+
# * Neither the name of the Glue project nor the names of its contributors
1157+
# may be used to endorse or promote products derived from this software
1158+
# without specific prior written permission.
1159+
#
1160+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
1161+
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
1162+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
1163+
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
1164+
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
1165+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
1166+
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
1167+
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
1168+
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
1169+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1170+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1171+
#
1172+
# Which itself was based on the solution at
1173+
#
1174+
# https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8
1175+
#
1176+
# which was released under the MIT license:
1177+
#
1178+
# Copyright (c) 2011 Sebastian Wiesner <[email protected]>
1179+
# Modifications by Charl Botha <[email protected]>
1180+
#
1181+
# Permission is hereby granted, free of charge, to any person obtaining a
1182+
# copy of this software and associated documentation files
1183+
# (the "Software"),to deal in the Software without restriction,
1184+
# including without limitation
1185+
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1186+
# and/or sell copies of the Software, and to permit persons to whom the
1187+
# Software is furnished to do so, subject to the following conditions:
1188+
#
1189+
# The above copyright notice and this permission notice shall be included
1190+
# in all copies or substantial portions of the Software.
1191+
#
1192+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1193+
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
1194+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
1195+
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
1196+
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
1197+
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
1198+
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,11 @@ All members of `Qt` stem directly from those available via PySide2, along with t
122122

123123
Qt.py also provides compatibility wrappers for critical functionality that differs across bindings, these can be found in the added `QtCompat` submodule.
124124

125-
| Attribute | Returns | Description
126-
|:------------------------|:------------|:------------
127-
| `loadUi(fname=str)` | `QObject` | Minimal wrapper of PyQt4.loadUi and PySide equivalent
128-
| `translate(...)` | `function` | Compatibility wrapper around [QCoreApplication.translate][]
129-
| `setSectionResizeMode()`| `method` | Compatibility wrapper around [QAbstractItemView.setSectionResizeMode][]
125+
| Attribute | Returns | Description
126+
|:------------------------------------------|:------------|:------------
127+
| `loadUi(fname=str, baseinstance=QWidget)` | `QObject` | Minimal wrapper of PyQt4.loadUi and PySide equivalent
128+
| `translate(...)` | `function` | Compatibility wrapper around [QCoreApplication.translate][]
129+
| `setSectionResizeMode()` | `method` | Compatibility wrapper around [QAbstractItemView.setSectionResizeMode][]
130130

131131
[QCoreApplication.translate]: https://doc.qt.io/qt-5/qcoreapplication.html#translate
132132
[QAbstractItemView.setSectionResizeMode]: https://doc.qt.io/qt-5/qheaderview.html#setSectionResizeMode
@@ -148,6 +148,7 @@ These are the publicly facing environment variables that in one way or another a
148148
|:---------------------|:------|:----------
149149
| QT_PREFERRED_BINDING | str | Override order and content of binding to try.
150150
| QT_VERBOSE | bool | Be a little more chatty about what's going on with Qt.py
151+
| QT_SIP_API_HINT | int | Sets the preferred SIP api version that will be attempted to set.
151152

152153
<br>
153154

@@ -212,21 +213,41 @@ Now you may use the file as you normally would, with Qt.py
212213

213214
<br>
214215

215-
##### Load Qt Designer files
216+
##### Loading Qt Designer files
216217

217-
The `uic.loadUi` function of PyQt4 and PyQt5 as well as the `QtUiTools.QUiLoader().load` function of PySide/PySide2 are mapped to a convenience function `load_ui`.
218+
The `uic.loadUi` function of PyQt4 and PyQt5 as well as the `QtUiTools.QUiLoader().load` function of PySide/PySide2 are mapped to a convenience function `loadUi`.
218219

219220
```python
220221
import sys
221222
from Qt import QtCompat
222223

223224
app = QtWidgets.QApplication(sys.argv)
224-
ui = QtCompat.load_ui(fname="my.ui")
225+
ui = QtCompat.loadUi(fname="my.ui")
225226
ui.show()
226227
app.exec_()
227228
```
229+
For `PyQt` bindings it uses their native implementation, whereas for `PySide` bindings it uses our custom implementation borrowed from the [qtpy](https://github.com/spyder-ide/qtpy) project.
230+
231+
`loadUi` has two arguments as opposed to the multiple that PyQt ships with. See [here](https://github.com/mottosso/Qt.py/pull/81) for details - in a nutshell, those arguments differ between PyQt and PySide in incompatible ways.
232+
The second argument is `baseinstance` which allows a ui to be dynamically loaded onto an existing QWidget instance.
233+
234+
```python
235+
QtCompat.loadUi(fname="my.ui", baseinstance=QtWidgets.QWidget)
236+
```
237+
238+
`uifile` is the string path to the ui file to load.
239+
240+
If `baseinstance` is `None`, the a new instance of the top-level
241+
widget will be created. Otherwise, the user interface is created within
242+
the given `baseinstance`. In this case `baseinstance` must be an
243+
instance of the top-level widget class in the UI file to load, or a
244+
subclass thereof. In other words, if you've created a `QMainWindow`
245+
interface in the designer, `baseinstance` must be a `QMainWindow`
246+
or a subclass thereof, too. You cannot load a `QMainWindow` UI file
247+
with a plain `QWidget` as `baseinstance`.
228248

229-
Please note, `load_ui` has only one argument, whereas the PyQt and PySide equivalent has more. See [here](https://github.com/mottosso/Qt.py/pull/81) for details - in a nutshell, those arguments differ between PyQt and PySide in incompatible ways.
249+
`loadUi` returns `baseinstance`, if `baseinstance` is provided.
250+
Otherwise it will return the newly created instance of the user interface.
230251

231252
<br>
232253

0 commit comments

Comments
 (0)