Skip to content

Commit 533ea03

Browse files
authored
Merge pull request #226 from blurstudio/QtCompatBackwardsCompat
Make QtSiteConfig.update_members backwards compatible
2 parents 45a13ed + 56581b6 commit 533ea03

File tree

7 files changed

+428
-44
lines changed

7 files changed

+428
-44
lines changed

CAVEATS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ Use compatibility wrapper.
264264
>>> app = QtWidgets.QApplication(sys.argv)
265265
>>> view = QtWidgets.QTreeWidget()
266266
>>> header = view.header()
267-
>>> QtCompat.setSectionResizeMode(header, QtWidgets.QHeaderView.Fixed)
267+
>>> QtCompat.QHeaderView.setSectionResizeMode(header, QtWidgets.QHeaderView.Fixed)
268268
```
269269

270270
Or a conditional.
@@ -281,6 +281,10 @@ Or a conditional.
281281
... header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
282282
```
283283

284+
Note: Qt.QtCompat.setSectionResizeMode is a older way this was handled and has been left in for now, but this will likely be removed in the future.
285+
286+
<br>
287+
<br>
284288

285289
#### QtWidgets.qApp
286290

Qt.py

Lines changed: 192 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
import shutil
4444
import importlib
4545

46-
__version__ = "1.1.0.b2"
46+
47+
__version__ = "1.1.0.b3"
4748

4849
# Enable support for `from Qt import *`
4950
__all__ = []
@@ -611,7 +612,7 @@
611612
612613
"""
613614
_misplaced_members = {
614-
"pyside2": {
615+
"PySide2": {
615616
"QtGui.QStringListModel": "QtCore.QStringListModel",
616617
"QtCore.Property": "QtCore.Property",
617618
"QtCore.Signal": "QtCore.Signal",
@@ -621,7 +622,7 @@
621622
"QtCore.QItemSelection": "QtCore.QItemSelection",
622623
"QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
623624
},
624-
"pyqt5": {
625+
"PyQt5": {
625626
"QtCore.pyqtProperty": "QtCore.Property",
626627
"QtCore.pyqtSignal": "QtCore.Signal",
627628
"QtCore.pyqtSlot": "QtCore.Slot",
@@ -631,7 +632,7 @@
631632
"QtCore.QItemSelection": "QtCore.QItemSelection",
632633
"QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
633634
},
634-
"pyside": {
635+
"PySide": {
635636
"QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
636637
"QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
637638
"QtGui.QStringListModel": "QtCore.QStringListModel",
@@ -642,7 +643,7 @@
642643
"QtCore.Slot": "QtCore.Slot",
643644

644645
},
645-
"pyqt4": {
646+
"PyQt4": {
646647
"QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
647648
"QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
648649
"QtGui.QItemSelection": "QtCore.QItemSelection",
@@ -654,6 +655,86 @@
654655
}
655656
}
656657

658+
""" Compatibility Members
659+
660+
This dictionary is used to build Qt.QtCompat objects that provide a consistent
661+
interface for obsolete members, and differences in binding return values.
662+
663+
{
664+
"binding": {
665+
"classname": {
666+
"targetname": "binding_namespace",
667+
}
668+
}
669+
}
670+
"""
671+
_compatibility_members = {
672+
"PySide2": {
673+
"QHeaderView": {
674+
"sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
675+
"setSectionsClickable":
676+
"QtWidgets.QHeaderView.setSectionsClickable",
677+
"sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
678+
"setSectionResizeMode":
679+
"QtWidgets.QHeaderView.setSectionResizeMode",
680+
"sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
681+
"setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
682+
},
683+
"QFileDialog": {
684+
"getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
685+
"getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
686+
"getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
687+
},
688+
},
689+
"PyQt5": {
690+
"QHeaderView": {
691+
"sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
692+
"setSectionsClickable":
693+
"QtWidgets.QHeaderView.setSectionsClickable",
694+
"sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
695+
"setSectionResizeMode":
696+
"QtWidgets.QHeaderView.setSectionResizeMode",
697+
"sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
698+
"setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
699+
},
700+
"QFileDialog": {
701+
"getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
702+
"getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
703+
"getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
704+
},
705+
},
706+
"PySide": {
707+
"QHeaderView": {
708+
"sectionsClickable": "QtWidgets.QHeaderView.isClickable",
709+
"setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
710+
"sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
711+
"setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
712+
"sectionsMovable": "QtWidgets.QHeaderView.isMovable",
713+
"setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
714+
},
715+
"QFileDialog": {
716+
"getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
717+
"getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
718+
"getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
719+
},
720+
},
721+
"PyQt4": {
722+
"QHeaderView": {
723+
"sectionsClickable": "QtWidgets.QHeaderView.isClickable",
724+
"setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
725+
"sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
726+
"setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
727+
"sectionsMovable": "QtWidgets.QHeaderView.isMovable",
728+
"setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
729+
},
730+
"QFileDialog": {
731+
"getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
732+
"getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
733+
"getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
734+
},
735+
},
736+
}
737+
657738

658739
def _apply_site_config():
659740
try:
@@ -663,8 +744,16 @@ def _apply_site_config():
663744
# to _common_members are needed.
664745
pass
665746
else:
666-
# Update _common_members with any changes made by QtSiteConfig
667-
QtSiteConfig.update_members(_common_members)
747+
# Provide the ability to modify the dicts used to build Qt.py
748+
if hasattr(QtSiteConfig, 'update_members'):
749+
QtSiteConfig.update_members(_common_members)
750+
751+
if hasattr(QtSiteConfig, 'update_misplaced_members'):
752+
QtSiteConfig.update_misplaced_members(members=_misplaced_members)
753+
754+
if hasattr(QtSiteConfig, 'update_compatibility_members'):
755+
QtSiteConfig.update_compatibility_members(
756+
members=_compatibility_members)
668757

669758

670759
def _new_module(name):
@@ -737,10 +826,10 @@ def _wrapinstance(func, ptr, base=None):
737826

738827

739828
def _reassign_misplaced_members(binding):
740-
"""Parse `_misplaced_members` dict and remap
741-
values based on the underlying binding.
829+
"""Apply misplaced members from `binding` to Qt.py
742830
743-
:param str binding: Top level binding in _misplaced_members.
831+
Arguments:
832+
binding (dict): Misplaced members
744833
745834
"""
746835

@@ -766,6 +855,67 @@ def _reassign_misplaced_members(binding):
766855
)
767856

768857

858+
def _build_compatibility_members(binding, decorators=None):
859+
"""Apply `binding` to QtCompat
860+
861+
Arguments:
862+
binding (str): Top level binding in _compatibility_members.
863+
decorators (dict, optional): Provides the ability to decorate the
864+
original Qt methods when needed by a binding. This can be used
865+
to change the returned value to a standard value. The key should
866+
be the classname, the value is a dict where the keys are the
867+
target method names, and the values are the decorator functions.
868+
869+
"""
870+
871+
decorators = decorators or dict()
872+
873+
# Allow optional site-level customization of the compatibility members.
874+
# This method does not need to be implemented in QtSiteConfig.
875+
try:
876+
import QtSiteConfig
877+
except ImportError:
878+
pass
879+
else:
880+
if hasattr(QtSiteConfig, 'update_compatibility_decorators'):
881+
QtSiteConfig.update_compatibility_decorators(binding, decorators)
882+
883+
_QtCompat = type("QtCompat", (object,), {})
884+
885+
for classname, bindings in _compatibility_members[binding].items():
886+
attrs = {}
887+
for target, binding in bindings.items():
888+
namespaces = binding.split('.')
889+
try:
890+
src_object = getattr(Qt, "_" + namespaces[0])
891+
except AttributeError as e:
892+
_log("QtCompat: AttributeError: %s" % e)
893+
# Skip reassignment of non-existing members.
894+
# This can happen if a request was made to
895+
# rename a member that didn't exist, for example
896+
# if QtWidgets isn't available on the target platform.
897+
continue
898+
899+
# Walk down any remaining namespace getting the object assuming
900+
# that if the first namespace exists the rest will exist.
901+
for namespace in namespaces[1:]:
902+
src_object = getattr(src_object, namespace)
903+
904+
# decorate the Qt method if a decorator was provided.
905+
if target in decorators.get(classname, []):
906+
# staticmethod must be called on the decorated method to
907+
# prevent a TypeError being raised when the decorated method
908+
# is called.
909+
src_object = staticmethod(
910+
decorators[classname][target](src_object))
911+
912+
attrs[target] = src_object
913+
914+
# Create the QtCompat class and install it into the namespace
915+
compat_class = type(classname, (_QtCompat,), attrs)
916+
setattr(Qt.QtCompat, classname, compat_class)
917+
918+
769919
def _pyside2():
770920
"""Initialise PySide2
771921
@@ -804,7 +954,8 @@ def _pyside2():
804954
Qt.QtCompat.setSectionResizeMode = \
805955
Qt._QtWidgets.QHeaderView.setSectionResizeMode
806956

807-
_reassign_misplaced_members("pyside2")
957+
_reassign_misplaced_members("PySide2")
958+
_build_compatibility_members("PySide2")
808959

809960

810961
def _pyside():
@@ -850,7 +1001,8 @@ def _pyside():
8501001
)
8511002
)
8521003

853-
_reassign_misplaced_members("pyside")
1004+
_reassign_misplaced_members("PySide")
1005+
_build_compatibility_members("PySide")
8541006

8551007

8561008
def _pyqt5():
@@ -883,7 +1035,8 @@ def _pyqt5():
8831035
Qt.QtCompat.setSectionResizeMode = \
8841036
Qt._QtWidgets.QHeaderView.setSectionResizeMode
8851037

886-
_reassign_misplaced_members("pyqt5")
1038+
_reassign_misplaced_members("PyQt5")
1039+
_build_compatibility_members('PyQt5')
8871040

8881041

8891042
def _pyqt4():
@@ -963,7 +1116,32 @@ def _pyqt4():
9631116
n)
9641117
)
9651118

966-
_reassign_misplaced_members("pyqt4")
1119+
_reassign_misplaced_members("PyQt4")
1120+
1121+
# QFileDialog QtCompat decorator
1122+
def _standardizeQFileDialog(some_function):
1123+
"""Decorator that makes PyQt4 return conform to other bindings"""
1124+
def wrapper(*args, **kwargs):
1125+
ret = (some_function(*args, **kwargs))
1126+
1127+
# PyQt4 only returns the selected filename, force it to a
1128+
# standard return of the selected filename, and a empty string
1129+
# for the selected filter
1130+
return ret, ''
1131+
1132+
wrapper.__doc__ = some_function.__doc__
1133+
wrapper.__name__ = some_function.__name__
1134+
1135+
return wrapper
1136+
1137+
decorators = {
1138+
"QFileDialog": {
1139+
"getOpenFileName": _standardizeQFileDialog,
1140+
"getOpenFileNames": _standardizeQFileDialog,
1141+
"getSaveFileName": _standardizeQFileDialog,
1142+
}
1143+
}
1144+
_build_compatibility_members('PyQt4', decorators)
9671145

9681146

9691147
def _none():

README.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,26 +128,39 @@ All members of `Qt` stem directly from those available via PySide2, along with t
128128
'PyQt5'
129129
```
130130

131+
### Compatibility
132+
131133
Qt.py also provides compatibility wrappers for critical functionality that differs across bindings, these can be found in the added `QtCompat` submodule.
132134

133135
| Attribute | Returns | Description
134136
|:------------------------------------------|:------------|:------------
135137
| `loadUi(uifile=str, baseinstance=QWidget)`| `QObject` | Minimal wrapper of PyQt4.loadUi and PySide equivalent
136138
| `translate(...)` | `function` | Compatibility wrapper around [QCoreApplication.translate][]
137-
| `setSectionResizeMode()` | `method` | Compatibility wrapper around [QAbstractItemView.setSectionResizeMode][]
138139
| `wrapInstance(addr=long, type=QObject)` | `QObject` | Wrapper around `shiboken2.wrapInstance` and PyQt equivalent
139140
| `getCppPointer(object=QObject)` | `long` | Wrapper around `shiboken2.getCppPointer` and PyQt equivalent
140141

141142
[QCoreApplication.translate]: https://doc.qt.io/qt-5/qcoreapplication.html#translate
142-
[QAbstractItemView.setSectionResizeMode]: https://doc.qt.io/qt-5/qheaderview.html#setSectionResizeMode
143143

144144
**Example**
145145

146146
```python
147147
>>> from Qt import QtCompat
148-
>>> QtCompat.setSectionResizeMode
148+
>>> QtCompat.loadUi
149149
```
150150

151+
#### Class specific compatibility objects
152+
153+
Between Qt4 and Qt5 there have been many classes and class members that are obsolete. Under Qt.QtCompat there are many classes with names matching the classes they provide compatibility functions. These will match the PySide2 naming convention.
154+
155+
```python
156+
from Qt import QtCore, QtWidgets, QtCompat
157+
header = QtWidgets.QHeaderView(QtCore.Qt.Horizontal)
158+
QtCompat.QHeaderView.setSectionsMovable(header, False)
159+
movable = QtCompat.QHeaderView.sectionsMovable(header)
160+
```
161+
162+
This also covers inconsistencies between bindings. For example PyQt4's QFileDialog matches Qt4's return value of the selected. While all other bindings return the selected filename and the file filter the user used to select the file. `Qt.QtCompat.QFileDialog` ensures that getOpenFileName(s) and getSaveFileName always return the tuple.
163+
151164
<br>
152165

153166
##### Environment Variables
@@ -220,13 +233,11 @@ If you need to expose a module that isn't included in Qt.py by default or wish t
220233
```python
221234
# QtSiteConfig.py
222235
def update_members(members):
223-
"""Called by Qt.py at run-time to modify the modules it makes available.
236+
"""Called by Qt.py at run-time to modify the modules it makes available.
224237
225238
Arguments:
226239
members (dict): The members considered by Qt.py
227-
228240
"""
229-
230241
members.pop("QtCore")
231242
```
232243

@@ -373,6 +384,7 @@ Send us a pull-request with your studio here.
373384
- [CGRU](http://cgru.info/)
374385
- [MPC](http://www.moving-picture.com)
375386
- [Rising Sun Pictures](https://rsp.com.au)
387+
- [Blur Studio](http://www.blur.com)
376388

377389
Presented at Siggraph 2016, BOF!
378390

0 commit comments

Comments
 (0)