##// END OF EJS Templates
ImportDenier: implement modern interface
Nikita Kniazev -
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,11 b''
1 import importlib
2 import pytest
3 from IPython.external.qt_loaders import ID
4
5
6 def test_import_denier():
7 ID.forbid("ipython_denied_module")
8 with pytest.raises(ImportError, match="disabled by IPython"):
9 import ipython_denied_module
10 with pytest.raises(ImportError, match="disabled by IPython"):
11 importlib.import_module("ipython_denied_module")
@@ -1,401 +1,400 b''
1 """
1 """
2 This module contains factory functions that attempt
2 This module contains factory functions that attempt
3 to return Qt submodules from the various python Qt bindings.
3 to return Qt submodules from the various python Qt bindings.
4
4
5 It also protects against double-importing Qt with different
5 It also protects against double-importing Qt with different
6 bindings, which is unstable and likely to crash
6 bindings, which is unstable and likely to crash
7
7
8 This is used primarily by qt and qt_for_kernel, and shouldn't
8 This is used primarily by qt and qt_for_kernel, and shouldn't
9 be accessed directly from the outside
9 be accessed directly from the outside
10 """
10 """
11 import importlib.abc
11 import sys
12 import sys
12 import types
13 import types
13 from functools import partial, lru_cache
14 from functools import partial, lru_cache
14 import operator
15 import operator
15
16
16 from IPython.utils.version import check_version
17 from IPython.utils.version import check_version
17
18
18 # ### Available APIs.
19 # ### Available APIs.
19 # Qt6
20 # Qt6
20 QT_API_PYQT6 = "pyqt6"
21 QT_API_PYQT6 = "pyqt6"
21 QT_API_PYSIDE6 = "pyside6"
22 QT_API_PYSIDE6 = "pyside6"
22
23
23 # Qt5
24 # Qt5
24 QT_API_PYQT5 = 'pyqt5'
25 QT_API_PYQT5 = 'pyqt5'
25 QT_API_PYSIDE2 = 'pyside2'
26 QT_API_PYSIDE2 = 'pyside2'
26
27
27 # Qt4
28 # Qt4
28 QT_API_PYQT = "pyqt" # Force version 2
29 QT_API_PYQT = "pyqt" # Force version 2
29 QT_API_PYQTv1 = "pyqtv1" # Force version 2
30 QT_API_PYQTv1 = "pyqtv1" # Force version 2
30 QT_API_PYSIDE = "pyside"
31 QT_API_PYSIDE = "pyside"
31
32
32 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
33 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
33
34
34 api_to_module = {
35 api_to_module = {
35 # Qt6
36 # Qt6
36 QT_API_PYQT6: "PyQt6",
37 QT_API_PYQT6: "PyQt6",
37 QT_API_PYSIDE6: "PySide6",
38 QT_API_PYSIDE6: "PySide6",
38 # Qt5
39 # Qt5
39 QT_API_PYQT5: "PyQt5",
40 QT_API_PYQT5: "PyQt5",
40 QT_API_PYSIDE2: "PySide2",
41 QT_API_PYSIDE2: "PySide2",
41 # Qt4
42 # Qt4
42 QT_API_PYSIDE: "PySide",
43 QT_API_PYSIDE: "PySide",
43 QT_API_PYQT: "PyQt4",
44 QT_API_PYQT: "PyQt4",
44 QT_API_PYQTv1: "PyQt4",
45 QT_API_PYQTv1: "PyQt4",
45 # default
46 # default
46 QT_API_PYQT_DEFAULT: "PyQt6",
47 QT_API_PYQT_DEFAULT: "PyQt6",
47 }
48 }
48
49
49
50
50 class ImportDenier(object):
51 class ImportDenier(importlib.abc.MetaPathFinder):
51 """Import Hook that will guard against bad Qt imports
52 """Import Hook that will guard against bad Qt imports
52 once IPython commits to a specific binding
53 once IPython commits to a specific binding
53 """
54 """
54
55
55 def __init__(self):
56 def __init__(self):
56 self.__forbidden = set()
57 self.__forbidden = set()
57
58
58 def forbid(self, module_name):
59 def forbid(self, module_name):
59 sys.modules.pop(module_name, None)
60 sys.modules.pop(module_name, None)
60 self.__forbidden.add(module_name)
61 self.__forbidden.add(module_name)
61
62
62 def find_module(self, fullname, path=None):
63 def find_spec(self, fullname, path, target=None):
63 if path:
64 if path:
64 return
65 return
65 if fullname in self.__forbidden:
66 if fullname in self.__forbidden:
66 return self
67 raise ImportError(
67
68 """
68 def load_module(self, fullname):
69 raise ImportError("""
70 Importing %s disabled by IPython, which has
69 Importing %s disabled by IPython, which has
71 already imported an Incompatible QT Binding: %s
70 already imported an Incompatible QT Binding: %s
72 """ % (fullname, loaded_api()))
71 """ % (fullname, loaded_api()))
73
72
74
73
75 ID = ImportDenier()
74 ID = ImportDenier()
76 sys.meta_path.insert(0, ID)
75 sys.meta_path.insert(0, ID)
77
76
78
77
79 def commit_api(api):
78 def commit_api(api):
80 """Commit to a particular API, and trigger ImportErrors on subsequent
79 """Commit to a particular API, and trigger ImportErrors on subsequent
81 dangerous imports"""
80 dangerous imports"""
82 modules = set(api_to_module.values())
81 modules = set(api_to_module.values())
83
82
84 modules.remove(api_to_module[api])
83 modules.remove(api_to_module[api])
85 for mod in modules:
84 for mod in modules:
86 ID.forbid(mod)
85 ID.forbid(mod)
87
86
88
87
89 def loaded_api():
88 def loaded_api():
90 """Return which API is loaded, if any
89 """Return which API is loaded, if any
91
90
92 If this returns anything besides None,
91 If this returns anything besides None,
93 importing any other Qt binding is unsafe.
92 importing any other Qt binding is unsafe.
94
93
95 Returns
94 Returns
96 -------
95 -------
97 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
96 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
98 """
97 """
99 if sys.modules.get("PyQt6.QtCore"):
98 if sys.modules.get("PyQt6.QtCore"):
100 return QT_API_PYQT6
99 return QT_API_PYQT6
101 elif sys.modules.get("PySide6.QtCore"):
100 elif sys.modules.get("PySide6.QtCore"):
102 return QT_API_PYSIDE6
101 return QT_API_PYSIDE6
103 elif sys.modules.get("PyQt5.QtCore"):
102 elif sys.modules.get("PyQt5.QtCore"):
104 return QT_API_PYQT5
103 return QT_API_PYQT5
105 elif sys.modules.get("PySide2.QtCore"):
104 elif sys.modules.get("PySide2.QtCore"):
106 return QT_API_PYSIDE2
105 return QT_API_PYSIDE2
107 elif sys.modules.get("PyQt4.QtCore"):
106 elif sys.modules.get("PyQt4.QtCore"):
108 if qtapi_version() == 2:
107 if qtapi_version() == 2:
109 return QT_API_PYQT
108 return QT_API_PYQT
110 else:
109 else:
111 return QT_API_PYQTv1
110 return QT_API_PYQTv1
112 elif sys.modules.get("PySide.QtCore"):
111 elif sys.modules.get("PySide.QtCore"):
113 return QT_API_PYSIDE
112 return QT_API_PYSIDE
114
113
115 return None
114 return None
116
115
117
116
118 def has_binding(api):
117 def has_binding(api):
119 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
118 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
120
119
121 Parameters
120 Parameters
122 ----------
121 ----------
123 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
122 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
124 Which module to check for
123 Which module to check for
125
124
126 Returns
125 Returns
127 -------
126 -------
128 True if the relevant module appears to be importable
127 True if the relevant module appears to be importable
129 """
128 """
130 module_name = api_to_module[api]
129 module_name = api_to_module[api]
131 from importlib.util import find_spec
130 from importlib.util import find_spec
132
131
133 required = ['QtCore', 'QtGui', 'QtSvg']
132 required = ['QtCore', 'QtGui', 'QtSvg']
134 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
133 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
135 # QT5 requires QtWidgets too
134 # QT5 requires QtWidgets too
136 required.append('QtWidgets')
135 required.append('QtWidgets')
137
136
138 for submod in required:
137 for submod in required:
139 try:
138 try:
140 spec = find_spec('%s.%s' % (module_name, submod))
139 spec = find_spec('%s.%s' % (module_name, submod))
141 except ImportError:
140 except ImportError:
142 # Package (e.g. PyQt5) not found
141 # Package (e.g. PyQt5) not found
143 return False
142 return False
144 else:
143 else:
145 if spec is None:
144 if spec is None:
146 # Submodule (e.g. PyQt5.QtCore) not found
145 # Submodule (e.g. PyQt5.QtCore) not found
147 return False
146 return False
148
147
149 if api == QT_API_PYSIDE:
148 if api == QT_API_PYSIDE:
150 # We can also safely check PySide version
149 # We can also safely check PySide version
151 import PySide
150 import PySide
152 return check_version(PySide.__version__, '1.0.3')
151 return check_version(PySide.__version__, '1.0.3')
153
152
154 return True
153 return True
155
154
156
155
157 def qtapi_version():
156 def qtapi_version():
158 """Return which QString API has been set, if any
157 """Return which QString API has been set, if any
159
158
160 Returns
159 Returns
161 -------
160 -------
162 The QString API version (1 or 2), or None if not set
161 The QString API version (1 or 2), or None if not set
163 """
162 """
164 try:
163 try:
165 import sip
164 import sip
166 except ImportError:
165 except ImportError:
167 # as of PyQt5 5.11, sip is no longer available as a top-level
166 # as of PyQt5 5.11, sip is no longer available as a top-level
168 # module and needs to be imported from the PyQt5 namespace
167 # module and needs to be imported from the PyQt5 namespace
169 try:
168 try:
170 from PyQt5 import sip
169 from PyQt5 import sip
171 except ImportError:
170 except ImportError:
172 return
171 return
173 try:
172 try:
174 return sip.getapi('QString')
173 return sip.getapi('QString')
175 except ValueError:
174 except ValueError:
176 return
175 return
177
176
178
177
179 def can_import(api):
178 def can_import(api):
180 """Safely query whether an API is importable, without importing it"""
179 """Safely query whether an API is importable, without importing it"""
181 if not has_binding(api):
180 if not has_binding(api):
182 return False
181 return False
183
182
184 current = loaded_api()
183 current = loaded_api()
185 if api == QT_API_PYQT_DEFAULT:
184 if api == QT_API_PYQT_DEFAULT:
186 return current in [QT_API_PYQT6, None]
185 return current in [QT_API_PYQT6, None]
187 else:
186 else:
188 return current in [api, None]
187 return current in [api, None]
189
188
190
189
191 def import_pyqt4(version=2):
190 def import_pyqt4(version=2):
192 """
191 """
193 Import PyQt4
192 Import PyQt4
194
193
195 Parameters
194 Parameters
196 ----------
195 ----------
197 version : 1, 2, or None
196 version : 1, 2, or None
198 Which QString/QVariant API to use. Set to None to use the system
197 Which QString/QVariant API to use. Set to None to use the system
199 default
198 default
200
199
201 ImportErrors raised within this function are non-recoverable
200 ImportErrors raised within this function are non-recoverable
202 """
201 """
203 # The new-style string API (version=2) automatically
202 # The new-style string API (version=2) automatically
204 # converts QStrings to Unicode Python strings. Also, automatically unpacks
203 # converts QStrings to Unicode Python strings. Also, automatically unpacks
205 # QVariants to their underlying objects.
204 # QVariants to their underlying objects.
206 import sip
205 import sip
207
206
208 if version is not None:
207 if version is not None:
209 sip.setapi('QString', version)
208 sip.setapi('QString', version)
210 sip.setapi('QVariant', version)
209 sip.setapi('QVariant', version)
211
210
212 from PyQt4 import QtGui, QtCore, QtSvg
211 from PyQt4 import QtGui, QtCore, QtSvg
213
212
214 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
213 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
215 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
214 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
216 QtCore.PYQT_VERSION_STR)
215 QtCore.PYQT_VERSION_STR)
217
216
218 # Alias PyQt-specific functions for PySide compatibility.
217 # Alias PyQt-specific functions for PySide compatibility.
219 QtCore.Signal = QtCore.pyqtSignal
218 QtCore.Signal = QtCore.pyqtSignal
220 QtCore.Slot = QtCore.pyqtSlot
219 QtCore.Slot = QtCore.pyqtSlot
221
220
222 # query for the API version (in case version == None)
221 # query for the API version (in case version == None)
223 version = sip.getapi('QString')
222 version = sip.getapi('QString')
224 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
223 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
225 return QtCore, QtGui, QtSvg, api
224 return QtCore, QtGui, QtSvg, api
226
225
227
226
228 def import_pyqt5():
227 def import_pyqt5():
229 """
228 """
230 Import PyQt5
229 Import PyQt5
231
230
232 ImportErrors raised within this function are non-recoverable
231 ImportErrors raised within this function are non-recoverable
233 """
232 """
234
233
235 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
234 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
236
235
237 # Alias PyQt-specific functions for PySide compatibility.
236 # Alias PyQt-specific functions for PySide compatibility.
238 QtCore.Signal = QtCore.pyqtSignal
237 QtCore.Signal = QtCore.pyqtSignal
239 QtCore.Slot = QtCore.pyqtSlot
238 QtCore.Slot = QtCore.pyqtSlot
240
239
241 # Join QtGui and QtWidgets for Qt4 compatibility.
240 # Join QtGui and QtWidgets for Qt4 compatibility.
242 QtGuiCompat = types.ModuleType('QtGuiCompat')
241 QtGuiCompat = types.ModuleType('QtGuiCompat')
243 QtGuiCompat.__dict__.update(QtGui.__dict__)
242 QtGuiCompat.__dict__.update(QtGui.__dict__)
244 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
243 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
245
244
246 api = QT_API_PYQT5
245 api = QT_API_PYQT5
247 return QtCore, QtGuiCompat, QtSvg, api
246 return QtCore, QtGuiCompat, QtSvg, api
248
247
249
248
250 def import_pyqt6():
249 def import_pyqt6():
251 """
250 """
252 Import PyQt6
251 Import PyQt6
253
252
254 ImportErrors raised within this function are non-recoverable
253 ImportErrors raised within this function are non-recoverable
255 """
254 """
256
255
257 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
256 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
258
257
259 # Alias PyQt-specific functions for PySide compatibility.
258 # Alias PyQt-specific functions for PySide compatibility.
260 QtCore.Signal = QtCore.pyqtSignal
259 QtCore.Signal = QtCore.pyqtSignal
261 QtCore.Slot = QtCore.pyqtSlot
260 QtCore.Slot = QtCore.pyqtSlot
262
261
263 # Join QtGui and QtWidgets for Qt4 compatibility.
262 # Join QtGui and QtWidgets for Qt4 compatibility.
264 QtGuiCompat = types.ModuleType("QtGuiCompat")
263 QtGuiCompat = types.ModuleType("QtGuiCompat")
265 QtGuiCompat.__dict__.update(QtGui.__dict__)
264 QtGuiCompat.__dict__.update(QtGui.__dict__)
266 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
265 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
267
266
268 api = QT_API_PYQT6
267 api = QT_API_PYQT6
269 return QtCore, QtGuiCompat, QtSvg, api
268 return QtCore, QtGuiCompat, QtSvg, api
270
269
271
270
272 def import_pyside():
271 def import_pyside():
273 """
272 """
274 Import PySide
273 Import PySide
275
274
276 ImportErrors raised within this function are non-recoverable
275 ImportErrors raised within this function are non-recoverable
277 """
276 """
278 from PySide import QtGui, QtCore, QtSvg
277 from PySide import QtGui, QtCore, QtSvg
279 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
278 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
280
279
281 def import_pyside2():
280 def import_pyside2():
282 """
281 """
283 Import PySide2
282 Import PySide2
284
283
285 ImportErrors raised within this function are non-recoverable
284 ImportErrors raised within this function are non-recoverable
286 """
285 """
287 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
286 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
288
287
289 # Join QtGui and QtWidgets for Qt4 compatibility.
288 # Join QtGui and QtWidgets for Qt4 compatibility.
290 QtGuiCompat = types.ModuleType('QtGuiCompat')
289 QtGuiCompat = types.ModuleType('QtGuiCompat')
291 QtGuiCompat.__dict__.update(QtGui.__dict__)
290 QtGuiCompat.__dict__.update(QtGui.__dict__)
292 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
291 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
293 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
292 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
294
293
295 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
294 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
296
295
297
296
298 def import_pyside6():
297 def import_pyside6():
299 """
298 """
300 Import PySide6
299 Import PySide6
301
300
302 ImportErrors raised within this function are non-recoverable
301 ImportErrors raised within this function are non-recoverable
303 """
302 """
304 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
303 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
305
304
306 # Join QtGui and QtWidgets for Qt4 compatibility.
305 # Join QtGui and QtWidgets for Qt4 compatibility.
307 QtGuiCompat = types.ModuleType("QtGuiCompat")
306 QtGuiCompat = types.ModuleType("QtGuiCompat")
308 QtGuiCompat.__dict__.update(QtGui.__dict__)
307 QtGuiCompat.__dict__.update(QtGui.__dict__)
309 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
308 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
310 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
309 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
311
310
312 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
311 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
313
312
314
313
315 def load_qt(api_options):
314 def load_qt(api_options):
316 """
315 """
317 Attempt to import Qt, given a preference list
316 Attempt to import Qt, given a preference list
318 of permissible bindings
317 of permissible bindings
319
318
320 It is safe to call this function multiple times.
319 It is safe to call this function multiple times.
321
320
322 Parameters
321 Parameters
323 ----------
322 ----------
324 api_options: List of strings
323 api_options: List of strings
325 The order of APIs to try. Valid items are 'pyside', 'pyside2',
324 The order of APIs to try. Valid items are 'pyside', 'pyside2',
326 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
325 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
327
326
328 Returns
327 Returns
329 -------
328 -------
330
329
331 A tuple of QtCore, QtGui, QtSvg, QT_API
330 A tuple of QtCore, QtGui, QtSvg, QT_API
332 The first three are the Qt modules. The last is the
331 The first three are the Qt modules. The last is the
333 string indicating which module was loaded.
332 string indicating which module was loaded.
334
333
335 Raises
334 Raises
336 ------
335 ------
337 ImportError, if it isn't possible to import any requested
336 ImportError, if it isn't possible to import any requested
338 bindings (either because they aren't installed, or because
337 bindings (either because they aren't installed, or because
339 an incompatible library has already been installed)
338 an incompatible library has already been installed)
340 """
339 """
341 loaders = {
340 loaders = {
342 # Qt6
341 # Qt6
343 QT_API_PYQT6: import_pyqt6,
342 QT_API_PYQT6: import_pyqt6,
344 QT_API_PYSIDE6: import_pyside6,
343 QT_API_PYSIDE6: import_pyside6,
345 # Qt5
344 # Qt5
346 QT_API_PYQT5: import_pyqt5,
345 QT_API_PYQT5: import_pyqt5,
347 QT_API_PYSIDE2: import_pyside2,
346 QT_API_PYSIDE2: import_pyside2,
348 # Qt4
347 # Qt4
349 QT_API_PYSIDE: import_pyside,
348 QT_API_PYSIDE: import_pyside,
350 QT_API_PYQT: import_pyqt4,
349 QT_API_PYQT: import_pyqt4,
351 QT_API_PYQTv1: partial(import_pyqt4, version=1),
350 QT_API_PYQTv1: partial(import_pyqt4, version=1),
352 # default
351 # default
353 QT_API_PYQT_DEFAULT: import_pyqt6,
352 QT_API_PYQT_DEFAULT: import_pyqt6,
354 }
353 }
355
354
356 for api in api_options:
355 for api in api_options:
357
356
358 if api not in loaders:
357 if api not in loaders:
359 raise RuntimeError(
358 raise RuntimeError(
360 "Invalid Qt API %r, valid values are: %s" %
359 "Invalid Qt API %r, valid values are: %s" %
361 (api, ", ".join(["%r" % k for k in loaders.keys()])))
360 (api, ", ".join(["%r" % k for k in loaders.keys()])))
362
361
363 if not can_import(api):
362 if not can_import(api):
364 continue
363 continue
365
364
366 #cannot safely recover from an ImportError during this
365 #cannot safely recover from an ImportError during this
367 result = loaders[api]()
366 result = loaders[api]()
368 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
367 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
369 commit_api(api)
368 commit_api(api)
370 return result
369 return result
371 else:
370 else:
372 raise ImportError("""
371 raise ImportError("""
373 Could not load requested Qt binding. Please ensure that
372 Could not load requested Qt binding. Please ensure that
374 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
373 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
375 and only one is imported per session.
374 and only one is imported per session.
376
375
377 Currently-imported Qt library: %r
376 Currently-imported Qt library: %r
378 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
377 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
379 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
378 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
380 PySide >= 1.0.3 installed: %s
379 PySide >= 1.0.3 installed: %s
381 PySide2 installed: %s
380 PySide2 installed: %s
382 Tried to load: %r
381 Tried to load: %r
383 """ % (loaded_api(),
382 """ % (loaded_api(),
384 has_binding(QT_API_PYQT),
383 has_binding(QT_API_PYQT),
385 has_binding(QT_API_PYQT5),
384 has_binding(QT_API_PYQT5),
386 has_binding(QT_API_PYSIDE),
385 has_binding(QT_API_PYSIDE),
387 has_binding(QT_API_PYSIDE2),
386 has_binding(QT_API_PYSIDE2),
388 api_options))
387 api_options))
389
388
390
389
391 def enum_factory(QT_API, QtCore):
390 def enum_factory(QT_API, QtCore):
392 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
391 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
393
392
394 @lru_cache(None)
393 @lru_cache(None)
395 def _enum(name):
394 def _enum(name):
396 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
395 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
397 return operator.attrgetter(
396 return operator.attrgetter(
398 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
397 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
399 )(sys.modules[QtCore.__package__])
398 )(sys.modules[QtCore.__package__])
400
399
401 return _enum
400 return _enum
General Comments 0
You need to be logged in to leave comments. Login now