Skip to content

Commit

Permalink
Merge pull request #226 from blurstudio/QtCompatBackwardsCompat
Browse files Browse the repository at this point in the history
Make QtSiteConfig.update_members backwards compatible
  • Loading branch information
mottosso authored Aug 10, 2017
2 parents 45a13ed + 56581b6 commit 533ea03
Show file tree
Hide file tree
Showing 7 changed files with 428 additions and 44 deletions.
6 changes: 5 additions & 1 deletion CAVEATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ Use compatibility wrapper.
>>> app = QtWidgets.QApplication(sys.argv)
>>> view = QtWidgets.QTreeWidget()
>>> header = view.header()
>>> QtCompat.setSectionResizeMode(header, QtWidgets.QHeaderView.Fixed)
>>> QtCompat.QHeaderView.setSectionResizeMode(header, QtWidgets.QHeaderView.Fixed)
```

Or a conditional.
Expand All @@ -281,6 +281,10 @@ Or a conditional.
... header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
```

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.

<br>
<br>

#### QtWidgets.qApp

Expand Down
206 changes: 192 additions & 14 deletions Qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
import shutil
import importlib

__version__ = "1.1.0.b2"

__version__ = "1.1.0.b3"

# Enable support for `from Qt import *`
__all__ = []
Expand Down Expand Up @@ -611,7 +612,7 @@
"""
_misplaced_members = {
"pyside2": {
"PySide2": {
"QtGui.QStringListModel": "QtCore.QStringListModel",
"QtCore.Property": "QtCore.Property",
"QtCore.Signal": "QtCore.Signal",
Expand All @@ -621,7 +622,7 @@
"QtCore.QItemSelection": "QtCore.QItemSelection",
"QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
},
"pyqt5": {
"PyQt5": {
"QtCore.pyqtProperty": "QtCore.Property",
"QtCore.pyqtSignal": "QtCore.Signal",
"QtCore.pyqtSlot": "QtCore.Slot",
Expand All @@ -631,7 +632,7 @@
"QtCore.QItemSelection": "QtCore.QItemSelection",
"QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel",
},
"pyside": {
"PySide": {
"QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
"QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
"QtGui.QStringListModel": "QtCore.QStringListModel",
Expand All @@ -642,7 +643,7 @@
"QtCore.Slot": "QtCore.Slot",

},
"pyqt4": {
"PyQt4": {
"QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel",
"QtGui.QSortFilterProxyModel": "QtCore.QSortFilterProxyModel",
"QtGui.QItemSelection": "QtCore.QItemSelection",
Expand All @@ -654,6 +655,86 @@
}
}

""" Compatibility Members
This dictionary is used to build Qt.QtCompat objects that provide a consistent
interface for obsolete members, and differences in binding return values.
{
"binding": {
"classname": {
"targetname": "binding_namespace",
}
}
}
"""
_compatibility_members = {
"PySide2": {
"QHeaderView": {
"sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
"setSectionsClickable":
"QtWidgets.QHeaderView.setSectionsClickable",
"sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
"setSectionResizeMode":
"QtWidgets.QHeaderView.setSectionResizeMode",
"sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
"setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
},
"QFileDialog": {
"getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
"getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
"getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
},
},
"PyQt5": {
"QHeaderView": {
"sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable",
"setSectionsClickable":
"QtWidgets.QHeaderView.setSectionsClickable",
"sectionResizeMode": "QtWidgets.QHeaderView.sectionResizeMode",
"setSectionResizeMode":
"QtWidgets.QHeaderView.setSectionResizeMode",
"sectionsMovable": "QtWidgets.QHeaderView.sectionsMovable",
"setSectionsMovable": "QtWidgets.QHeaderView.setSectionsMovable",
},
"QFileDialog": {
"getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
"getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
"getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
},
},
"PySide": {
"QHeaderView": {
"sectionsClickable": "QtWidgets.QHeaderView.isClickable",
"setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
"sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
"setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
"sectionsMovable": "QtWidgets.QHeaderView.isMovable",
"setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
},
"QFileDialog": {
"getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
"getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
"getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
},
},
"PyQt4": {
"QHeaderView": {
"sectionsClickable": "QtWidgets.QHeaderView.isClickable",
"setSectionsClickable": "QtWidgets.QHeaderView.setClickable",
"sectionResizeMode": "QtWidgets.QHeaderView.resizeMode",
"setSectionResizeMode": "QtWidgets.QHeaderView.setResizeMode",
"sectionsMovable": "QtWidgets.QHeaderView.isMovable",
"setSectionsMovable": "QtWidgets.QHeaderView.setMovable",
},
"QFileDialog": {
"getOpenFileName": "QtWidgets.QFileDialog.getOpenFileName",
"getOpenFileNames": "QtWidgets.QFileDialog.getOpenFileNames",
"getSaveFileName": "QtWidgets.QFileDialog.getSaveFileName",
},
},
}


def _apply_site_config():
try:
Expand All @@ -663,8 +744,16 @@ def _apply_site_config():
# to _common_members are needed.
pass
else:
# Update _common_members with any changes made by QtSiteConfig
QtSiteConfig.update_members(_common_members)
# Provide the ability to modify the dicts used to build Qt.py
if hasattr(QtSiteConfig, 'update_members'):
QtSiteConfig.update_members(_common_members)

if hasattr(QtSiteConfig, 'update_misplaced_members'):
QtSiteConfig.update_misplaced_members(members=_misplaced_members)

if hasattr(QtSiteConfig, 'update_compatibility_members'):
QtSiteConfig.update_compatibility_members(
members=_compatibility_members)


def _new_module(name):
Expand Down Expand Up @@ -737,10 +826,10 @@ def _wrapinstance(func, ptr, base=None):


def _reassign_misplaced_members(binding):
"""Parse `_misplaced_members` dict and remap
values based on the underlying binding.
"""Apply misplaced members from `binding` to Qt.py
:param str binding: Top level binding in _misplaced_members.
Arguments:
binding (dict): Misplaced members
"""

Expand All @@ -766,6 +855,67 @@ def _reassign_misplaced_members(binding):
)


def _build_compatibility_members(binding, decorators=None):
"""Apply `binding` to QtCompat
Arguments:
binding (str): Top level binding in _compatibility_members.
decorators (dict, optional): Provides the ability to decorate the
original Qt methods when needed by a binding. This can be used
to change the returned value to a standard value. The key should
be the classname, the value is a dict where the keys are the
target method names, and the values are the decorator functions.
"""

decorators = decorators or dict()

# Allow optional site-level customization of the compatibility members.
# This method does not need to be implemented in QtSiteConfig.
try:
import QtSiteConfig
except ImportError:
pass
else:
if hasattr(QtSiteConfig, 'update_compatibility_decorators'):
QtSiteConfig.update_compatibility_decorators(binding, decorators)

_QtCompat = type("QtCompat", (object,), {})

for classname, bindings in _compatibility_members[binding].items():
attrs = {}
for target, binding in bindings.items():
namespaces = binding.split('.')
try:
src_object = getattr(Qt, "_" + namespaces[0])
except AttributeError as e:
_log("QtCompat: AttributeError: %s" % e)
# Skip reassignment of non-existing members.
# This can happen if a request was made to
# rename a member that didn't exist, for example
# if QtWidgets isn't available on the target platform.
continue

# Walk down any remaining namespace getting the object assuming
# that if the first namespace exists the rest will exist.
for namespace in namespaces[1:]:
src_object = getattr(src_object, namespace)

# decorate the Qt method if a decorator was provided.
if target in decorators.get(classname, []):
# staticmethod must be called on the decorated method to
# prevent a TypeError being raised when the decorated method
# is called.
src_object = staticmethod(
decorators[classname][target](src_object))

attrs[target] = src_object

# Create the QtCompat class and install it into the namespace
compat_class = type(classname, (_QtCompat,), attrs)
setattr(Qt.QtCompat, classname, compat_class)


def _pyside2():
"""Initialise PySide2
Expand Down Expand Up @@ -804,7 +954,8 @@ def _pyside2():
Qt.QtCompat.setSectionResizeMode = \
Qt._QtWidgets.QHeaderView.setSectionResizeMode

_reassign_misplaced_members("pyside2")
_reassign_misplaced_members("PySide2")
_build_compatibility_members("PySide2")


def _pyside():
Expand Down Expand Up @@ -850,7 +1001,8 @@ def _pyside():
)
)

_reassign_misplaced_members("pyside")
_reassign_misplaced_members("PySide")
_build_compatibility_members("PySide")


def _pyqt5():
Expand Down Expand Up @@ -883,7 +1035,8 @@ def _pyqt5():
Qt.QtCompat.setSectionResizeMode = \
Qt._QtWidgets.QHeaderView.setSectionResizeMode

_reassign_misplaced_members("pyqt5")
_reassign_misplaced_members("PyQt5")
_build_compatibility_members('PyQt5')


def _pyqt4():
Expand Down Expand Up @@ -963,7 +1116,32 @@ def _pyqt4():
n)
)

_reassign_misplaced_members("pyqt4")
_reassign_misplaced_members("PyQt4")

# QFileDialog QtCompat decorator
def _standardizeQFileDialog(some_function):
"""Decorator that makes PyQt4 return conform to other bindings"""
def wrapper(*args, **kwargs):
ret = (some_function(*args, **kwargs))

# PyQt4 only returns the selected filename, force it to a
# standard return of the selected filename, and a empty string
# for the selected filter
return ret, ''

wrapper.__doc__ = some_function.__doc__
wrapper.__name__ = some_function.__name__

return wrapper

decorators = {
"QFileDialog": {
"getOpenFileName": _standardizeQFileDialog,
"getOpenFileNames": _standardizeQFileDialog,
"getSaveFileName": _standardizeQFileDialog,
}
}
_build_compatibility_members('PyQt4', decorators)


def _none():
Expand Down
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,26 +128,39 @@ All members of `Qt` stem directly from those available via PySide2, along with t
'PyQt5'
```

### Compatibility

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

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

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

**Example**

```python
>>> from Qt import QtCompat
>>> QtCompat.setSectionResizeMode
>>> QtCompat.loadUi
```

#### Class specific compatibility objects

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.

```python
from Qt import QtCore, QtWidgets, QtCompat
header = QtWidgets.QHeaderView(QtCore.Qt.Horizontal)
QtCompat.QHeaderView.setSectionsMovable(header, False)
movable = QtCompat.QHeaderView.sectionsMovable(header)
```

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.

<br>

##### Environment Variables
Expand Down Expand Up @@ -220,13 +233,11 @@ If you need to expose a module that isn't included in Qt.py by default or wish t
```python
# QtSiteConfig.py
def update_members(members):
"""Called by Qt.py at run-time to modify the modules it makes available.
"""Called by Qt.py at run-time to modify the modules it makes available.
Arguments:
members (dict): The members considered by Qt.py
"""

members.pop("QtCore")
```

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

Presented at Siggraph 2016, BOF!

Expand Down
Loading

0 comments on commit 533ea03

Please sign in to comment.