Hello community, here is the log from the commit of package python-Qt.py for openSUSE:Factory checked in at 2020-09-22 21:12:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-Qt.py (Old) and /work/SRC/openSUSE:Factory/.python-Qt.py.new.4249 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-Qt.py" Tue Sep 22 21:12:10 2020 rev:4 rq:835961 version:1.3.1 Changes: -------- --- /work/SRC/openSUSE:Factory/python-Qt.py/python-Qt.py.changes 2019-11-08 15:25:49.999001557 +0100 +++ /work/SRC/openSUSE:Factory/.python-Qt.py.new.4249/python-Qt.py.changes 2020-09-22 21:12:25.091994328 +0200 @@ -1,0 +2,8 @@ +Tue Sep 22 04:41:36 UTC 2020 - Steve Kowalik <[email protected]> + +- Update to 1.3.1: + * Stability improvements and greater ability for QtCompat.wrapInstance + to do its job + * Bugfixes and additional members + +------------------------------------------------------------------- Old: ---- Qt.py-1.1.0.tar.gz New: ---- Qt.py-1.3.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-Qt.py.spec ++++++ --- /var/tmp/diff_new_pack.dBeHBn/_old 2020-09-22 21:12:26.139995245 +0200 +++ /var/tmp/diff_new_pack.dBeHBn/_new 2020-09-22 21:12:26.143995249 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-Qt.py # -# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany. +# Copyright (c) 2020 SUSE LLC # Copyright (c) 2018, Martin Hauke <[email protected]> # # All modifications and additions to the file contributed by third parties @@ -19,7 +19,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-Qt.py -Version: 1.1.0 +Version: 1.3.1 Release: 0 Summary: Python compat-wrapper around all Qt bindings License: MIT ++++++ Qt.py-1.1.0.tar.gz -> Qt.py-1.3.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Qt.py-1.1.0/PKG-INFO new/Qt.py-1.3.1/PKG-INFO --- old/Qt.py-1.1.0/PKG-INFO 2018-01-25 11:56:43.000000000 +0100 +++ new/Qt.py-1.3.1/PKG-INFO 2020-09-20 16:00:46.000000000 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 1.1 Name: Qt.py -Version: 1.1.0 +Version: 1.3.1 Summary: Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5. Home-page: https://github.com/mottosso/Qt Author: Marcus Ottosson Author-email: [email protected] License: MIT -Description-Content-Type: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 4 - Beta diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Qt.py-1.1.0/Qt.py new/Qt.py-1.3.1/Qt.py --- old/Qt.py-1.1.0/Qt.py 2018-01-25 11:53:46.000000000 +0100 +++ new/Qt.py-1.3.1/Qt.py 2020-09-20 15:58:17.000000000 +0200 @@ -41,15 +41,18 @@ import sys import types import shutil +import importlib +import json -__version__ = "1.1.0" +__version__ = "1.3.1" # Enable support for `from Qt import *` __all__ = [] # Flags from environment variables QT_VERBOSE = bool(os.getenv("QT_VERBOSE")) +QT_PREFERRED_BINDING_JSON = os.getenv("QT_PREFERRED_BINDING_JSON", "") QT_PREFERRED_BINDING = os.getenv("QT_PREFERRED_BINDING", "") QT_SIP_API_HINT = os.getenv("QT_SIP_API_HINT") @@ -63,6 +66,7 @@ # Python 3 compatibility long = int + """Common members of all bindings This is where each member of Qt.py is explicitly defined. @@ -677,6 +681,322 @@ ] } +""" Missing members + +This mapping describes members that have been deprecated +in one or more bindings and have been left out of the +_common_members mapping. + +The member can provide an extra details string to be +included in exceptions and warnings. +""" + +_missing_members = { + "QtGui": { + "QMatrix": "Deprecated in PyQt5", + }, +} + + +def _qInstallMessageHandler(handler): + """Install a message handler that works in all bindings + + Args: + handler: A function that takes 3 arguments, or None + """ + def messageOutputHandler(*args): + # In Qt4 bindings, message handlers are passed 2 arguments + # In Qt5 bindings, message handlers are passed 3 arguments + # The first argument is a QtMsgType + # The last argument is the message to be printed + # The Middle argument (if passed) is a QMessageLogContext + if len(args) == 3: + msgType, logContext, msg = args + elif len(args) == 2: + msgType, msg = args + logContext = None + else: + raise TypeError( + "handler expected 2 or 3 arguments, got {0}".format(len(args))) + + if isinstance(msg, bytes): + # In python 3, some bindings pass a bytestring, which cannot be + # used elsewhere. Decoding a python 2 or 3 bytestring object will + # consistently return a unicode object. + msg = msg.decode() + + handler(msgType, logContext, msg) + + passObject = messageOutputHandler if handler else handler + if Qt.IsPySide or Qt.IsPyQt4: + return Qt._QtCore.qInstallMsgHandler(passObject) + elif Qt.IsPySide2 or Qt.IsPyQt5: + return Qt._QtCore.qInstallMessageHandler(passObject) + + +def _getcpppointer(object): + if hasattr(Qt, "_shiboken2"): + return getattr(Qt, "_shiboken2").getCppPointer(object)[0] + elif hasattr(Qt, "_shiboken"): + return getattr(Qt, "_shiboken").getCppPointer(object)[0] + elif hasattr(Qt, "_sip"): + return getattr(Qt, "_sip").unwrapinstance(object) + raise AttributeError("'module' has no attribute 'getCppPointer'") + + +def _wrapinstance(ptr, base=None): + """Enable implicit cast of pointer to most suitable class + + This behaviour is available in sip per default. + + Based on http://nathanhorne.com/pyqtpyside-wrap-instance + + Usage: + This mechanism kicks in under these circumstances. + 1. Qt.py is using PySide 1 or 2. + 2. A `base` argument is not provided. + + See :func:`QtCompat.wrapInstance()` + + Arguments: + ptr (long): Pointer to QObject in memory + base (QObject, optional): Base class to wrap with. Defaults to QObject, + which should handle anything. + + """ + + assert isinstance(ptr, long), "Argument 'ptr' must be of type <long>" + assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( + "Argument 'base' must be of type <QObject>") + + if Qt.IsPyQt4 or Qt.IsPyQt5: + func = getattr(Qt, "_sip").wrapinstance + elif Qt.IsPySide2: + func = getattr(Qt, "_shiboken2").wrapInstance + elif Qt.IsPySide: + func = getattr(Qt, "_shiboken").wrapInstance + else: + raise AttributeError("'module' has no attribute 'wrapInstance'") + + if base is None: + if Qt.IsPyQt4 or Qt.IsPyQt5: + base = Qt.QtCore.QObject + else: + q_object = func(long(ptr), Qt.QtCore.QObject) + meta_object = q_object.metaObject() + + while True: + class_name = meta_object.className() + + try: + base = getattr(Qt.QtWidgets, class_name) + except AttributeError: + try: + base = getattr(Qt.QtCore, class_name) + except AttributeError: + meta_object = meta_object.superClass() + continue + + break + + return func(long(ptr), base) + + +def _isvalid(object): + """Check if the object is valid to use in Python runtime. + + Usage: + See :func:`QtCompat.isValid()` + + Arguments: + object (QObject): QObject to check the validity of. + + """ + + assert isinstance(object, Qt.QtCore.QObject) + + if hasattr(Qt, "_shiboken2"): + return getattr(Qt, "_shiboken2").isValid(object) + + elif hasattr(Qt, "_shiboken"): + return getattr(Qt, "_shiboken").isValid(object) + + elif hasattr(Qt, "_sip"): + return not getattr(Qt, "_sip").isdeleted(object) + + else: + raise AttributeError("'module' has no attribute isValid") + + +def _translate(context, sourceText, *args): + # In Qt4 bindings, translate can be passed 2 or 3 arguments + # In Qt5 bindings, translate can be passed 2 arguments + # The first argument is disambiguation[str] + # The last argument is n[int] + # The middle argument can be encoding[QtCore.QCoreApplication.Encoding] + if len(args) == 3: + disambiguation, encoding, n = args + elif len(args) == 2: + disambiguation, n = args + encoding = None + else: + raise TypeError( + "Expected 4 or 5 arguments, got {0}.".format(len(args) + 2)) + + if hasattr(Qt.QtCore, "QCoreApplication"): + app = getattr(Qt.QtCore, "QCoreApplication") + else: + raise NotImplementedError( + "Missing QCoreApplication implementation for {binding}".format( + binding=Qt.__binding__, + ) + ) + if Qt.__binding__ in ("PySide2", "PyQt5"): + sanitized_args = [context, sourceText, disambiguation, n] + else: + sanitized_args = [ + context, + sourceText, + disambiguation, + encoding or app.CodecForTr, + n + ] + return app.translate(*sanitized_args) + + +def _loadUi(uifile, baseinstance=None): + """Dynamically load a user interface from the given `uifile` + + This function calls `uic.loadUi` if using PyQt bindings, + else it implements a comparable binding for PySide. + + Documentation: + http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi + + Arguments: + uifile (str): Absolute path to Qt Designer file. + baseinstance (QWidget): Instantiated QWidget or subclass thereof + + Return: + baseinstance if `baseinstance` is not `None`. Otherwise + return the newly created instance of the user interface. + + """ + if hasattr(Qt, "_uic"): + return Qt._uic.loadUi(uifile, baseinstance) + + elif hasattr(Qt, "_QtUiTools"): + # Implement `PyQt5.uic.loadUi` for PySide(2) + + class _UiLoader(Qt._QtUiTools.QUiLoader): + """Create the user interface in a base instance. + + Unlike `Qt._QtUiTools.QUiLoader` itself this class does not + create a new instance of the top-level widget, but creates the user + interface in an existing instance of the top-level class if needed. + + This mimics the behaviour of `PyQt5.uic.loadUi`. + + """ + + def __init__(self, baseinstance): + super(_UiLoader, self).__init__(baseinstance) + self.baseinstance = baseinstance + self.custom_widgets = {} + + def _loadCustomWidgets(self, etree): + """ + Workaround to pyside-77 bug. + + From QUiLoader doc we should use registerCustomWidget method. + But this causes a segfault on some platforms. + + Instead we fetch from customwidgets DOM node the python class + objects. Then we can directly use them in createWidget method. + """ + + def headerToModule(header): + """ + Translate a header file to python module path + foo/bar.h => foo.bar + """ + # Remove header extension + module = os.path.splitext(header)[0] + + # Replace os separator by python module separator + return module.replace("/", ".").replace("\\", ".") + + custom_widgets = etree.find("customwidgets") + + if custom_widgets is None: + return + + for custom_widget in custom_widgets: + class_name = custom_widget.find("class").text + header = custom_widget.find("header").text + module = importlib.import_module(headerToModule(header)) + self.custom_widgets[class_name] = getattr(module, + class_name) + + def load(self, uifile, *args, **kwargs): + from xml.etree.ElementTree import ElementTree + + # For whatever reason, if this doesn't happen then + # reading an invalid or non-existing .ui file throws + # a RuntimeError. + etree = ElementTree() + etree.parse(uifile) + self._loadCustomWidgets(etree) + + widget = Qt._QtUiTools.QUiLoader.load( + self, uifile, *args, **kwargs) + + # Workaround for PySide 1.0.9, see issue #208 + widget.parentWidget() + + return widget + + def createWidget(self, class_name, parent=None, name=""): + """Called for each widget defined in ui file + + Overridden here to populate `baseinstance` instead. + + """ + + if parent is None and self.baseinstance: + # Supposed to create the top-level widget, + # return the base instance instead + return self.baseinstance + + # For some reason, Line is not in the list of available + # widgets, but works fine, so we have to special case it here. + if class_name in self.availableWidgets() + ["Line"]: + # Create a new widget for child widgets + widget = Qt._QtUiTools.QUiLoader.createWidget(self, + class_name, + parent, + name) + elif class_name in self.custom_widgets: + widget = self.custom_widgets[class_name](parent=parent) + else: + raise Exception("Custom widget '%s' not supported" + % class_name) + + if self.baseinstance: + # Set an attribute for the new child widget on the base + # instance, just like PyQt5.uic.loadUi does. + setattr(self.baseinstance, name, widget) + + return widget + + widget = _UiLoader(baseinstance).load(uifile) + Qt.QtCore.QMetaObject.connectSlotsByName(widget) + + return widget + + else: + raise NotImplementedError("No implementation available for loadUi") + """Misplaced members @@ -685,6 +1005,7 @@ """ _misplaced_members = { "PySide2": { + "QtCore.QStringListModel": "QtCore.QStringListModel", "QtGui.QStringListModel": "QtCore.QStringListModel", "QtCore.Property": "QtCore.Property", "QtCore.Signal": "QtCore.Signal", @@ -694,6 +1015,21 @@ "QtCore.QItemSelection": "QtCore.QItemSelection", "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", + "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], + "shiboken2.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], + "shiboken2.getCppPointer": ["QtCompat.getCppPointer", _getcpppointer], + "shiboken2.isValid": ["QtCompat.isValid", _isvalid], + "QtWidgets.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtWidgets.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMessageHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", }, "PyQt5": { "QtCore.pyqtProperty": "QtCore.Property", @@ -705,6 +1041,21 @@ "QtCore.QItemSelection": "QtCore.QItemSelection", "QtCore.QItemSelectionModel": "QtCore.QItemSelectionModel", "QtCore.QItemSelectionRange": "QtCore.QItemSelectionRange", + "uic.loadUi": ["QtCompat.loadUi", _loadUi], + "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], + "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], + "sip.isdeleted": ["QtCompat.isValid", _isvalid], + "QtWidgets.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtWidgets.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMessageHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + "QtWidgets.QStyleOptionViewItem": "QtCompat.QStyleOptionViewItemV4", }, "PySide": { "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", @@ -724,6 +1075,21 @@ "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", "QtGui.QPrinter": "QtPrintSupport.QPrinter", "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", + "QtUiTools.QUiLoader": ["QtCompat.loadUi", _loadUi], + "shiboken.wrapInstance": ["QtCompat.wrapInstance", _wrapinstance], + "shiboken.unwrapInstance": ["QtCompat.getCppPointer", _getcpppointer], + "shiboken.isValid": ["QtCompat.isValid", _isvalid], + "QtGui.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtGui.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMsgHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4", }, "PyQt4": { "QtGui.QAbstractProxyModel": "QtCore.QAbstractProxyModel", @@ -743,6 +1109,23 @@ "QtGui.QPrintPreviewWidget": "QtPrintSupport.QPrintPreviewWidget", "QtGui.QPrinter": "QtPrintSupport.QPrinter", "QtGui.QPrinterInfo": "QtPrintSupport.QPrinterInfo", + # "QtCore.pyqtSignature": "QtCore.Slot", + "uic.loadUi": ["QtCompat.loadUi", _loadUi], + "sip.wrapinstance": ["QtCompat.wrapInstance", _wrapinstance], + "sip.unwrapinstance": ["QtCompat.getCppPointer", _getcpppointer], + "sip.isdeleted": ["QtCompat.isValid", _isvalid], + "QtCore.QString": "str", + "QtGui.qApp": "QtWidgets.QApplication.instance()", + "QtCore.QCoreApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtGui.QApplication.translate": [ + "QtCompat.translate", _translate + ], + "QtCore.qInstallMsgHandler": [ + "QtCompat.qInstallMessageHandler", _qInstallMessageHandler + ], + "QtGui.QStyleOptionViewItemV4": "QtCompat.QStyleOptionViewItemV4", } } @@ -761,6 +1144,9 @@ """ _compatibility_members = { "PySide2": { + "QWidget": { + "grab": "QtWidgets.QWidget.grab", + }, "QHeaderView": { "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", "setSectionsClickable": @@ -778,6 +1164,9 @@ }, }, "PyQt5": { + "QWidget": { + "grab": "QtWidgets.QWidget.grab", + }, "QHeaderView": { "sectionsClickable": "QtWidgets.QHeaderView.sectionsClickable", "setSectionsClickable": @@ -795,6 +1184,9 @@ }, }, "PySide": { + "QWidget": { + "grab": "QtWidgets.QPixmap.grabWidget", + }, "QHeaderView": { "sectionsClickable": "QtWidgets.QHeaderView.isClickable", "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", @@ -810,6 +1202,9 @@ }, }, "PyQt4": { + "QWidget": { + "grab": "QtWidgets.QPixmap.grabWidget", + }, "QHeaderView": { "sectionsClickable": "QtWidgets.QHeaderView.isClickable", "setSectionsClickable": "QtWidgets.QHeaderView.setClickable", @@ -864,12 +1259,25 @@ Qt.__binding__ = module.__name__ + def _warn_import_error(exc): + msg = str(exc) + if "No module named" in msg: + return + _warn("ImportError: %s" % msg) + for name in list(_common_members) + extras: try: submodule = _import_sub_module( module, name) - except ImportError: - continue + except ImportError as e: + try: + # For extra modules like sip and shiboken that may not be + # children of the binding. + submodule = __import__(name) + except ImportError as e2: + _warn_import_error(e) + _warn_import_error(e2) + continue setattr(Qt, "_" + name, submodule) @@ -880,50 +1288,6 @@ setattr(Qt, name, _new_module(name)) -def _wrapinstance(func, ptr, base=None): - """Enable implicit cast of pointer to most suitable class - - This behaviour is available in sip per default. - - Based on http://nathanhorne.com/pyqtpyside-wrap-instance - - Usage: - This mechanism kicks in under these circumstances. - 1. Qt.py is using PySide 1 or 2. - 2. A `base` argument is not provided. - - See :func:`QtCompat.wrapInstance()` - - Arguments: - func (function): Original function - ptr (long): Pointer to QObject in memory - base (QObject, optional): Base class to wrap with. Defaults to QObject, - which should handle anything. - - """ - - assert isinstance(ptr, long), "Argument 'ptr' must be of type <long>" - assert (base is None) or issubclass(base, Qt.QtCore.QObject), ( - "Argument 'base' must be of type <QObject>") - - if base is None: - q_object = func(long(ptr), Qt.QtCore.QObject) - meta_object = q_object.metaObject() - class_name = meta_object.className() - super_class_name = meta_object.superClass().className() - - if hasattr(Qt.QtWidgets, class_name): - base = getattr(Qt.QtWidgets, class_name) - - elif hasattr(Qt.QtWidgets, super_class_name): - base = getattr(Qt.QtWidgets, super_class_name) - - else: - base = Qt.QtCore.QObject - - return func(long(ptr), base) - - def _reassign_misplaced_members(binding): """Apply misplaced members from `binding` to Qt.py @@ -933,19 +1297,38 @@ """ for src, dst in _misplaced_members[binding].items(): - src_module, src_member = src.split(".") - dst_module, dst_member = dst.split(".") + dst_value = None + + src_parts = src.split(".") + src_module = src_parts[0] + src_member = None + if len(src_parts) > 1: + src_member = src_parts[1:] + + if isinstance(dst, (list, tuple)): + dst, dst_value = dst + + dst_parts = dst.split(".") + dst_module = dst_parts[0] + dst_member = None + if len(dst_parts) > 1: + dst_member = dst_parts[1] # Get the member we want to store in the namesapce. - try: - dst_value = getattr(getattr(Qt, "_" + src_module), src_member) - except AttributeError: - # If the member we want to store in the namespace does not exist, - # there is no need to continue. 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. - _log("Misplaced member has no source: {}".format(src)) - continue + if not dst_value: + try: + _part = getattr(Qt, "_" + src_module) + while src_member: + member = src_member.pop(0) + _part = getattr(_part, member) + dst_value = _part + except AttributeError: + # If the member we want to store in the namespace does not + # exist, there is no need to continue. 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. + _log("Misplaced member has no source: {0}".format(src)) + continue try: src_object = getattr(Qt, dst_module) @@ -965,9 +1348,14 @@ # Enable direct import of the new module sys.modules[__name__ + "." + dst_module] = src_object + if not dst_value: + dst_value = getattr(Qt, "_" + src_module) + if src_member: + dst_value = getattr(dst_value, src_member) + setattr( src_object, - dst_member, + dst_member or dst_module, dst_value ) @@ -1044,10 +1432,7 @@ """ import PySide2 as module - _setup(module, ["QtUiTools"]) - - Qt.__binding_version__ = module.__version__ - + extras = ["QtUiTools"] try: try: # Before merge of PySide and shiboken @@ -1055,24 +1440,27 @@ except ImportError: # After merge of PySide and shiboken, May 2017 from PySide2 import shiboken2 + extras.append("shiboken2") + except ImportError: + pass - Qt.QtCompat.wrapInstance = ( - lambda ptr, base=None: _wrapinstance( - shiboken2.wrapInstance, ptr, base) - ) - Qt.QtCompat.getCppPointer = lambda object: \ - shiboken2.getCppPointer(object)[0] + _setup(module, extras) + Qt.__binding_version__ = module.__version__ - except ImportError: - pass # Optional + if hasattr(Qt, "_shiboken2"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + Qt.QtCompat.delete = shiboken2.delete if hasattr(Qt, "_QtUiTools"): Qt.QtCompat.loadUi = _loadUi if hasattr(Qt, "_QtCore"): Qt.__qt_version__ = Qt._QtCore.qVersion() - Qt.QtCompat.qInstallMessageHandler = _qInstallMessageHandler - Qt.QtCompat.translate = Qt._QtCore.QCoreApplication.translate + Qt.QtCompat.dataChanged = ( + lambda self, topleft, bottomright, roles=None: + self.dataChanged.emit(topleft, bottomright, roles or []) + ) if hasattr(Qt, "_QtWidgets"): Qt.QtCompat.setSectionResizeMode = \ @@ -1086,10 +1474,7 @@ """Initialise PySide""" import PySide as module - _setup(module, ["QtUiTools"]) - - Qt.__binding_version__ = module.__version__ - + extras = ["QtUiTools"] try: try: # Before merge of PySide and shiboken @@ -1097,16 +1482,17 @@ except ImportError: # After merge of PySide and shiboken, May 2017 from PySide import shiboken + extras.append("shiboken") + except ImportError: + pass - Qt.QtCompat.wrapInstance = ( - lambda ptr, base=None: _wrapinstance( - shiboken.wrapInstance, ptr, base) - ) - Qt.QtCompat.getCppPointer = lambda object: \ - shiboken.getCppPointer(object)[0] + _setup(module, extras) + Qt.__binding_version__ = module.__version__ - except ImportError: - pass # Optional + if hasattr(Qt, "_shiboken"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + Qt.QtCompat.delete = shiboken.delete if hasattr(Qt, "_QtUiTools"): Qt.QtCompat.loadUi = _loadUi @@ -1122,17 +1508,9 @@ if hasattr(Qt, "_QtCore"): Qt.__qt_version__ = Qt._QtCore.qVersion() - QCoreApplication = Qt._QtCore.QCoreApplication - Qt.QtCompat.qInstallMessageHandler = _qInstallMessageHandler - Qt.QtCompat.translate = ( - lambda context, sourceText, disambiguation, n: - QCoreApplication.translate( - context, - sourceText, - disambiguation, - QCoreApplication.CodecForTr, - n - ) + Qt.QtCompat.dataChanged = ( + lambda self, topleft, bottomright, roles=None: + self.dataChanged.emit(topleft, bottomright) ) _reassign_misplaced_members("PySide") @@ -1143,19 +1521,25 @@ """Initialise PyQt5""" import PyQt5 as module - _setup(module, ["uic"]) + extras = ["uic"] try: - import sip - Qt.QtCompat.wrapInstance = ( - lambda ptr, base=None: _wrapinstance( - sip.wrapinstance, ptr, base) - ) - Qt.QtCompat.getCppPointer = lambda object: \ - sip.unwrapinstance(object) - + # Relevant to PyQt5 5.11 and above + from PyQt5 import sip + extras += ["sip"] except ImportError: - pass # Optional + + try: + import sip + extras += ["sip"] + except ImportError: + sip = None + + _setup(module, extras) + if hasattr(Qt, "_sip"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + Qt.QtCompat.delete = sip.delete if hasattr(Qt, "_uic"): Qt.QtCompat.loadUi = _loadUi @@ -1163,8 +1547,10 @@ if hasattr(Qt, "_QtCore"): Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR - Qt.QtCompat.qInstallMessageHandler = _qInstallMessageHandler - Qt.QtCompat.translate = Qt._QtCore.QCoreApplication.translate + Qt.QtCompat.dataChanged = ( + lambda self, topleft, bottomright, roles=None: + self.dataChanged.emit(topleft, bottomright, roles or []) + ) if hasattr(Qt, "_QtWidgets"): Qt.QtCompat.setSectionResizeMode = \ @@ -1212,19 +1598,18 @@ ) import PyQt4 as module - _setup(module, ["uic"]) - + extras = ["uic"] try: import sip - Qt.QtCompat.wrapInstance = ( - lambda ptr, base=None: _wrapinstance( - sip.wrapinstance, ptr, base) - ) - Qt.QtCompat.getCppPointer = lambda object: \ - sip.unwrapinstance(object) - + extras.append(sip.__name__) except ImportError: - pass # Optional + sip = None + + _setup(module, extras) + if hasattr(Qt, "_sip"): + Qt.QtCompat.wrapInstance = _wrapinstance + Qt.QtCompat.getCppPointer = _getcpppointer + Qt.QtCompat.delete = sip.delete if hasattr(Qt, "_uic"): Qt.QtCompat.loadUi = _loadUi @@ -1242,16 +1627,9 @@ if hasattr(Qt, "_QtCore"): Qt.__binding_version__ = Qt._QtCore.PYQT_VERSION_STR Qt.__qt_version__ = Qt._QtCore.QT_VERSION_STR - QCoreApplication = Qt._QtCore.QCoreApplication - Qt.QtCompat.qInstallMessageHandler = _qInstallMessageHandler - Qt.QtCompat.translate = ( - lambda context, sourceText, disambiguation, n: - QCoreApplication.translate( - context, - sourceText, - disambiguation, - QCoreApplication.CodecForTr, - n) + Qt.QtCompat.dataChanged = ( + lambda self, topleft, bottomright, roles=None: + self.dataChanged.emit(topleft, bottomright) ) _reassign_misplaced_members("PyQt4") @@ -1300,142 +1678,11 @@ def _log(text): if QT_VERBOSE: - sys.stdout.write(text + "\n") - - -def _loadUi(uifile, baseinstance=None): - """Dynamically load a user interface from the given `uifile` - - This function calls `uic.loadUi` if using PyQt bindings, - else it implements a comparable binding for PySide. - - Documentation: - http://pyqt.sourceforge.net/Docs/PyQt5/designer.html#PyQt5.uic.loadUi - - Arguments: - uifile (str): Absolute path to Qt Designer file. - baseinstance (QWidget): Instantiated QWidget or subclass thereof - - Return: - baseinstance if `baseinstance` is not `None`. Otherwise - return the newly created instance of the user interface. - - """ - - if hasattr(Qt, "_uic"): - return Qt._uic.loadUi(uifile, baseinstance) - - elif hasattr(Qt, "_QtUiTools"): - # Implement `PyQt5.uic.loadUi` for PySide(2) - - class _UiLoader(Qt._QtUiTools.QUiLoader): - """Create the user interface in a base instance. - - Unlike `Qt._QtUiTools.QUiLoader` itself this class does not - create a new instance of the top-level widget, but creates the user - interface in an existing instance of the top-level class if needed. - - This mimics the behaviour of `PyQt5.uic.loadUi`. - - """ - - def __init__(self, baseinstance): - super(_UiLoader, self).__init__(baseinstance) - self.baseinstance = baseinstance - - def load(self, uifile, *args, **kwargs): - from xml.etree.ElementTree import ElementTree + sys.stdout.write("Qt.py [info]: %s\n" % text) - # For whatever reason, if this doesn't happen then - # reading an invalid or non-existing .ui file throws - # a RuntimeError. - etree = ElementTree() - etree.parse(uifile) - - widget = Qt._QtUiTools.QUiLoader.load( - self, uifile, *args, **kwargs) - - # Workaround for PySide 1.0.9, see issue #208 - widget.parentWidget() - - return widget - - def createWidget(self, class_name, parent=None, name=""): - """Called for each widget defined in ui file - - Overridden here to populate `baseinstance` instead. - - """ - - if parent is None and self.baseinstance: - # Supposed to create the top-level widget, - # return the base instance instead - return self.baseinstance - - # For some reason, Line is not in the list of available - # widgets, but works fine, so we have to special case it here. - if class_name in self.availableWidgets() + ["Line"]: - # Create a new widget for child widgets - widget = Qt._QtUiTools.QUiLoader.createWidget(self, - class_name, - parent, - name) - - else: - raise Exception("Custom widget '%s' not supported" - % class_name) - - if self.baseinstance: - # Set an attribute for the new child widget on the base - # instance, just like PyQt5.uic.loadUi does. - setattr(self.baseinstance, name, widget) - - return widget - - widget = _UiLoader(baseinstance).load(uifile) - Qt.QtCore.QMetaObject.connectSlotsByName(widget) - - return widget - - else: - raise NotImplementedError("No implementation available for loadUi") - - -def _qInstallMessageHandler(handler): - """Install a message handler that works in all bindings - - Args: - handler: A function that takes 3 arguments, or None - """ - def messageOutputHandler(*args): - # In Qt4 bindings, message handlers are passed 2 arguments - # In Qt5 bindings, message handlers are passed 3 arguments - # The first argument is a QtMsgType - # The last argument is the message to be printed - # The Middle argument (if passed) is a QMessageLogContext - if len(args) == 3: - msgType, logContext, msg = args - elif len(args) == 2: - msgType, msg = args - logContext = None - else: - raise TypeError( - "handler expected 2 or 3 arguments, got {0}".format(len(args))) - - if isinstance(msg, bytes): - # In python 3, some bindings pass a bytestring, which cannot be - # used elsewhere. Decoding a python 2 or 3 bytestring object will - # consistently return a unicode object. - msg = msg.decode() - - handler(msgType, logContext, msg) - - passObject = messageOutputHandler if handler else handler - if Qt.IsPySide or Qt.IsPyQt4: - return Qt._QtCore.qInstallMsgHandler(passObject) - elif Qt.IsPySide2 or Qt.IsPyQt5: - return Qt._QtCore.qInstallMessageHandler(passObject) +def _warn(text): + sys.stderr.write("Qt.py [warning]: %s\n" % text) def _convert(lines): @@ -1523,12 +1770,67 @@ sys.stdout.write("Successfully converted \"%s\"\n" % args.convert) +class MissingMember(object): + """ + A placeholder type for a missing Qt object not + included in Qt.py + + Args: + name (str): The name of the missing type + details (str): An optional custom error message + """ + ERR_TMPL = ("{} is not a common object across PySide2 " + "and the other Qt bindings. It is not included " + "as a common member in the Qt.py layer") + + def __init__(self, name, details=''): + self.__name = name + self.__err = self.ERR_TMPL.format(name) + + if details: + self.__err = "{}: {}".format(self.__err, details) + + def __repr__(self): + return "<{}: {}>".format(self.__class__.__name__, self.__name) + + def __getattr__(self, name): + raise NotImplementedError(self.__err) + + def __call__(self, *a, **kw): + raise NotImplementedError(self.__err) + + def _install(): - # Default order (customise order and content via QT_PREFERRED_BINDING) + # Default order (customize order and content via QT_PREFERRED_BINDING) default_order = ("PySide2", "PyQt5", "PySide", "PyQt4") - preferred_order = list( - b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b - ) + preferred_order = None + if QT_PREFERRED_BINDING_JSON: + # A per-vendor preferred binding customization was defined + # This should be a dictionary of the full Qt.py module namespace to + # apply binding settings to. The "default" key can be used to apply + # custom bindings to all modules not explicitly defined. If the json + # data is invalid this will raise a exception. + # Example: + # {"mylibrary.vendor.Qt": ["PySide2"], "default":["PyQt5","PyQt4"]} + try: + preferred_bindings = json.loads(QT_PREFERRED_BINDING_JSON) + except ValueError: + # Python 2 raises ValueError, Python 3 raises json.JSONDecodeError + # a subclass of ValueError + _warn("Failed to parse QT_PREFERRED_BINDING_JSON='%s'" + % QT_PREFERRED_BINDING_JSON) + _warn("Falling back to default preferred order") + else: + preferred_order = preferred_bindings.get(__name__) + # If no matching binding was used, optionally apply a default. + if preferred_order is None: + preferred_order = preferred_bindings.get("default", None) + if preferred_order is None: + # If a json preferred binding was not used use, respect the + # QT_PREFERRED_BINDING environment variable if defined. + preferred_order = list( + b for b in QT_PREFERRED_BINDING.split(os.pathsep) if b + ) order = preferred_order or default_order @@ -1590,6 +1892,22 @@ setattr(our_submodule, member, their_member) + # Install missing member placeholders + for name, members in _missing_members.items(): + our_submodule = getattr(Qt, name) + + for member in members: + + # If the submodule already has this member installed, + # either by the common members, or the site config, + # then skip installing this one over it. + if hasattr(our_submodule, member): + continue + + placeholder = MissingMember("{}.{}".format(name, member), + details=members[member]) + setattr(our_submodule, member, placeholder) + # Enable direct import of QtCompat sys.modules['Qt.QtCompat'] = Qt.QtCompat diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Qt.py-1.1.0/Qt.py.egg-info/PKG-INFO new/Qt.py-1.3.1/Qt.py.egg-info/PKG-INFO --- old/Qt.py-1.1.0/Qt.py.egg-info/PKG-INFO 2018-01-25 11:56:43.000000000 +0100 +++ new/Qt.py-1.3.1/Qt.py.egg-info/PKG-INFO 2020-09-20 16:00:45.000000000 +0200 @@ -1,12 +1,11 @@ Metadata-Version: 1.1 Name: Qt.py -Version: 1.1.0 +Version: 1.3.1 Summary: Python 2 & 3 compatibility wrapper around all Qt bindings - PySide, PySide2, PyQt4 and PyQt5. Home-page: https://github.com/mottosso/Qt Author: Marcus Ottosson Author-email: [email protected] License: MIT -Description-Content-Type: UNKNOWN Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 4 - Beta diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/Qt.py-1.1.0/README.md new/Qt.py-1.3.1/README.md --- old/Qt.py-1.1.0/README.md 2018-01-25 11:53:46.000000000 +0100 +++ new/Qt.py-1.3.1/README.md 2020-09-20 15:58:17.000000000 +0200 @@ -1,7 +1,8 @@ -[](https://travis-ci.org/mottosso/Qt.py) [](https://pypi.python.org/pypi/Qt.py) -[](https://anaconda.org/conda-forge/qt.py) +<img width=260 src=logo.svg> -### Qt.py +[](https://pepy.tech/project/qt-py) [](https://travis-ci.org/mottosso/Qt.py) [](https://pypi.python.org/pypi/Qt.py) +[](https://anaconda.org/conda-forge/qt.py) [](https://gitter.im/Qt-py/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[](https://houndci.com) Qt.py enables you to write software that runs on any of the 4 supported bindings - PySide2, PyQt5, PySide and PyQt4. @@ -11,7 +12,9 @@ | Date | Version | Event |:---------|:----------|:---------- -| Dec 2017 | [1.1.0][] | Adds new test suite, new members +| Sep 2020 | [1.3.0][] | Stability improvements and greater ability for `QtCompat.wrapInstance` to do its job +| Jun 2019 | [1.2.1][] | Bugfixes and [additional members](https://github.com/mottosso/Qt.py/releases/tag/1.2.0) +| Jan 2018 | [1.1.0][] | Adds new test suite, new members | Mar 2017 | [1.0.0][] | Increased safety, **backwards incompatible** | Sep 2016 | [0.6.9][] | Stable release | Sep 2016 | [0.5.0][] | Alpha release of `--convert` @@ -24,6 +27,8 @@ [0.6.9]: https://github.com/mottosso/Qt.py/releases/tag/0.6.9 [1.0.0]: https://github.com/mottosso/Qt.py/releases/tag/1.0.0 [1.1.0]: https://github.com/mottosso/Qt.py/releases/tag/1.1.0 +[1.2.1]: https://github.com/mottosso/Qt.py/releases/tag/1.2.1 +[1.3.0]: https://github.com/mottosso/Qt.py/releases/tag/1.3.0 ##### Guides @@ -31,6 +36,7 @@ - [Dealing with Maya 2017 and PySide2](https://fredrikaverpil.github.io/2016/07/25/dealing-with-maya-2017-and-pyside2/) - [Vendoring Qt.py](https://fredrikaverpil.github.io/2017/05/04/vendoring-qt-py/) - [Udemy Course](https://www.udemy.com/python-for-maya/learn/v4/t/lecture/6027394) +- [PythonBytes #77](https://pythonbytes.fm/episodes/show/77/you-don-t-have-to-be-a-workaholic-to-win) (Starts at 5:00) ##### Table of contents @@ -149,6 +155,8 @@ | `translate(...)` | `function` | Compatibility wrapper around [QCoreApplication.translate][] | `wrapInstance(addr=long, type=QObject)` | `QObject` | Wrapper around `shiboken2.wrapInstance` and PyQt equivalent | `getCppPointer(object=QObject)` | `long` | Wrapper around `shiboken2.getCppPointer` and PyQt equivalent +| `isValid(object=QObject)` | `bool` | Wrapper around `shiboken2.isValid` and PyQt equivalent +| `dataChanged(topLeft=QModelIndex, bottomRight=QModelIndex, roles=[])` | `None` | Wrapper around `QtCore.QAbstractItemModel.dataChanged.emit` [QCoreApplication.translate]: https://doc.qt.io/qt-5/qcoreapplication.html#translate @@ -178,11 +186,12 @@ These are the publicly facing environment variables that in one way or another affect the way Qt.py is run. -| Variable | Type | Description -|:---------------------|:------|:---------- -| QT_PREFERRED_BINDING | str | Override order and content of binding to try. -| QT_VERBOSE | bool | Be a little more chatty about what's going on with Qt.py -| QT_SIP_API_HINT | int | Sets the preferred SIP api version that will be attempted to set. +| Variable | Type | Description +|:--------------------------|:------|:---------- +| QT_PREFERRED_BINDING_JSON | str | Override order and content of binding to try. This can apply per Qt.py namespace. +| QT_PREFERRED_BINDING | str | Override order and content of binding to try. Used if QT_PREFERRED_BINDING_JSON does not apply. +| QT_VERBOSE | bool | Be a little more chatty about what's going on with Qt.py +| QT_SIP_API_HINT | int | Sets the preferred SIP api version that will be attempted to set. <br> @@ -221,12 +230,31 @@ Constrain available choices and order of discovery by supplying multiple values. ```bash -# Try PyQt first and then PySide, but nothing else. -$ export QT_PREFERRED_BINDING=PyQt:PySide +# Try PyQt4 first and then PySide, but nothing else. +$ export QT_PREFERRED_BINDING=PyQt4:PySide ``` Using the OS path separator (`os.pathsep`) which is `:` on Unix systems and `;` on Windows. +If you need to control the preferred choice of a specific vendored Qt.py you can use the `QT_PREFERRED_BINDING_JSON` environment variable instead. + +```json +{ + "Qt":["PyQt5"], + "myproject.vendor.Qt":["PyQt5"], + "default":["PySide2"] +} +``` + +This json data forces any code that uses `import Qt` or `import myproject.vendor.Qt` to use PyQt5(`from x import Qt` etc works too, this is based on `__name__` of the Qt.py being imported). Any other imports of a Qt module will use the "default" PySide2 only. If `"default"` is not provided or a Qt.py being used does not support `QT_PREFERRED_BINDING_JSON`, `QT_PREFERRED_BINDING` will be respected. + +```bash +# Try PyQt5 first and then PyQt4 for the Qt module name space. +$ export QT_PREFERRED_BINDING_JSON="{"Qt":["PyQt5","PyQt4"]}" +# Use PyQt4 for any other Qt module name spaces. +$ export QT_PREFERRED_BINDING=PySide2 +``` + <br> ##### QtSiteConfig.py @@ -381,25 +409,33 @@ Send us a pull-request with your studio here. - [Atomic Fiction](http://www.atomicfiction.com/) -- [Industrial Brothers](http://industrialbrothers.com/) -- [Moonbot Studios](http://moonbotstudios.com/) -- [Sony Pictures Imageworks](http://www.imageworks.com/) +- [Bläck](http://www.blackstudios.se/) +- [Blur Studio](http://www.blur.com) +- [CGRU](http://cgru.info/) - [Colorbleed](http://www.colorbleed.nl/) -- [Method Studios](http://www.methodstudios.com/) -- [Framestore](https://framestore.com) -- [Weta Digital](https://www.wetafx.co.nz/) +- [Digital Domain](https://www.digitaldomain.com/) - [Disney Animation](https://www.disneyanimation.com/) -- [Industriromantik](http://www.industriromantik.se/) -- [Psyop](http://www.psyop.com/) -- [ftrack](https://www.ftrack.com/) +- [Dreamworks Animation](https://github.com/dreamworksanimation) +- [Epic Games](https://www.epicgames.com/) - [Fido](http://fido.se/) -- [Bläck](http://www.blackstudios.se/) -- [CGRU](http://cgru.info/) +- [Framestore](https://framestore.com) +- [ftrack](https://www.ftrack.com/) +- [Futureworks](http://futureworks.in/) +- [Industrial Brothers](http://industrialbrothers.com/) +- [Industriromantik](http://www.industriromantik.se/) +- [Mackevision](http://www.mackevision.com/) +- [Method Studios](http://www.methodstudios.com/) +- [Mikros Image](http://www.mikrosimage.com/) +- [Moonbot Studios](http://moonbotstudios.com/) - [MPC](http://www.moving-picture.com) +- [Overmind Studios](https://www.overmind-studios.de/) +- [Psyop](http://www.psyop.com/) +- [Raynault VFX](https://www.raynault.com/) - [Rising Sun Pictures](https://rsp.com.au) -- [Blur Studio](http://www.blur.com) -- [Mikros Image](http://www.mikrosimage.com/) -- [Mackevision](http://www.mackevision.com/) +- [Rodeo FX](https://www.rodeofx.com/en/) +- [Sony Pictures Imageworks](http://www.imageworks.com/) +- [Spin VFX](http://www.spinvfx.com/) +- [Weta Digital](https://www.wetafx.co.nz/) Presented at Siggraph 2016, BOF! @@ -413,6 +449,7 @@ Send us a pull-request with your project here. +- [USD Manager](http://www.usdmanager.org) - [Cosmos](http://cosmos.toolsfrom.space/) - [maya-capture-gui](https://github.com/BigRoy/maya-capture-gui) - [pyblish-lite](https://github.com/pyblish/pyblish-lite) @@ -423,6 +460,9 @@ - [Kraken](https://github.com/fabric-engine/Kraken) - [AFANASY](http://cgru.info/afanasy/afanasy) - [Syncplay](https://github.com/Syncplay/syncplay) +- [BlenderUpdater](https://github.com/overmindstudios/BlenderUpdater) +- [QtPyConvert](https://github.com/DigitalDomain/QtPyConvert) +- [Pyper](https://gitlab.com/brunoebe/pyper.git) <br> <br> @@ -439,6 +479,7 @@ | [QtPy][] | Scientific | N/A | MIT | | X | X | | [pyqode.qt][] | Scientific | PyQt5 | MIT | X | | X | | [QtExt][] | Film | N/A | N/A | | X | | +| [python_qt_binding][] | Robotics | N/A | BSD | X | X | X | X Also worth mentioning, [pyqt4topyqt5](https://github.com/rferrazz/pyqt4topyqt5); a good starting point for transitioning to Qt.py. @@ -448,6 +489,7 @@ [jupyter]: https://github.com/jupyter/qtconsole/blob/master/qtconsole/qt_loaders.py [pyqode.qt]: https://github.com/pyQode/pyqode.qt [QtExt]: https://bitbucket.org/ftrack/qtext +[python_qt_binding]: https://github.com/ros-visualization/python_qt_binding <br> <br> @@ -455,6 +497,8 @@ ### Developer Guide +- [Chat with us](https://gitter.im/Qt-py/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + Tests are performed on each aspect of the shim. - [Functional](tests.py)
