##// END OF EJS Templates
STY: apply darker
Thomas A Caswell -
Show More
@@ -1,119 +1,129 b''
1 """ Import Qt in a manner suitable for an IPython kernel.
1 """ Import Qt in a manner suitable for an IPython kernel.
2
2
3 This is the import used for the `gui=qt` or `matplotlib=qt` initialization.
3 This is the import used for the `gui=qt` or `matplotlib=qt` initialization.
4
4
5 Import Priority:
5 Import Priority:
6
6
7 if Qt has been imported anywhere else:
7 if Qt has been imported anywhere else:
8 use that
8 use that
9
9
10 if matplotlib has been imported and doesn't support v2 (<= 1.0.1):
10 if matplotlib has been imported and doesn't support v2 (<= 1.0.1):
11 use PyQt4 @v1
11 use PyQt4 @v1
12
12
13 Next, ask QT_API env variable
13 Next, ask QT_API env variable
14
14
15 if QT_API not set:
15 if QT_API not set:
16 ask matplotlib what it's using. If Qt4Agg or Qt5Agg, then use the
16 ask matplotlib what it's using. If Qt4Agg or Qt5Agg, then use the
17 version matplotlib is configured with
17 version matplotlib is configured with
18
18
19 else: (matplotlib said nothing)
19 else: (matplotlib said nothing)
20 # this is the default path - nobody told us anything
20 # this is the default path - nobody told us anything
21 try in this order:
21 try in this order:
22 PyQt default version, PySide, PyQt5
22 PyQt default version, PySide, PyQt5
23 else:
23 else:
24 use what QT_API says
24 use what QT_API says
25
25
26 """
26 """
27 # NOTE: This is no longer an external, third-party module, and should be
27 # NOTE: This is no longer an external, third-party module, and should be
28 # considered part of IPython. For compatibility however, it is being kept in
28 # considered part of IPython. For compatibility however, it is being kept in
29 # IPython/external.
29 # IPython/external.
30
30
31 import os
31 import os
32 import sys
32 import sys
33
33
34 from IPython.utils.version import check_version
34 from IPython.utils.version import check_version
35 from IPython.external.qt_loaders import (
35 from IPython.external.qt_loaders import (
36 load_qt, loaded_api, enum_factory,
36 load_qt,
37 loaded_api,
38 enum_factory,
37 # QT6
39 # QT6
38 QT_API_PYQT6, QT_API_PYSIDE6,
40 QT_API_PYQT6,
41 QT_API_PYSIDE6,
39 # QT5
42 # QT5
40 QT_API_PYQT5, QT_API_PYSIDE2,
43 QT_API_PYQT5,
44 QT_API_PYSIDE2,
41 # QT4
45 # QT4
42 QT_API_PYQTv1, QT_API_PYQT, QT_API_PYSIDE,
46 QT_API_PYQTv1,
47 QT_API_PYQT,
48 QT_API_PYSIDE,
43 # default
49 # default
44 QT_API_PYQT_DEFAULT
50 QT_API_PYQT_DEFAULT,
45 )
51 )
46
52
47 _qt_apis = (
53 _qt_apis = (
48 # QT6
54 # QT6
49 QT_API_PYQT6, QT_API_PYSIDE6,
55 QT_API_PYQT6,
56 QT_API_PYSIDE6,
50 # QT5
57 # QT5
51 QT_API_PYQT5, QT_API_PYSIDE2,
58 QT_API_PYQT5,
59 QT_API_PYSIDE2,
52 # QT4
60 # QT4
53 QT_API_PYQTv1, QT_API_PYQT, QT_API_PYSIDE,
61 QT_API_PYQTv1,
62 QT_API_PYQT,
63 QT_API_PYSIDE,
54 # default
64 # default
55 QT_API_PYQT_DEFAULT
65 QT_API_PYQT_DEFAULT,
56 )
66 )
57
67
58
68
59 def matplotlib_options(mpl):
69 def matplotlib_options(mpl):
60 """Constraints placed on an imported matplotlib."""
70 """Constraints placed on an imported matplotlib."""
61 if mpl is None:
71 if mpl is None:
62 return
72 return
63 backend = mpl.rcParams.get('backend', None)
73 backend = mpl.rcParams.get('backend', None)
64 if backend == 'Qt4Agg':
74 if backend == 'Qt4Agg':
65 mpqt = mpl.rcParams.get('backend.qt4', None)
75 mpqt = mpl.rcParams.get('backend.qt4', None)
66 if mpqt is None:
76 if mpqt is None:
67 return None
77 return None
68 if mpqt.lower() == 'pyside':
78 if mpqt.lower() == 'pyside':
69 return [QT_API_PYSIDE]
79 return [QT_API_PYSIDE]
70 elif mpqt.lower() == 'pyqt4':
80 elif mpqt.lower() == 'pyqt4':
71 return [QT_API_PYQT_DEFAULT]
81 return [QT_API_PYQT_DEFAULT]
72 elif mpqt.lower() == 'pyqt4v2':
82 elif mpqt.lower() == 'pyqt4v2':
73 return [QT_API_PYQT]
83 return [QT_API_PYQT]
74 raise ImportError("unhandled value for backend.qt4 from matplotlib: %r" %
84 raise ImportError("unhandled value for backend.qt4 from matplotlib: %r" %
75 mpqt)
85 mpqt)
76 elif backend == 'Qt5Agg':
86 elif backend == 'Qt5Agg':
77 mpqt = mpl.rcParams.get('backend.qt5', None)
87 mpqt = mpl.rcParams.get('backend.qt5', None)
78 if mpqt is None:
88 if mpqt is None:
79 return None
89 return None
80 if mpqt.lower() == 'pyqt5':
90 if mpqt.lower() == 'pyqt5':
81 return [QT_API_PYQT5]
91 return [QT_API_PYQT5]
82 raise ImportError("unhandled value for backend.qt5 from matplotlib: %r" %
92 raise ImportError("unhandled value for backend.qt5 from matplotlib: %r" %
83 mpqt)
93 mpqt)
84
94
85 def get_options():
95 def get_options():
86 """Return a list of acceptable QT APIs, in decreasing order of preference."""
96 """Return a list of acceptable QT APIs, in decreasing order of preference."""
87 #already imported Qt somewhere. Use that
97 #already imported Qt somewhere. Use that
88 loaded = loaded_api()
98 loaded = loaded_api()
89 if loaded is not None:
99 if loaded is not None:
90 return [loaded]
100 return [loaded]
91
101
92 mpl = sys.modules.get('matplotlib', None)
102 mpl = sys.modules.get('matplotlib', None)
93
103
94 if mpl is not None and not check_version(mpl.__version__, '1.0.2'):
104 if mpl is not None and not check_version(mpl.__version__, '1.0.2'):
95 #1.0.1 only supports PyQt4 v1
105 #1.0.1 only supports PyQt4 v1
96 return [QT_API_PYQT_DEFAULT]
106 return [QT_API_PYQT_DEFAULT]
97
107
98 qt_api = os.environ.get('QT_API', None)
108 qt_api = os.environ.get('QT_API', None)
99 if qt_api is None:
109 if qt_api is None:
100 #no ETS variable. Ask mpl, then use default fallback path
110 #no ETS variable. Ask mpl, then use default fallback path
101 return matplotlib_options(mpl) or [
111 return matplotlib_options(mpl) or [
102 QT_API_PYQT_DEFAULT,
112 QT_API_PYQT_DEFAULT,
103 QT_API_PYQT6,
113 QT_API_PYQT6,
104 QT_API_PYSIDE6,
114 QT_API_PYSIDE6,
105 QT_API_PYQT5,
115 QT_API_PYQT5,
106 QT_API_PYSIDE2,
116 QT_API_PYSIDE2,
107 QT_API_PYQT,
117 QT_API_PYQT,
108 QT_API_PYSIDE
118 QT_API_PYSIDE,
109 ]
119 ]
110 elif qt_api not in _qt_apis:
120 elif qt_api not in _qt_apis:
111 raise RuntimeError("Invalid Qt API %r, valid values are: %r" %
121 raise RuntimeError("Invalid Qt API %r, valid values are: %r" %
112 (qt_api, ', '.join(_qt_apis)))
122 (qt_api, ', '.join(_qt_apis)))
113 else:
123 else:
114 return [qt_api]
124 return [qt_api]
115
125
116
126
117 api_opts = get_options()
127 api_opts = get_options()
118 QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts)
128 QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts)
119 enum_helper = enum_factory(QT_API, QtCore)
129 enum_helper = enum_factory(QT_API, QtCore)
@@ -1,398 +1,401 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 sys
11 import sys
12 import types
12 import types
13 from functools import partial, lru_cache
13 from functools import partial, lru_cache
14 import operator
14 import operator
15
15
16 from IPython.utils.version import check_version
16 from IPython.utils.version import check_version
17
17
18 # ### Available APIs.
18 # ### Available APIs.
19 # Qt6
19 # Qt6
20 QT_API_PYQT6 = "pyqt6"
20 QT_API_PYQT6 = "pyqt6"
21 QT_API_PYSIDE6 = "pyside6"
21 QT_API_PYSIDE6 = "pyside6"
22
22
23 # Qt5
23 # Qt5
24 QT_API_PYQT5 = 'pyqt5'
24 QT_API_PYQT5 = 'pyqt5'
25 QT_API_PYSIDE2 = 'pyside2'
25 QT_API_PYSIDE2 = 'pyside2'
26
26
27 # Qt4
27 # Qt4
28 QT_API_PYQT = 'pyqt' # Force version 2
28 QT_API_PYQT = "pyqt" # Force version 2
29 QT_API_PYQTv1 = 'pyqtv1' # Force version 2
29 QT_API_PYQTv1 = "pyqtv1" # Force version 2
30 QT_API_PYSIDE = 'pyside'
30 QT_API_PYSIDE = "pyside"
31
31
32 QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2
32 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
33
33
34 api_to_module = {
34 api_to_module = {
35 # Qt6
35 # Qt6
36 QT_API_PYQT6: "PyQt6",
36 QT_API_PYQT6: "PyQt6",
37 QT_API_PYSIDE6: "PySide6",
37 QT_API_PYSIDE6: "PySide6",
38 # Qt5
38 # Qt5
39 QT_API_PYQT5: 'PyQt5',
39 QT_API_PYQT5: "PyQt5",
40 QT_API_PYSIDE2: 'PySide2',
40 QT_API_PYSIDE2: "PySide2",
41 # Qt4
41 # Qt4
42 QT_API_PYSIDE: 'PySide',
42 QT_API_PYSIDE: "PySide",
43 QT_API_PYQT: 'PyQt4',
43 QT_API_PYQT: "PyQt4",
44 QT_API_PYQTv1: 'PyQt4',
44 QT_API_PYQTv1: "PyQt4",
45 # default
45 # default
46 QT_API_PYQT_DEFAULT: 'PyQt6',
46 QT_API_PYQT_DEFAULT: "PyQt6",
47 }
47 }
48
48
49
49
50 class ImportDenier(object):
50 class ImportDenier(object):
51 """Import Hook that will guard against bad Qt imports
51 """Import Hook that will guard against bad Qt imports
52 once IPython commits to a specific binding
52 once IPython commits to a specific binding
53 """
53 """
54
54
55 def __init__(self):
55 def __init__(self):
56 self.__forbidden = set()
56 self.__forbidden = set()
57
57
58 def forbid(self, module_name):
58 def forbid(self, module_name):
59 sys.modules.pop(module_name, None)
59 sys.modules.pop(module_name, None)
60 self.__forbidden.add(module_name)
60 self.__forbidden.add(module_name)
61
61
62 def find_module(self, fullname, path=None):
62 def find_module(self, fullname, path=None):
63 if path:
63 if path:
64 return
64 return
65 if fullname in self.__forbidden:
65 if fullname in self.__forbidden:
66 return self
66 return self
67
67
68 def load_module(self, fullname):
68 def load_module(self, fullname):
69 raise ImportError("""
69 raise ImportError("""
70 Importing %s disabled by IPython, which has
70 Importing %s disabled by IPython, which has
71 already imported an Incompatible QT Binding: %s
71 already imported an Incompatible QT Binding: %s
72 """ % (fullname, loaded_api()))
72 """ % (fullname, loaded_api()))
73
73
74
74
75 ID = ImportDenier()
75 ID = ImportDenier()
76 sys.meta_path.insert(0, ID)
76 sys.meta_path.insert(0, ID)
77
77
78
78
79 def commit_api(api):
79 def commit_api(api):
80 """Commit to a particular API, and trigger ImportErrors on subsequent
80 """Commit to a particular API, and trigger ImportErrors on subsequent
81 dangerous imports"""
81 dangerous imports"""
82 modules = set(api_to_module.values())
82 modules = set(api_to_module.values())
83
83
84 modules.remove(api_to_module[api])
84 modules.remove(api_to_module[api])
85 for mod in modules:
85 for mod in modules:
86 ID.forbid(mod)
86 ID.forbid(mod)
87
87
88
88
89 def loaded_api():
89 def loaded_api():
90 """Return which API is loaded, if any
90 """Return which API is loaded, if any
91
91
92 If this returns anything besides None,
92 If this returns anything besides None,
93 importing any other Qt binding is unsafe.
93 importing any other Qt binding is unsafe.
94
94
95 Returns
95 Returns
96 -------
96 -------
97 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
97 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
98 """
98 """
99 if sys.modules.get("PyQt6.QtCore"):
99 if sys.modules.get("PyQt6.QtCore"):
100 return QT_API_PYQT6
100 return QT_API_PYQT6
101 elif sys.modules.get("PySide6.QtCore"):
101 elif sys.modules.get("PySide6.QtCore"):
102 return QT_API_PYSIDE6
102 return QT_API_PYSIDE6
103 elif sys.modules.get("PyQt5.QtCore"):
103 elif sys.modules.get("PyQt5.QtCore"):
104 return QT_API_PYQT5
104 return QT_API_PYQT5
105 elif sys.modules.get("PySide2.QtCore"):
105 elif sys.modules.get("PySide2.QtCore"):
106 return QT_API_PYSIDE2
106 return QT_API_PYSIDE2
107 elif sys.modules.get('PyQt4.QtCore'):
107 elif sys.modules.get("PyQt4.QtCore"):
108 if qtapi_version() == 2:
108 if qtapi_version() == 2:
109 return QT_API_PYQT
109 return QT_API_PYQT
110 else:
110 else:
111 return QT_API_PYQTv1
111 return QT_API_PYQTv1
112 elif sys.modules.get('PySide.QtCore'):
112 elif sys.modules.get("PySide.QtCore"):
113 return QT_API_PYSIDE
113 return QT_API_PYSIDE
114
114
115 return None
115 return None
116
116
117
117
118 def has_binding(api):
118 def has_binding(api):
119 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
119 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
120
120
121 Parameters
121 Parameters
122 ----------
122 ----------
123 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
123 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
124 Which module to check for
124 Which module to check for
125
125
126 Returns
126 Returns
127 -------
127 -------
128 True if the relevant module appears to be importable
128 True if the relevant module appears to be importable
129 """
129 """
130 module_name = api_to_module[api]
130 module_name = api_to_module[api]
131 from importlib.util import find_spec
131 from importlib.util import find_spec
132
132
133 required = ['QtCore', 'QtGui', 'QtSvg']
133 required = ['QtCore', 'QtGui', 'QtSvg']
134 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
134 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
135 # QT5 requires QtWidgets too
135 # QT5 requires QtWidgets too
136 required.append('QtWidgets')
136 required.append('QtWidgets')
137
137
138 for submod in required:
138 for submod in required:
139 try:
139 try:
140 spec = find_spec('%s.%s' % (module_name, submod))
140 spec = find_spec('%s.%s' % (module_name, submod))
141 except ImportError:
141 except ImportError:
142 # Package (e.g. PyQt5) not found
142 # Package (e.g. PyQt5) not found
143 return False
143 return False
144 else:
144 else:
145 if spec is None:
145 if spec is None:
146 # Submodule (e.g. PyQt5.QtCore) not found
146 # Submodule (e.g. PyQt5.QtCore) not found
147 return False
147 return False
148
148
149 if api == QT_API_PYSIDE:
149 if api == QT_API_PYSIDE:
150 # We can also safely check PySide version
150 # We can also safely check PySide version
151 import PySide
151 import PySide
152 return check_version(PySide.__version__, '1.0.3')
152 return check_version(PySide.__version__, '1.0.3')
153
153
154 return True
154 return True
155
155
156
156
157 def qtapi_version():
157 def qtapi_version():
158 """Return which QString API has been set, if any
158 """Return which QString API has been set, if any
159
159
160 Returns
160 Returns
161 -------
161 -------
162 The QString API version (1 or 2), or None if not set
162 The QString API version (1 or 2), or None if not set
163 """
163 """
164 try:
164 try:
165 import sip
165 import sip
166 except ImportError:
166 except ImportError:
167 # as of PyQt5 5.11, sip is no longer available as a top-level
167 # 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
168 # module and needs to be imported from the PyQt5 namespace
169 try:
169 try:
170 from PyQt5 import sip
170 from PyQt5 import sip
171 except ImportError:
171 except ImportError:
172 return
172 return
173 try:
173 try:
174 return sip.getapi('QString')
174 return sip.getapi('QString')
175 except ValueError:
175 except ValueError:
176 return
176 return
177
177
178
178
179 def can_import(api):
179 def can_import(api):
180 """Safely query whether an API is importable, without importing it"""
180 """Safely query whether an API is importable, without importing it"""
181 if not has_binding(api):
181 if not has_binding(api):
182 return False
182 return False
183
183
184 current = loaded_api()
184 current = loaded_api()
185 if api == QT_API_PYQT_DEFAULT:
185 if api == QT_API_PYQT_DEFAULT:
186 return current in [QT_API_PYQT6, None]
186 return current in [QT_API_PYQT6, None]
187 else:
187 else:
188 return current in [api, None]
188 return current in [api, None]
189
189
190
190
191 def import_pyqt4(version=2):
191 def import_pyqt4(version=2):
192 """
192 """
193 Import PyQt4
193 Import PyQt4
194
194
195 Parameters
195 Parameters
196 ----------
196 ----------
197 version : 1, 2, or None
197 version : 1, 2, or None
198 Which QString/QVariant API to use. Set to None to use the system
198 Which QString/QVariant API to use. Set to None to use the system
199 default
199 default
200
200
201 ImportErrors rasied within this function are non-recoverable
201 ImportErrors rasied within this function are non-recoverable
202 """
202 """
203 # The new-style string API (version=2) automatically
203 # The new-style string API (version=2) automatically
204 # converts QStrings to Unicode Python strings. Also, automatically unpacks
204 # converts QStrings to Unicode Python strings. Also, automatically unpacks
205 # QVariants to their underlying objects.
205 # QVariants to their underlying objects.
206 import sip
206 import sip
207
207
208 if version is not None:
208 if version is not None:
209 sip.setapi('QString', version)
209 sip.setapi('QString', version)
210 sip.setapi('QVariant', version)
210 sip.setapi('QVariant', version)
211
211
212 from PyQt4 import QtGui, QtCore, QtSvg
212 from PyQt4 import QtGui, QtCore, QtSvg
213
213
214 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
214 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
215 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
215 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
216 QtCore.PYQT_VERSION_STR)
216 QtCore.PYQT_VERSION_STR)
217
217
218 # Alias PyQt-specific functions for PySide compatibility.
218 # Alias PyQt-specific functions for PySide compatibility.
219 QtCore.Signal = QtCore.pyqtSignal
219 QtCore.Signal = QtCore.pyqtSignal
220 QtCore.Slot = QtCore.pyqtSlot
220 QtCore.Slot = QtCore.pyqtSlot
221
221
222 # query for the API version (in case version == None)
222 # query for the API version (in case version == None)
223 version = sip.getapi('QString')
223 version = sip.getapi('QString')
224 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
224 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
225 return QtCore, QtGui, QtSvg, api
225 return QtCore, QtGui, QtSvg, api
226
226
227
227
228 def import_pyqt5():
228 def import_pyqt5():
229 """
229 """
230 Import PyQt5
230 Import PyQt5
231
231
232 ImportErrors rasied within this function are non-recoverable
232 ImportErrors rasied within this function are non-recoverable
233 """
233 """
234
234
235 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
235 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
236
236
237 # Alias PyQt-specific functions for PySide compatibility.
237 # Alias PyQt-specific functions for PySide compatibility.
238 QtCore.Signal = QtCore.pyqtSignal
238 QtCore.Signal = QtCore.pyqtSignal
239 QtCore.Slot = QtCore.pyqtSlot
239 QtCore.Slot = QtCore.pyqtSlot
240
240
241 # Join QtGui and QtWidgets for Qt4 compatibility.
241 # Join QtGui and QtWidgets for Qt4 compatibility.
242 QtGuiCompat = types.ModuleType('QtGuiCompat')
242 QtGuiCompat = types.ModuleType('QtGuiCompat')
243 QtGuiCompat.__dict__.update(QtGui.__dict__)
243 QtGuiCompat.__dict__.update(QtGui.__dict__)
244 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
244 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
245
245
246 api = QT_API_PYQT5
246 api = QT_API_PYQT5
247 return QtCore, QtGuiCompat, QtSvg, api
247 return QtCore, QtGuiCompat, QtSvg, api
248
248
249
249 def import_pyqt6():
250 def import_pyqt6():
250 """
251 """
251 Import PyQt6
252 Import PyQt6
252
253
253 ImportErrors rasied within this function are non-recoverable
254 ImportErrors rasied within this function are non-recoverable
254 """
255 """
255
256
256 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
257 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
257
258
258 # Alias PyQt-specific functions for PySide compatibility.
259 # Alias PyQt-specific functions for PySide compatibility.
259 QtCore.Signal = QtCore.pyqtSignal
260 QtCore.Signal = QtCore.pyqtSignal
260 QtCore.Slot = QtCore.pyqtSlot
261 QtCore.Slot = QtCore.pyqtSlot
261
262
262 # Join QtGui and QtWidgets for Qt4 compatibility.
263 # Join QtGui and QtWidgets for Qt4 compatibility.
263 QtGuiCompat = types.ModuleType('QtGuiCompat')
264 QtGuiCompat = types.ModuleType("QtGuiCompat")
264 QtGuiCompat.__dict__.update(QtGui.__dict__)
265 QtGuiCompat.__dict__.update(QtGui.__dict__)
265 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
266 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
266
267
267 api = QT_API_PYQT6
268 api = QT_API_PYQT6
268 return QtCore, QtGuiCompat, QtSvg, api
269 return QtCore, QtGuiCompat, QtSvg, api
269
270
270
271
271 def import_pyside():
272 def import_pyside():
272 """
273 """
273 Import PySide
274 Import PySide
274
275
275 ImportErrors raised within this function are non-recoverable
276 ImportErrors raised within this function are non-recoverable
276 """
277 """
277 from PySide import QtGui, QtCore, QtSvg
278 from PySide import QtGui, QtCore, QtSvg
278 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
279 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
279
280
280 def import_pyside2():
281 def import_pyside2():
281 """
282 """
282 Import PySide2
283 Import PySide2
283
284
284 ImportErrors raised within this function are non-recoverable
285 ImportErrors raised within this function are non-recoverable
285 """
286 """
286 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
287 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
287
288
288 # Join QtGui and QtWidgets for Qt4 compatibility.
289 # Join QtGui and QtWidgets for Qt4 compatibility.
289 QtGuiCompat = types.ModuleType('QtGuiCompat')
290 QtGuiCompat = types.ModuleType('QtGuiCompat')
290 QtGuiCompat.__dict__.update(QtGui.__dict__)
291 QtGuiCompat.__dict__.update(QtGui.__dict__)
291 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
292 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
292 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
293 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
293
294
294 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
295 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
295
296
297
296 def import_pyside6():
298 def import_pyside6():
297 """
299 """
298 Import PySide6
300 Import PySide6
299
301
300 ImportErrors raised within this function are non-recoverable
302 ImportErrors raised within this function are non-recoverable
301 """
303 """
302 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
304 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
303
305
304 # Join QtGui and QtWidgets for Qt4 compatibility.
306 # Join QtGui and QtWidgets for Qt4 compatibility.
305 QtGuiCompat = types.ModuleType('QtGuiCompat')
307 QtGuiCompat = types.ModuleType("QtGuiCompat")
306 QtGuiCompat.__dict__.update(QtGui.__dict__)
308 QtGuiCompat.__dict__.update(QtGui.__dict__)
307 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
309 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
308 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
310 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
309
311
310 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
312 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
311
313
312
314
313 def load_qt(api_options):
315 def load_qt(api_options):
314 """
316 """
315 Attempt to import Qt, given a preference list
317 Attempt to import Qt, given a preference list
316 of permissible bindings
318 of permissible bindings
317
319
318 It is safe to call this function multiple times.
320 It is safe to call this function multiple times.
319
321
320 Parameters
322 Parameters
321 ----------
323 ----------
322 api_options: List of strings
324 api_options: List of strings
323 The order of APIs to try. Valid items are 'pyside', 'pyside2',
325 The order of APIs to try. Valid items are 'pyside', 'pyside2',
324 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
326 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
325
327
326 Returns
328 Returns
327 -------
329 -------
328
330
329 A tuple of QtCore, QtGui, QtSvg, QT_API
331 A tuple of QtCore, QtGui, QtSvg, QT_API
330 The first three are the Qt modules. The last is the
332 The first three are the Qt modules. The last is the
331 string indicating which module was loaded.
333 string indicating which module was loaded.
332
334
333 Raises
335 Raises
334 ------
336 ------
335 ImportError, if it isn't possible to import any requested
337 ImportError, if it isn't possible to import any requested
336 bindings (either because they aren't installed, or because
338 bindings (either because they aren't installed, or because
337 an incompatible library has already been installed)
339 an incompatible library has already been installed)
338 """
340 """
339 loaders = {
341 loaders = {
340 # Qt6
342 # Qt6
341 QT_API_PYQT6: import_pyqt6,
343 QT_API_PYQT6: import_pyqt6,
342 QT_API_PYSIDE6: import_pyside6,
344 QT_API_PYSIDE6: import_pyside6,
343 # Qt5
345 # Qt5
344 QT_API_PYQT5: import_pyqt5,
346 QT_API_PYQT5: import_pyqt5,
345 QT_API_PYSIDE2: import_pyside2,
347 QT_API_PYSIDE2: import_pyside2,
346 # Qt4
348 # Qt4
347 QT_API_PYSIDE: import_pyside,
349 QT_API_PYSIDE: import_pyside,
348 QT_API_PYQT: import_pyqt4,
350 QT_API_PYQT: import_pyqt4,
349 QT_API_PYQTv1: partial(import_pyqt4, version=1),
351 QT_API_PYQTv1: partial(import_pyqt4, version=1),
350 # default
352 # default
351 QT_API_PYQT_DEFAULT: import_pyqt6,
353 QT_API_PYQT_DEFAULT: import_pyqt6,
352 }
354 }
353
355
354 for api in api_options:
356 for api in api_options:
355
357
356 if api not in loaders:
358 if api not in loaders:
357 raise RuntimeError(
359 raise RuntimeError(
358 "Invalid Qt API %r, valid values are: %s" %
360 "Invalid Qt API %r, valid values are: %s" %
359 (api, ", ".join(["%r" % k for k in loaders.keys()])))
361 (api, ", ".join(["%r" % k for k in loaders.keys()])))
360
362
361 if not can_import(api):
363 if not can_import(api):
362 continue
364 continue
363
365
364 #cannot safely recover from an ImportError during this
366 #cannot safely recover from an ImportError during this
365 result = loaders[api]()
367 result = loaders[api]()
366 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
368 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
367 commit_api(api)
369 commit_api(api)
368 return result
370 return result
369 else:
371 else:
370 raise ImportError("""
372 raise ImportError("""
371 Could not load requested Qt binding. Please ensure that
373 Could not load requested Qt binding. Please ensure that
372 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
374 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
373 and only one is imported per session.
375 and only one is imported per session.
374
376
375 Currently-imported Qt library: %r
377 Currently-imported Qt library: %r
376 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
378 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
377 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
379 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
378 PySide >= 1.0.3 installed: %s
380 PySide >= 1.0.3 installed: %s
379 PySide2 installed: %s
381 PySide2 installed: %s
380 Tried to load: %r
382 Tried to load: %r
381 """ % (loaded_api(),
383 """ % (loaded_api(),
382 has_binding(QT_API_PYQT),
384 has_binding(QT_API_PYQT),
383 has_binding(QT_API_PYQT5),
385 has_binding(QT_API_PYQT5),
384 has_binding(QT_API_PYSIDE),
386 has_binding(QT_API_PYSIDE),
385 has_binding(QT_API_PYSIDE2),
387 has_binding(QT_API_PYSIDE2),
386 api_options))
388 api_options))
387
389
388
390
389 def enum_factory(QT_API, QtCore):
391 def enum_factory(QT_API, QtCore):
390 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
392 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
393
391 @lru_cache(None)
394 @lru_cache(None)
392 def _enum(name):
395 def _enum(name):
393 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
396 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
394 return operator.attrgetter(
397 return operator.attrgetter(
395 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
398 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
396 )(sys.modules[QtCore.__package__])
399 )(sys.modules[QtCore.__package__])
397
400
398 return _enum
401 return _enum
@@ -1,55 +1,61 b''
1 import importlib
1 import importlib
2 import os
2 import os
3
3
4 aliases = {
4 aliases = {
5 'qt4': 'qt',
5 'qt4': 'qt',
6 'gtk2': 'gtk',
6 'gtk2': 'gtk',
7 }
7 }
8
8
9 backends = [
9 backends = [
10 'qt', 'qt4', 'qt5', 'qt6',
10 "qt",
11 'gtk', 'gtk2', 'gtk3',
11 "qt4",
12 'tk',
12 "qt5",
13 'wx',
13 "qt6",
14 'pyglet', 'glut',
14 "gtk",
15 'osx',
15 "gtk2",
16 'asyncio'
16 "gtk3",
17 "tk",
18 "wx",
19 "pyglet",
20 "glut",
21 "osx",
22 "asyncio",
17 ]
23 ]
18
24
19 registered = {}
25 registered = {}
20
26
21 def register(name, inputhook):
27 def register(name, inputhook):
22 """Register the function *inputhook* as an event loop integration."""
28 """Register the function *inputhook* as an event loop integration."""
23 registered[name] = inputhook
29 registered[name] = inputhook
24
30
25
31
26 class UnknownBackend(KeyError):
32 class UnknownBackend(KeyError):
27 def __init__(self, name):
33 def __init__(self, name):
28 self.name = name
34 self.name = name
29
35
30 def __str__(self):
36 def __str__(self):
31 return ("No event loop integration for {!r}. "
37 return ("No event loop integration for {!r}. "
32 "Supported event loops are: {}").format(self.name,
38 "Supported event loops are: {}").format(self.name,
33 ', '.join(backends + sorted(registered)))
39 ', '.join(backends + sorted(registered)))
34
40
35
41
36 def get_inputhook_name_and_func(gui):
42 def get_inputhook_name_and_func(gui):
37 if gui in registered:
43 if gui in registered:
38 return gui, registered[gui]
44 return gui, registered[gui]
39
45
40 if gui not in backends:
46 if gui not in backends:
41 raise UnknownBackend(gui)
47 raise UnknownBackend(gui)
42
48
43 if gui in aliases:
49 if gui in aliases:
44 return get_inputhook_name_and_func(aliases[gui])
50 return get_inputhook_name_and_func(aliases[gui])
45
51
46 gui_mod = gui
52 gui_mod = gui
47 if gui == 'qt5':
53 if gui == "qt5":
48 os.environ['QT_API'] = 'pyqt5'
54 os.environ["QT_API"] = "pyqt5"
49 gui_mod = 'qt'
55 gui_mod = "qt"
50 elif gui == 'qt6':
56 elif gui == "qt6":
51 os.environ['QT_API'] = 'pyqt6'
57 os.environ["QT_API"] = "pyqt6"
52 gui_mod = 'qt'
58 gui_mod = "qt"
53
59
54 mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod)
60 mod = importlib.import_module('IPython.terminal.pt_inputhooks.'+gui_mod)
55 return gui, mod.inputhook
61 return gui, mod.inputhook
@@ -1,86 +1,85 b''
1 import sys
1 import sys
2 import os
2 import os
3 from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper
3 from IPython.external.qt_for_kernel import QtCore, QtGui, enum_helper
4 from IPython import get_ipython
4 from IPython import get_ipython
5
5
6 # If we create a QApplication, keep a reference to it so that it doesn't get
6 # If we create a QApplication, keep a reference to it so that it doesn't get
7 # garbage collected.
7 # garbage collected.
8 _appref = None
8 _appref = None
9 _already_warned = False
9 _already_warned = False
10
10
11
11
12 def _exec(obj):
12 def _exec(obj):
13 # exec on PyQt6, exec_ elsewhere.
13 # exec on PyQt6, exec_ elsewhere.
14 obj.exec() if hasattr(obj, "exec") else obj.exec_()
14 obj.exec() if hasattr(obj, "exec") else obj.exec_()
15
15
16
16
17 def _reclaim_excepthook():
17 def _reclaim_excepthook():
18 shell = get_ipython()
18 shell = get_ipython()
19 if shell is not None:
19 if shell is not None:
20 sys.excepthook = shell.excepthook
20 sys.excepthook = shell.excepthook
21
21
22
22
23 def inputhook(context):
23 def inputhook(context):
24 global _appref
24 global _appref
25 app = QtCore.QCoreApplication.instance()
25 app = QtCore.QCoreApplication.instance()
26 if not app:
26 if not app:
27 if sys.platform == 'linux':
27 if sys.platform == 'linux':
28 if not os.environ.get('DISPLAY') \
28 if not os.environ.get('DISPLAY') \
29 and not os.environ.get('WAYLAND_DISPLAY'):
29 and not os.environ.get('WAYLAND_DISPLAY'):
30 import warnings
30 import warnings
31 global _already_warned
31 global _already_warned
32 if not _already_warned:
32 if not _already_warned:
33 _already_warned = True
33 _already_warned = True
34 warnings.warn(
34 warnings.warn(
35 'The DISPLAY or WAYLAND_DISPLAY environment variable is '
35 'The DISPLAY or WAYLAND_DISPLAY environment variable is '
36 'not set or empty and Qt5 requires this environment '
36 'not set or empty and Qt5 requires this environment '
37 'variable. Deactivate Qt5 code.'
37 'variable. Deactivate Qt5 code.'
38 )
38 )
39 return
39 return
40 try:
40 try:
41 QtCore.QApplication.setAttribute(
41 QtCore.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling)
42 QtCore.Qt.AA_EnableHighDpiScaling)
43 except AttributeError: # Only for Qt>=5.6, <6.
42 except AttributeError: # Only for Qt>=5.6, <6.
44 pass
43 pass
45 try:
44 try:
46 QtCore.QApplication.setHighDpiScaleFactorRoundingPolicy(
45 QtCore.QApplication.setHighDpiScaleFactorRoundingPolicy(
47 QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
46 QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
47 )
48 except AttributeError: # Only for Qt>=5.14.
48 except AttributeError: # Only for Qt>=5.14.
49 pass
49 pass
50 _appref = app = QtGui.QApplication([" "])
50 _appref = app = QtGui.QApplication([" "])
51
51
52 # "reclaim" IPython sys.excepthook after event loop starts
52 # "reclaim" IPython sys.excepthook after event loop starts
53 # without this, it defaults back to BaseIPythonApplication.excepthook
53 # without this, it defaults back to BaseIPythonApplication.excepthook
54 # and exceptions in the Qt event loop are rendered without traceback
54 # and exceptions in the Qt event loop are rendered without traceback
55 # formatting and look like "bug in IPython".
55 # formatting and look like "bug in IPython".
56 QtCore.QTimer.singleShot(0, _reclaim_excepthook)
56 QtCore.QTimer.singleShot(0, _reclaim_excepthook)
57
57
58 event_loop = QtCore.QEventLoop(app)
58 event_loop = QtCore.QEventLoop(app)
59
59
60 if sys.platform == 'win32':
60 if sys.platform == 'win32':
61 # The QSocketNotifier method doesn't appear to work on Windows.
61 # The QSocketNotifier method doesn't appear to work on Windows.
62 # Use polling instead.
62 # Use polling instead.
63 timer = QtCore.QTimer()
63 timer = QtCore.QTimer()
64 timer.timeout.connect(event_loop.quit)
64 timer.timeout.connect(event_loop.quit)
65 while not context.input_is_ready():
65 while not context.input_is_ready():
66 timer.start(50) # 50 ms
66 timer.start(50) # 50 ms
67 event_loop.exec_()
67 event_loop.exec_()
68 timer.stop()
68 timer.stop()
69 else:
69 else:
70 # On POSIX platforms, we can use a file descriptor to quit the event
70 # On POSIX platforms, we can use a file descriptor to quit the event
71 # loop when there is input ready to read.
71 # loop when there is input ready to read.
72 notifier = QtCore.QSocketNotifier(
72 notifier = QtCore.QSocketNotifier(
73 context.fileno(),
73 context.fileno(), enum_helper("QtCore.QSocketNotifier.Type").Read
74 enum_helper('QtCore.QSocketNotifier.Type').Read
75 )
74 )
76 try:
75 try:
77 # connect the callback we care about before we turn it on
76 # connect the callback we care about before we turn it on
78 # lambda is necessary as PyQT inspect the function signature to know
77 # lambda is necessary as PyQT inspect the function signature to know
79 # what arguments to pass to. See https://github.com/ipython/ipython/pull/12355
78 # what arguments to pass to. See https://github.com/ipython/ipython/pull/12355
80 notifier.activated.connect(lambda: event_loop.exit())
79 notifier.activated.connect(lambda: event_loop.exit())
81 notifier.setEnabled(True)
80 notifier.setEnabled(True)
82 # only start the event loop we are not already flipped
81 # only start the event loop we are not already flipped
83 if not context.input_is_ready():
82 if not context.input_is_ready():
84 _exec(event_loop)
83 _exec(event_loop)
85 finally:
84 finally:
86 notifier.setEnabled(False)
85 notifier.setEnabled(False)
General Comments 0
You need to be logged in to leave comments. Login now