-
-
Notifications
You must be signed in to change notification settings - Fork 69
/
Copy pathqt_compat.py
202 lines (160 loc) · 6.4 KB
/
qt_compat.py
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
"""
Provide a common way to import Qt classes used by pytest-qt in a unique manner,
abstracting API differences between PyQt5/6 and PySide2/6.
.. note:: This module is not part of pytest-qt public API, hence its interface
may change between releases and users should not rely on it.
Based on from https://github.com/epage/PythonUtils.
"""
from collections import namedtuple, OrderedDict
import os
import sys
import pytest
VersionTuple = namedtuple("VersionTuple", "qt_api, qt_api_version, runtime, compiled")
QT_APIS = OrderedDict()
QT_APIS["pyside6"] = "PySide6"
QT_APIS["pyside2"] = "PySide2"
QT_APIS["pyqt6"] = "PyQt6"
QT_APIS["pyqt5"] = "PyQt5"
def _import(name):
"""Think call so we can mock it during testing"""
return __import__(name)
def _is_library_loaded(name):
return name in sys.modules
class _QtApi:
"""
Interface to the underlying Qt API currently configured for pytest-qt.
This object lazily loads all class references and other objects when the ``set_qt_api`` method
gets called, providing a uniform way to access the Qt classes.
"""
def __init__(self):
self._import_errors = {}
def _get_qt_api_from_env(self):
api = os.environ.get("PYTEST_QT_API")
supported_apis = QT_APIS.keys()
if api is not None:
api = api.lower()
if api not in supported_apis: # pragma: no cover
msg = f"Invalid value for $PYTEST_QT_API: {api}, expected one of {supported_apis}"
raise pytest.UsageError(msg)
return api
def _get_already_loaded_backend(self):
for api, backend in QT_APIS.items():
if _is_library_loaded(backend):
return api
return None
def _guess_qt_api(self): # pragma: no cover
def _can_import(name):
try:
_import(name)
return True
except ModuleNotFoundError as e:
self._import_errors[name] = str(e)
return False
# Note, not importing only the root namespace because when uninstalling from conda,
# the namespace can still be there.
for api, backend in QT_APIS.items():
if _can_import(f"{backend}.QtCore"):
return api
return None
def set_qt_api(self, api):
self.pytest_qt_api = (
self._get_qt_api_from_env()
or api
or self._get_already_loaded_backend()
or self._guess_qt_api()
)
self.is_pyside = self.pytest_qt_api in ["pyside2", "pyside6"]
self.is_pyqt = self.pytest_qt_api in ["pyqt5", "pyqt6"]
if not self.pytest_qt_api: # pragma: no cover
errors = "\n".join(
f" {module}: {reason}"
for module, reason in sorted(self._import_errors.items())
)
msg = (
"pytest-qt requires either PySide2, PySide6, PyQt5 or PyQt6 installed.\n"
+ errors
)
raise pytest.UsageError(msg)
_root_module = QT_APIS[self.pytest_qt_api]
def _import_module(module_name):
m = __import__(_root_module, globals(), locals(), [module_name], 0)
return getattr(m, module_name)
self.QtCore = QtCore = _import_module("QtCore")
self.QtGui = _import_module("QtGui")
self.QtTest = _import_module("QtTest")
self.QtWidgets = _import_module("QtWidgets")
self.QtQml = _import_module("QtQml")
self.QtQuick = _import_module("QtQuick")
self._check_qt_api_version()
# qInfo is not exposed in PySide2/6 (#232)
if hasattr(QtCore, "QMessageLogger"):
self.qInfo = lambda msg: QtCore.QMessageLogger().info(msg)
elif hasattr(QtCore, "qInfo"):
self.qInfo = QtCore.qInfo
else:
self.qInfo = None
self.qDebug = QtCore.qDebug
self.qWarning = QtCore.qWarning
self.qCritical = QtCore.qCritical
self.qFatal = QtCore.qFatal
if self.is_pyside:
self.Signal = QtCore.Signal
self.Slot = QtCore.Slot
self.Property = QtCore.Property
elif self.is_pyqt:
self.Signal = QtCore.pyqtSignal
self.Slot = QtCore.pyqtSlot
self.Property = QtCore.pyqtProperty
else:
assert False, "Expected either is_pyqt or is_pyside"
def _check_qt_api_version(self):
if not self.is_pyqt:
# We support all PySide versions
return
if self.QtCore.PYQT_VERSION == 0x060000: # 6.0.0
raise pytest.UsageError(
"PyQt 6.0 is not supported by pytest-qt, use 6.1+ instead."
)
elif self.QtCore.PYQT_VERSION < 0x050B00: # 5.11.0
raise pytest.UsageError(
"PyQt < 5.11 is not supported by pytest-qt, use 5.11+ instead."
)
def exec(self, obj, *args, **kwargs):
# exec was a keyword in Python 2, so PySide2 (and also PySide6 6.0)
# name the corresponding method "exec_" instead.
#
# The old _exec() alias is removed in PyQt6 and also deprecated as of
# PySide 6.1:
# https://codereview.qt-project.org/c/pyside/pyside-setup/+/342095
if hasattr(obj, "exec"):
return obj.exec(*args, **kwargs)
return obj.exec_(*args, **kwargs)
def get_versions(self):
if self.pytest_qt_api == "pyside6":
import PySide6
version = PySide6.__version__
return VersionTuple(
"PySide6", version, self.QtCore.qVersion(), self.QtCore.__version__
)
elif self.pytest_qt_api == "pyside2":
import PySide2
version = PySide2.__version__
return VersionTuple(
"PySide2", version, self.QtCore.qVersion(), self.QtCore.__version__
)
elif self.pytest_qt_api == "pyqt6":
return VersionTuple(
"PyQt6",
self.QtCore.PYQT_VERSION_STR,
self.QtCore.qVersion(),
self.QtCore.QT_VERSION_STR,
)
elif self.pytest_qt_api == "pyqt5":
return VersionTuple(
"PyQt5",
self.QtCore.PYQT_VERSION_STR,
self.QtCore.qVersion(),
self.QtCore.QT_VERSION_STR,
)
assert False, f"Internal error, unknown pytest_qt_api: {self.pytest_qt_api}"
qt_api = _QtApi()