1010"""
1111import sys
1212import types
13- from functools import partial
14- from importlib import import_module
13+ from functools import partial , lru_cache
14+ import operator
1515
1616from IPython .utils .version import check_version
1717
18- # Available APIs.
19- QT_API_PYQT = 'pyqt' # Force version 2
18+ # ### Available APIs.
19+ # Qt6
20+ QT_API_PYQT6 = "pyqt6"
21+ QT_API_PYSIDE6 = "pyside6"
22+
23+ # Qt5
2024QT_API_PYQT5 = 'pyqt5'
21- QT_API_PYQTv1 = 'pyqtv1' # Force version 2
22- QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2
23- QT_API_PYSIDE = 'pyside'
2425QT_API_PYSIDE2 = 'pyside2'
2526
26- api_to_module = {QT_API_PYSIDE2 : 'PySide2' ,
27- QT_API_PYSIDE : 'PySide' ,
28- QT_API_PYQT : 'PyQt4' ,
29- QT_API_PYQTv1 : 'PyQt4' ,
30- QT_API_PYQT5 : 'PyQt5' ,
31- QT_API_PYQT_DEFAULT : 'PyQt4' ,
32- }
27+ # Qt4
28+ QT_API_PYQT = 'pyqt' # Force version 2
29+ QT_API_PYQTv1 = 'pyqtv1' # Force version 2
30+ QT_API_PYSIDE = 'pyside'
31+
32+ QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2
33+
34+ api_to_module = {
35+ # Qt6
36+ QT_API_PYQT6 : "PyQt6" ,
37+ QT_API_PYSIDE6 : "PySide6" ,
38+ # Qt5
39+ QT_API_PYQT5 : 'PyQt5' ,
40+ QT_API_PYSIDE2 : 'PySide2' ,
41+ # Qt4
42+ QT_API_PYSIDE : 'PySide' ,
43+ QT_API_PYQT : 'PyQt4' ,
44+ QT_API_PYQTv1 : 'PyQt4' ,
45+ # default
46+ QT_API_PYQT_DEFAULT : 'PyQt6' ,
47+ }
3348
3449
3550class ImportDenier (object ):
@@ -56,30 +71,19 @@ def load_module(self, fullname):
5671 already imported an Incompatible QT Binding: %s
5772 """ % (fullname , loaded_api ()))
5873
74+
5975ID = ImportDenier ()
6076sys .meta_path .insert (0 , ID )
6177
6278
6379def commit_api (api ):
6480 """Commit to a particular API, and trigger ImportErrors on subsequent
6581 dangerous imports"""
82+ modules = set (api_to_module .values ())
6683
67- if api == QT_API_PYSIDE2 :
68- ID .forbid ('PySide' )
69- ID .forbid ('PyQt4' )
70- ID .forbid ('PyQt5' )
71- elif api == QT_API_PYSIDE :
72- ID .forbid ('PySide2' )
73- ID .forbid ('PyQt4' )
74- ID .forbid ('PyQt5' )
75- elif api == QT_API_PYQT5 :
76- ID .forbid ('PySide2' )
77- ID .forbid ('PySide' )
78- ID .forbid ('PyQt4' )
79- else : # There are three other possibilities, all representing PyQt4
80- ID .forbid ('PyQt5' )
81- ID .forbid ('PySide2' )
82- ID .forbid ('PySide' )
84+ modules .remove (api_to_module [api ])
85+ for mod in modules :
86+ ID .forbid (mod )
8387
8488
8589def loaded_api ():
@@ -90,19 +94,24 @@ def loaded_api():
9094
9195 Returns
9296 -------
93- None, 'pyside2', 'pyside', 'pyqt', 'pyqt5', or 'pyqtv1'
97+ None, 'pyside6', 'pyqt6', ' pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
9498 """
95- if 'PyQt4.QtCore' in sys .modules :
99+ if sys .modules .get ("PyQt6.QtCore" ):
100+ return QT_API_PYQT6
101+ elif sys .modules .get ("PySide6.QtCore" ):
102+ return QT_API_PYSIDE6
103+ elif sys .modules .get ("PyQt5.QtCore" ):
104+ return QT_API_PYQT5
105+ elif sys .modules .get ("PySide2.QtCore" ):
106+ return QT_API_PYSIDE2
107+ elif sys .modules .get ('PyQt4.QtCore' ):
96108 if qtapi_version () == 2 :
97109 return QT_API_PYQT
98110 else :
99111 return QT_API_PYQTv1
100- elif 'PySide.QtCore' in sys . modules :
112+ elif sys . modules . get ( 'PySide.QtCore' ) :
101113 return QT_API_PYSIDE
102- elif 'PySide2.QtCore' in sys .modules :
103- return QT_API_PYSIDE2
104- elif 'PyQt5.QtCore' in sys .modules :
105- return QT_API_PYQT5
114+
106115 return None
107116
108117
@@ -122,7 +131,7 @@ def has_binding(api):
122131 from importlib .util import find_spec
123132
124133 required = ['QtCore' , 'QtGui' , 'QtSvg' ]
125- if api in (QT_API_PYQT5 , QT_API_PYSIDE2 ):
134+ if api in (QT_API_PYQT5 , QT_API_PYSIDE2 , QT_API_PYQT6 , QT_API_PYSIDE6 ):
126135 # QT5 requires QtWidgets too
127136 required .append ('QtWidgets' )
128137
@@ -174,7 +183,7 @@ def can_import(api):
174183
175184 current = loaded_api ()
176185 if api == QT_API_PYQT_DEFAULT :
177- return current in [QT_API_PYQT , QT_API_PYQTv1 , None ]
186+ return current in [QT_API_PYQT6 , None ]
178187 else :
179188 return current in [api , None ]
180189
@@ -224,7 +233,7 @@ def import_pyqt5():
224233 """
225234
226235 from PyQt5 import QtCore , QtSvg , QtWidgets , QtGui
227-
236+
228237 # Alias PyQt-specific functions for PySide compatibility.
229238 QtCore .Signal = QtCore .pyqtSignal
230239 QtCore .Slot = QtCore .pyqtSlot
@@ -237,6 +246,27 @@ def import_pyqt5():
237246 api = QT_API_PYQT5
238247 return QtCore , QtGuiCompat , QtSvg , api
239248
249+ def import_pyqt6 ():
250+ """
251+ Import PyQt6
252+
253+ ImportErrors rasied within this function are non-recoverable
254+ """
255+
256+ from PyQt6 import QtCore , QtSvg , QtWidgets , QtGui
257+
258+ # Alias PyQt-specific functions for PySide compatibility.
259+ QtCore .Signal = QtCore .pyqtSignal
260+ QtCore .Slot = QtCore .pyqtSlot
261+
262+ # Join QtGui and QtWidgets for Qt4 compatibility.
263+ QtGuiCompat = types .ModuleType ('QtGuiCompat' )
264+ QtGuiCompat .__dict__ .update (QtGui .__dict__ )
265+ QtGuiCompat .__dict__ .update (QtWidgets .__dict__ )
266+
267+ api = QT_API_PYQT6
268+ return QtCore , QtGuiCompat , QtSvg , api
269+
240270
241271def import_pyside ():
242272 """
@@ -263,6 +293,22 @@ def import_pyside2():
263293
264294 return QtCore , QtGuiCompat , QtSvg , QT_API_PYSIDE2
265295
296+ def import_pyside6 ():
297+ """
298+ Import PySide6
299+
300+ ImportErrors raised within this function are non-recoverable
301+ """
302+ from PySide6 import QtGui , QtCore , QtSvg , QtWidgets , QtPrintSupport
303+
304+ # Join QtGui and QtWidgets for Qt4 compatibility.
305+ QtGuiCompat = types .ModuleType ('QtGuiCompat' )
306+ QtGuiCompat .__dict__ .update (QtGui .__dict__ )
307+ QtGuiCompat .__dict__ .update (QtWidgets .__dict__ )
308+ QtGuiCompat .__dict__ .update (QtPrintSupport .__dict__ )
309+
310+ return QtCore , QtGuiCompat , QtSvg , QT_API_PYSIDE6
311+
266312
267313def load_qt (api_options ):
268314 """
@@ -291,13 +337,19 @@ def load_qt(api_options):
291337 an incompatible library has already been installed)
292338 """
293339 loaders = {
294- QT_API_PYSIDE2 : import_pyside2 ,
295- QT_API_PYSIDE : import_pyside ,
296- QT_API_PYQT : import_pyqt4 ,
297- QT_API_PYQT5 : import_pyqt5 ,
298- QT_API_PYQTv1 : partial (import_pyqt4 , version = 1 ),
299- QT_API_PYQT_DEFAULT : partial (import_pyqt4 , version = None )
300- }
340+ # Qt6
341+ QT_API_PYQT6 : import_pyqt6 ,
342+ QT_API_PYSIDE6 : import_pyside6 ,
343+ # Qt5
344+ QT_API_PYQT5 : import_pyqt5 ,
345+ QT_API_PYSIDE2 : import_pyside2 ,
346+ # Qt4
347+ QT_API_PYSIDE : import_pyside ,
348+ QT_API_PYQT : import_pyqt4 ,
349+ QT_API_PYQTv1 : partial (import_pyqt4 , version = 1 ),
350+ # default
351+ QT_API_PYQT_DEFAULT : import_pyqt6 ,
352+ }
301353
302354 for api in api_options :
303355
@@ -332,3 +384,15 @@ def load_qt(api_options):
332384 has_binding (QT_API_PYSIDE ),
333385 has_binding (QT_API_PYSIDE2 ),
334386 api_options ))
387+
388+
389+ def enum_factory (QT_API , QtCore ):
390+ """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
391+ @lru_cache (None )
392+ def _enum (name ):
393+ # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
394+ return operator .attrgetter (
395+ name if QT_API == QT_API_PYQT6 else name .rpartition ("." )[0 ]
396+ )(sys .modules [QtCore .__package__ ])
397+
398+ return _enum
0 commit comments