File: _fixes.py

package info (click to toggle)
python-anyqt 0.2.0-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 560 kB
  • sloc: python: 4,087; makefile: 192; sh: 3
file content (298 lines) | stat: -rw-r--r-- 10,607 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
import os
import enum
import warnings
from typing import Type, Dict, Any


def fix_pyqt5_QGraphicsItem_itemChange():
    """
    Attempt to remedy:
    https://www.riverbankcomputing.com/pipermail/pyqt/2016-February/037015.html
    """
    from PyQt5.QtWidgets import QGraphicsObject, QGraphicsItem

    class Obj(QGraphicsObject):
        def itemChange(self, change, value):
            return QGraphicsObject.itemChange(self, change, value)

    obj = Obj()
    parent = Obj()
    obj.setParentItem(parent)

    if obj.parentItem() is None:
        # There was probably already some signal defined using QObject's
        # subclass from QtWidgets.
        # We will monkey patch the QGraphicsItem.itemChange and explicitly
        # sip.cast all input and output QGraphicsItem instances
        import sip
        QGraphicsItem_itemChange_old = QGraphicsItem.itemChange

        # All the QGraphicsItem.ItemChange flags which accept/return
        # a QGraphicsItem
        changeset = {
            QGraphicsItem.ItemParentChange,
            QGraphicsItem.ItemParentHasChanged,
            QGraphicsItem.ItemChildAddedChange,
            QGraphicsItem.ItemChildRemovedChange,
        }

        def QGraphicsItem_itemChange(self, change, value):
            if change in changeset:
                if isinstance(value, QGraphicsItem):
                    value = sip.cast(value, QGraphicsItem)
                rval = QGraphicsItem_itemChange_old(self, change, value)
                if isinstance(rval, QGraphicsItem):
                    rval = sip.cast(rval, QGraphicsItem)
                return rval
            else:
                return QGraphicsItem_itemChange_old(self, change, value)

        QGraphicsItem.itemChange = QGraphicsItem_itemChange
        warnings.warn("Monkey patching QGraphicsItem.itemChange",
                      RuntimeWarning)


def fix_pyqt6_qtgui_qaction_menu(namespace):
    if namespace.get("__name__") != "AnyQt.QtGui":
        return
    import ctypes
    from ._ctypes import load_qtlib
    qtgui = load_qtlib("QtGui")
    if os.name == "posix":  # assuming gcc compatible compiler
        _QAction_setMenuObject = qtgui['_ZN7QAction13setMenuObjectEP7QObject']
        _QAction_menuObject = qtgui['_ZNK7QAction10menuObjectEv']
    elif os.name == "nt":
        _QAction_setMenuObject = qtgui['?setMenuObject@QAction@@AEAAXPEAVQObject@@@Z']
        _QAction_menuObject = qtgui['?menuObject@QAction@@AEBAPEAVQObject@@XZ']
    else:
        return

    _QAction_menuObject.argtypes = [ctypes.c_void_p]
    _QAction_menuObject.restype = ctypes.c_void_p

    _QAction_setMenuObject.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

    from PyQt6.QtGui import QAction
    from PyQt6.sip import isdeleted, unwrapinstance, wrapinstance
    try:
        from PyQt6.QtWidgets import QMenu
    except ImportError:
        return  # No QtWidgets then no setMenu

    if hasattr(QAction, "setMenu"):
        return

    def QAction_setMenu(self, menu):
        if menu is not None and not isinstance(menu, QMenu):
            raise TypeError()
        if menu is not None and isdeleted(menu):
            raise RuntimeError()
        if isdeleted(self):
            raise RuntimeError()
        self.__QAction_menu = menu
        _QAction_setMenuObject(
            unwrapinstance(self),
            unwrapinstance(menu) if menu is not None else 0
        )

    def QAction_menu(self):
        if isdeleted(self):
            raise RuntimeError()
        ptr = _QAction_menuObject(unwrapinstance(self))
        if ptr is None:
            return None
        menu = wrapinstance(ptr, QMenu)
        return menu

    QAction.setMenu = QAction_setMenu
    QAction.menu = QAction_menu


def fix_pyqt6_unscoped_enum(namespace: Dict[str, Any]):
    """
    Lift all PyQt6 enum members up to class level.
    i.e. Qt.ItemFlags.DisplayRole -> Qt.DisplayRole
    """
    from PyQt6 import sip

    def members(enum: Type[enum.Enum]):
        return ((name, enum[name]) for name in enum.__members__)

    def lift_enum_namespace(type_, enum: Type[enum.Enum]):
        for name, value in members(enum):
            setattr(type_, name, value)

    def is_unscoped_enum(value):
        return isinstance(value, type) and issubclass(value, enum.Enum)

    def can_lift(type_, enum: Type[enum.Enum]):
        namespace = type_.__dict__
        return not any(name in namespace and namespace[name] is not value
                       for name, value in members(enum))

    for _, class_ in list(namespace.items()):
        if isinstance(class_, (sip.simplewrapper, sip.wrappertype)):
            for name, value in list(class_.__dict__.items()):
                if is_unscoped_enum(value):
                    if can_lift(class_, value):
                        lift_enum_namespace(class_, value)


def fix_pyqt5_missing_enum_members(namespace: Dict[str, Any]):
    import enum
    from AnyQt import sip

    def is_pyqt_enum_type(value):
        return (isinstance(value, type)
                and issubclass(value, int)
                and value is not int
                and "." in value.__qualname__
                and not issubclass(value, enum.Enum))

    for _, class_ in list(namespace.items()):
        if isinstance(class_, (sip.simplewrapper, sip.wrappertype)):
            enum_types = {}
            for name, value in list(class_.__dict__.items()):
                if is_pyqt_enum_type(value):
                    enum_types[value.__qualname__] = value
            types_ = tuple(enum_types.values())
            for name, value in list(class_.__dict__.items()):
                if type(value) in types_:
                    type_ = enum_types[type(value).__qualname__]
                    if hasattr(type_, name) and (
                            getattr(type_, name) != value and
                            type_.__qualname__ != "QKeySequence.StandardKey"
                    ):
                        warnings.warn(
                            f"{type_} {name} is already present and is not "
                            f"{value}", RuntimeWarning
                        )
                    elif not hasattr(type_, name):
                        setattr(type_, name, value)


def fix_pyside_QActionEvent_action(namespace):
    if namespace.get("__name__") != "AnyQt.QtGui":
        return
    import ctypes
    try:
        from PySide2 import shiboken2  # PySide2 < 5.12.0
    except ImportError:
        import shiboken2
    from AnyQt.QtGui import QActionEvent
    from AnyQt.QtWidgets import QAction

    class _QActionEvent(ctypes.Structure):
        _fields_ = [
            ("vtable", ctypes.c_void_p),
            # QEvent
            ("d", ctypes.c_void_p),       # private data ptr
            ("t", ctypes.c_ushort),       # type
            ("_flags", ctypes.c_ushort),  # various flags
            # QActionEvent
            ("act", ctypes.c_void_p),     # QAction *act
            ("bef", ctypes.c_void_p),     # QAction *bef
        ]

        def action(self):
            return from_address(self.act, QAction)

        def before(self):
            return from_address(self.bef, QAction)

        @classmethod
        def from_event(cls, event: QActionEvent):
            p, = shiboken2.getCppPointer(event)
            return cls.from_address(p)

    def from_address(address: int, type_):
        if address:
            return shiboken2.wrapInstance(address, type_)
        else:
            return None

    def action(self):
        ev = _QActionEvent.from_event(self)
        return ev.action()

    def before(self):
        ev = _QActionEvent.from_event(self)
        return ev.before()

    if not hasattr(QActionEvent, "action"):
        QActionEvent.action = action
    if not hasattr(QActionEvent, "before"):
        QActionEvent.before = before


def fix_pyside_exec(namespace):
    if namespace.get("__name__") == "AnyQt.QtWidgets":
        from PySide2.QtWidgets import QApplication, QDialog, QMenu
        if "exec" not in QApplication.__dict__:
            QApplication.exec = lambda self: QApplication.exec_()
        if not hasattr(QDialog, "exec"):
            QDialog.exec = lambda self: QDialog.exec_(self)
        if not hasattr(QMenu, "exec"):
            QMenu.exec = lambda self: QMenu.exec_(self)
    if namespace.get("__name__") == "AnyQt.QtGui":
        from PySide2.QtGui import QGuiApplication, QDrag
        if "exec" not in QGuiApplication.__dict__:
            QGuiApplication.exec = lambda self: QGuiApplication.exec_()
        if not hasattr(QDrag, "exec"):
            QDrag.exec = (
                lambda self, *args, **kwargs: QDrag.exec_(self, *args, **kwargs)
            )
    elif namespace.get("__name__") == "AnyQt.QtCore":
        from PySide2.QtCore import QCoreApplication, QEventLoop, QThread
        if not hasattr(QCoreApplication, "exec"):
            QCoreApplication.exec = lambda self: QCoreApplication.exec_()
        if not hasattr(QEventLoop, "exec"):
            QEventLoop.exec = (
                lambda self, *args, **kwargs:
                    QEventLoop.exec_(self, *args, **kwargs)
            )
        if not hasattr(QThread, "exec"):
            QThread.exec = lambda self: QThread.exec_(self)
    elif namespace.get("__name__") == "AnyQt.QtPrintSupport":
        from PySide2.QtPrintSupport import QPageSetupDialog, QPrintDialog
        if "exec" not in QPageSetupDialog.__dict__:
            QPageSetupDialog.exec = lambda self: QPageSetupDialog.exec_(self)
        if "exec" not in QPrintDialog.__dict__:
            QPrintDialog.exec = lambda self: QPrintDialog.exec_(self)


def fix_qstandarditem_insert_row(namespace):
    if namespace.get("__name__") == "AnyQt.QtGui":
        QStandardItem = namespace["QStandardItem"]
        __QStandardItem_insertRow = QStandardItem.insertRow

        def QStandardItem_insertRow(self, row, items):
            if isinstance(items, QStandardItem):
                # PYSIDE-237
                __QStandardItem_insertRow(self, row, [items])
            else:
                __QStandardItem_insertRow(self, row, items)
        QStandardItem.insertRow = QStandardItem_insertRow


GLOBAL_FIXES = {
    "pyqt6": [
        fix_pyqt6_unscoped_enum,
        fix_pyqt6_qtgui_qaction_menu,
    ],
    "pyqt5": [
        fix_pyqt5_missing_enum_members,
    ],
    "pyside2": [
        fix_pyside_QActionEvent_action,
        fix_pyside_exec,
        fix_qstandarditem_insert_row,
    ],
}


def global_fixes(namespace):
    from AnyQt import _api
    fixes = GLOBAL_FIXES.get(_api.USED_API, [])
    for fixer in fixes:
        fixer(namespace)