##// END OF EJS Templates
Deprecate `IPython.utils.version`...
Nikita Kniazev -
Show More
@@ -1,129 +1,128 b''
1 1 """ Import Qt in a manner suitable for an IPython kernel.
2 2
3 3 This is the import used for the `gui=qt` or `matplotlib=qt` initialization.
4 4
5 5 Import Priority:
6 6
7 7 if Qt has been imported anywhere else:
8 8 use that
9 9
10 10 if matplotlib has been imported and doesn't support v2 (<= 1.0.1):
11 11 use PyQt4 @v1
12 12
13 13 Next, ask QT_API env variable
14 14
15 15 if QT_API not set:
16 16 ask matplotlib what it's using. If Qt4Agg or Qt5Agg, then use the
17 17 version matplotlib is configured with
18 18
19 19 else: (matplotlib said nothing)
20 20 # this is the default path - nobody told us anything
21 21 try in this order:
22 22 PyQt default version, PySide, PyQt5
23 23 else:
24 24 use what QT_API says
25 25
26 26 """
27 27 # NOTE: This is no longer an external, third-party module, and should be
28 28 # considered part of IPython. For compatibility however, it is being kept in
29 29 # IPython/external.
30 30
31 31 import os
32 32 import sys
33 33
34 from IPython.utils.version import check_version
35 34 from IPython.external.qt_loaders import (
36 35 load_qt,
37 36 loaded_api,
38 37 enum_factory,
39 38 # QT6
40 39 QT_API_PYQT6,
41 40 QT_API_PYSIDE6,
42 41 # QT5
43 42 QT_API_PYQT5,
44 43 QT_API_PYSIDE2,
45 44 # QT4
46 45 QT_API_PYQTv1,
47 46 QT_API_PYQT,
48 47 QT_API_PYSIDE,
49 48 # default
50 49 QT_API_PYQT_DEFAULT,
51 50 )
52 51
53 52 _qt_apis = (
54 53 # QT6
55 54 QT_API_PYQT6,
56 55 QT_API_PYSIDE6,
57 56 # QT5
58 57 QT_API_PYQT5,
59 58 QT_API_PYSIDE2,
60 59 # QT4
61 60 QT_API_PYQTv1,
62 61 QT_API_PYQT,
63 62 QT_API_PYSIDE,
64 63 # default
65 64 QT_API_PYQT_DEFAULT,
66 65 )
67 66
68 67
69 68 def matplotlib_options(mpl):
70 69 """Constraints placed on an imported matplotlib."""
71 70 if mpl is None:
72 71 return
73 72 backend = mpl.rcParams.get('backend', None)
74 73 if backend == 'Qt4Agg':
75 74 mpqt = mpl.rcParams.get('backend.qt4', None)
76 75 if mpqt is None:
77 76 return None
78 77 if mpqt.lower() == 'pyside':
79 78 return [QT_API_PYSIDE]
80 79 elif mpqt.lower() == 'pyqt4':
81 80 return [QT_API_PYQT_DEFAULT]
82 81 elif mpqt.lower() == 'pyqt4v2':
83 82 return [QT_API_PYQT]
84 83 raise ImportError("unhandled value for backend.qt4 from matplotlib: %r" %
85 84 mpqt)
86 85 elif backend == 'Qt5Agg':
87 86 mpqt = mpl.rcParams.get('backend.qt5', None)
88 87 if mpqt is None:
89 88 return None
90 89 if mpqt.lower() == 'pyqt5':
91 90 return [QT_API_PYQT5]
92 91 raise ImportError("unhandled value for backend.qt5 from matplotlib: %r" %
93 92 mpqt)
94 93
95 94 def get_options():
96 95 """Return a list of acceptable QT APIs, in decreasing order of preference."""
97 96 #already imported Qt somewhere. Use that
98 97 loaded = loaded_api()
99 98 if loaded is not None:
100 99 return [loaded]
101 100
102 101 mpl = sys.modules.get('matplotlib', None)
103 102
104 if mpl is not None and not check_version(mpl.__version__, '1.0.2'):
105 #1.0.1 only supports PyQt4 v1
103 if mpl is not None and tuple(mpl.__version__.split(".")) < ("1", "0", "2"):
104 # 1.0.1 only supports PyQt4 v1
106 105 return [QT_API_PYQT_DEFAULT]
107 106
108 107 qt_api = os.environ.get('QT_API', None)
109 108 if qt_api is None:
110 109 #no ETS variable. Ask mpl, then use default fallback path
111 110 return matplotlib_options(mpl) or [
112 111 QT_API_PYQT_DEFAULT,
113 112 QT_API_PYQT6,
114 113 QT_API_PYSIDE6,
115 114 QT_API_PYQT5,
116 115 QT_API_PYSIDE2,
117 116 QT_API_PYQT,
118 117 QT_API_PYSIDE,
119 118 ]
120 119 elif qt_api not in _qt_apis:
121 120 raise RuntimeError("Invalid Qt API %r, valid values are: %r" %
122 121 (qt_api, ', '.join(_qt_apis)))
123 122 else:
124 123 return [qt_api]
125 124
126 125
127 126 api_opts = get_options()
128 127 QtCore, QtGui, QtSvg, QT_API = load_qt(api_opts)
129 128 enum_helper = enum_factory(QT_API, QtCore)
@@ -1,401 +1,400 b''
1 1 """
2 2 This module contains factory functions that attempt
3 3 to return Qt submodules from the various python Qt bindings.
4 4
5 5 It also protects against double-importing Qt with different
6 6 bindings, which is unstable and likely to crash
7 7
8 8 This is used primarily by qt and qt_for_kernel, and shouldn't
9 9 be accessed directly from the outside
10 10 """
11 11 import sys
12 12 import types
13 13 from functools import partial, lru_cache
14 14 import operator
15 15
16 from IPython.utils.version import check_version
17
18 16 # ### Available APIs.
19 17 # Qt6
20 18 QT_API_PYQT6 = "pyqt6"
21 19 QT_API_PYSIDE6 = "pyside6"
22 20
23 21 # Qt5
24 22 QT_API_PYQT5 = 'pyqt5'
25 23 QT_API_PYSIDE2 = 'pyside2'
26 24
27 25 # Qt4
28 26 QT_API_PYQT = "pyqt" # Force version 2
29 27 QT_API_PYQTv1 = "pyqtv1" # Force version 2
30 28 QT_API_PYSIDE = "pyside"
31 29
32 30 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
33 31
34 32 api_to_module = {
35 33 # Qt6
36 34 QT_API_PYQT6: "PyQt6",
37 35 QT_API_PYSIDE6: "PySide6",
38 36 # Qt5
39 37 QT_API_PYQT5: "PyQt5",
40 38 QT_API_PYSIDE2: "PySide2",
41 39 # Qt4
42 40 QT_API_PYSIDE: "PySide",
43 41 QT_API_PYQT: "PyQt4",
44 42 QT_API_PYQTv1: "PyQt4",
45 43 # default
46 44 QT_API_PYQT_DEFAULT: "PyQt6",
47 45 }
48 46
49 47
50 48 class ImportDenier(object):
51 49 """Import Hook that will guard against bad Qt imports
52 50 once IPython commits to a specific binding
53 51 """
54 52
55 53 def __init__(self):
56 54 self.__forbidden = set()
57 55
58 56 def forbid(self, module_name):
59 57 sys.modules.pop(module_name, None)
60 58 self.__forbidden.add(module_name)
61 59
62 60 def find_module(self, fullname, path=None):
63 61 if path:
64 62 return
65 63 if fullname in self.__forbidden:
66 64 return self
67 65
68 66 def load_module(self, fullname):
69 67 raise ImportError("""
70 68 Importing %s disabled by IPython, which has
71 69 already imported an Incompatible QT Binding: %s
72 70 """ % (fullname, loaded_api()))
73 71
74 72
75 73 ID = ImportDenier()
76 74 sys.meta_path.insert(0, ID)
77 75
78 76
79 77 def commit_api(api):
80 78 """Commit to a particular API, and trigger ImportErrors on subsequent
81 79 dangerous imports"""
82 80 modules = set(api_to_module.values())
83 81
84 82 modules.remove(api_to_module[api])
85 83 for mod in modules:
86 84 ID.forbid(mod)
87 85
88 86
89 87 def loaded_api():
90 88 """Return which API is loaded, if any
91 89
92 90 If this returns anything besides None,
93 91 importing any other Qt binding is unsafe.
94 92
95 93 Returns
96 94 -------
97 95 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
98 96 """
99 97 if sys.modules.get("PyQt6.QtCore"):
100 98 return QT_API_PYQT6
101 99 elif sys.modules.get("PySide6.QtCore"):
102 100 return QT_API_PYSIDE6
103 101 elif sys.modules.get("PyQt5.QtCore"):
104 102 return QT_API_PYQT5
105 103 elif sys.modules.get("PySide2.QtCore"):
106 104 return QT_API_PYSIDE2
107 105 elif sys.modules.get("PyQt4.QtCore"):
108 106 if qtapi_version() == 2:
109 107 return QT_API_PYQT
110 108 else:
111 109 return QT_API_PYQTv1
112 110 elif sys.modules.get("PySide.QtCore"):
113 111 return QT_API_PYSIDE
114 112
115 113 return None
116 114
117 115
118 116 def has_binding(api):
119 117 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
120 118
121 119 Parameters
122 120 ----------
123 121 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
124 122 Which module to check for
125 123
126 124 Returns
127 125 -------
128 126 True if the relevant module appears to be importable
129 127 """
130 128 module_name = api_to_module[api]
131 129 from importlib.util import find_spec
132 130
133 131 required = ['QtCore', 'QtGui', 'QtSvg']
134 132 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
135 133 # QT5 requires QtWidgets too
136 134 required.append('QtWidgets')
137 135
138 136 for submod in required:
139 137 try:
140 138 spec = find_spec('%s.%s' % (module_name, submod))
141 139 except ImportError:
142 140 # Package (e.g. PyQt5) not found
143 141 return False
144 142 else:
145 143 if spec is None:
146 144 # Submodule (e.g. PyQt5.QtCore) not found
147 145 return False
148 146
149 147 if api == QT_API_PYSIDE:
150 148 # We can also safely check PySide version
151 149 import PySide
152 return check_version(PySide.__version__, '1.0.3')
150
151 return PySide.__version_info__ >= (1, 0, 3)
153 152
154 153 return True
155 154
156 155
157 156 def qtapi_version():
158 157 """Return which QString API has been set, if any
159 158
160 159 Returns
161 160 -------
162 161 The QString API version (1 or 2), or None if not set
163 162 """
164 163 try:
165 164 import sip
166 165 except ImportError:
167 166 # as of PyQt5 5.11, sip is no longer available as a top-level
168 167 # module and needs to be imported from the PyQt5 namespace
169 168 try:
170 169 from PyQt5 import sip
171 170 except ImportError:
172 171 return
173 172 try:
174 173 return sip.getapi('QString')
175 174 except ValueError:
176 175 return
177 176
178 177
179 178 def can_import(api):
180 179 """Safely query whether an API is importable, without importing it"""
181 180 if not has_binding(api):
182 181 return False
183 182
184 183 current = loaded_api()
185 184 if api == QT_API_PYQT_DEFAULT:
186 185 return current in [QT_API_PYQT6, None]
187 186 else:
188 187 return current in [api, None]
189 188
190 189
191 190 def import_pyqt4(version=2):
192 191 """
193 192 Import PyQt4
194 193
195 194 Parameters
196 195 ----------
197 196 version : 1, 2, or None
198 197 Which QString/QVariant API to use. Set to None to use the system
199 198 default
200 199
201 200 ImportErrors raised within this function are non-recoverable
202 201 """
203 202 # The new-style string API (version=2) automatically
204 203 # converts QStrings to Unicode Python strings. Also, automatically unpacks
205 204 # QVariants to their underlying objects.
206 205 import sip
207 206
208 207 if version is not None:
209 208 sip.setapi('QString', version)
210 209 sip.setapi('QVariant', version)
211 210
212 211 from PyQt4 import QtGui, QtCore, QtSvg
213 212
214 if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
213 if QtCore.PYQT_VERSION < 0x040700:
215 214 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
216 215 QtCore.PYQT_VERSION_STR)
217 216
218 217 # Alias PyQt-specific functions for PySide compatibility.
219 218 QtCore.Signal = QtCore.pyqtSignal
220 219 QtCore.Slot = QtCore.pyqtSlot
221 220
222 221 # query for the API version (in case version == None)
223 222 version = sip.getapi('QString')
224 223 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
225 224 return QtCore, QtGui, QtSvg, api
226 225
227 226
228 227 def import_pyqt5():
229 228 """
230 229 Import PyQt5
231 230
232 231 ImportErrors raised within this function are non-recoverable
233 232 """
234 233
235 234 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
236 235
237 236 # Alias PyQt-specific functions for PySide compatibility.
238 237 QtCore.Signal = QtCore.pyqtSignal
239 238 QtCore.Slot = QtCore.pyqtSlot
240 239
241 240 # Join QtGui and QtWidgets for Qt4 compatibility.
242 241 QtGuiCompat = types.ModuleType('QtGuiCompat')
243 242 QtGuiCompat.__dict__.update(QtGui.__dict__)
244 243 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
245 244
246 245 api = QT_API_PYQT5
247 246 return QtCore, QtGuiCompat, QtSvg, api
248 247
249 248
250 249 def import_pyqt6():
251 250 """
252 251 Import PyQt6
253 252
254 253 ImportErrors raised within this function are non-recoverable
255 254 """
256 255
257 256 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
258 257
259 258 # Alias PyQt-specific functions for PySide compatibility.
260 259 QtCore.Signal = QtCore.pyqtSignal
261 260 QtCore.Slot = QtCore.pyqtSlot
262 261
263 262 # Join QtGui and QtWidgets for Qt4 compatibility.
264 263 QtGuiCompat = types.ModuleType("QtGuiCompat")
265 264 QtGuiCompat.__dict__.update(QtGui.__dict__)
266 265 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
267 266
268 267 api = QT_API_PYQT6
269 268 return QtCore, QtGuiCompat, QtSvg, api
270 269
271 270
272 271 def import_pyside():
273 272 """
274 273 Import PySide
275 274
276 275 ImportErrors raised within this function are non-recoverable
277 276 """
278 277 from PySide import QtGui, QtCore, QtSvg
279 278 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
280 279
281 280 def import_pyside2():
282 281 """
283 282 Import PySide2
284 283
285 284 ImportErrors raised within this function are non-recoverable
286 285 """
287 286 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
288 287
289 288 # Join QtGui and QtWidgets for Qt4 compatibility.
290 289 QtGuiCompat = types.ModuleType('QtGuiCompat')
291 290 QtGuiCompat.__dict__.update(QtGui.__dict__)
292 291 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
293 292 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
294 293
295 294 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
296 295
297 296
298 297 def import_pyside6():
299 298 """
300 299 Import PySide6
301 300
302 301 ImportErrors raised within this function are non-recoverable
303 302 """
304 303 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
305 304
306 305 # Join QtGui and QtWidgets for Qt4 compatibility.
307 306 QtGuiCompat = types.ModuleType("QtGuiCompat")
308 307 QtGuiCompat.__dict__.update(QtGui.__dict__)
309 308 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
310 309 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
311 310
312 311 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
313 312
314 313
315 314 def load_qt(api_options):
316 315 """
317 316 Attempt to import Qt, given a preference list
318 317 of permissible bindings
319 318
320 319 It is safe to call this function multiple times.
321 320
322 321 Parameters
323 322 ----------
324 323 api_options: List of strings
325 324 The order of APIs to try. Valid items are 'pyside', 'pyside2',
326 325 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
327 326
328 327 Returns
329 328 -------
330 329
331 330 A tuple of QtCore, QtGui, QtSvg, QT_API
332 331 The first three are the Qt modules. The last is the
333 332 string indicating which module was loaded.
334 333
335 334 Raises
336 335 ------
337 336 ImportError, if it isn't possible to import any requested
338 337 bindings (either because they aren't installed, or because
339 338 an incompatible library has already been installed)
340 339 """
341 340 loaders = {
342 341 # Qt6
343 342 QT_API_PYQT6: import_pyqt6,
344 343 QT_API_PYSIDE6: import_pyside6,
345 344 # Qt5
346 345 QT_API_PYQT5: import_pyqt5,
347 346 QT_API_PYSIDE2: import_pyside2,
348 347 # Qt4
349 348 QT_API_PYSIDE: import_pyside,
350 349 QT_API_PYQT: import_pyqt4,
351 350 QT_API_PYQTv1: partial(import_pyqt4, version=1),
352 351 # default
353 352 QT_API_PYQT_DEFAULT: import_pyqt6,
354 353 }
355 354
356 355 for api in api_options:
357 356
358 357 if api not in loaders:
359 358 raise RuntimeError(
360 359 "Invalid Qt API %r, valid values are: %s" %
361 360 (api, ", ".join(["%r" % k for k in loaders.keys()])))
362 361
363 362 if not can_import(api):
364 363 continue
365 364
366 365 #cannot safely recover from an ImportError during this
367 366 result = loaders[api]()
368 367 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
369 368 commit_api(api)
370 369 return result
371 370 else:
372 371 raise ImportError("""
373 372 Could not load requested Qt binding. Please ensure that
374 373 PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
375 374 and only one is imported per session.
376 375
377 376 Currently-imported Qt library: %r
378 377 PyQt4 available (requires QtCore, QtGui, QtSvg): %s
379 378 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
380 379 PySide >= 1.0.3 installed: %s
381 380 PySide2 installed: %s
382 381 Tried to load: %r
383 382 """ % (loaded_api(),
384 383 has_binding(QT_API_PYQT),
385 384 has_binding(QT_API_PYQT5),
386 385 has_binding(QT_API_PYSIDE),
387 386 has_binding(QT_API_PYSIDE2),
388 387 api_options))
389 388
390 389
391 390 def enum_factory(QT_API, QtCore):
392 391 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
393 392
394 393 @lru_cache(None)
395 394 def _enum(name):
396 395 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
397 396 return operator.attrgetter(
398 397 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
399 398 )(sys.modules[QtCore.__package__])
400 399
401 400 return _enum
@@ -1,36 +1,40 b''
1 1 # encoding: utf-8
2 2 """
3 3 Utilities for version comparison
4 4
5 5 It is a bit ridiculous that we need these.
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2013 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 #-----------------------------------------------------------------------------
16 # Imports
17 #-----------------------------------------------------------------------------
15 from warnings import warn
18 16
19 from distutils.version import LooseVersion
17 warn("The `IPython.utils.version` module has been deprecated since IPython 8.0.")
20 18
21 #-----------------------------------------------------------------------------
22 # Code
23 #-----------------------------------------------------------------------------
24 19
25 20 def check_version(v, check):
26 21 """check version string v >= check
27 22
28 23 If dev/prerelease tags result in TypeError for string-number comparison,
29 24 it is assumed that the dependency is satisfied.
30 25 Users on dev branches are responsible for keeping their own packages up to date.
31 26 """
27 warn(
28 "`check_version` function is deprecated as of IPython 8.0"
29 "and will be removed in future versions.",
30 DeprecationWarning,
31 stacklevel=2,
32 )
33
34 from distutils.version import LooseVersion
35
32 36 try:
33 37 return LooseVersion(v) >= LooseVersion(check)
34 38 except TypeError:
35 39 return True
36 40
@@ -1,47 +1,48 b''
1 1 [pytest]
2 2 addopts = --durations=10
3 3 -p IPython.testing.plugin.pytest_ipdoctest --ipdoctest-modules
4 4 --ignore=docs
5 5 --ignore=examples
6 6 --ignore=htmlcov
7 7 --ignore=ipython_kernel
8 8 --ignore=ipython_parallel
9 9 --ignore=results
10 10 --ignore=tmp
11 11 --ignore=tools
12 12 --ignore=traitlets
13 13 --ignore=IPython/core/tests/daft_extension
14 14 --ignore=IPython/sphinxext
15 15 --ignore=IPython/terminal/pt_inputhooks
16 16 --ignore=IPython/__main__.py
17 17 --ignore=IPython/config.py
18 18 --ignore=IPython/frontend.py
19 19 --ignore=IPython/html.py
20 20 --ignore=IPython/nbconvert.py
21 21 --ignore=IPython/nbformat.py
22 22 --ignore=IPython/parallel.py
23 23 --ignore=IPython/qt.py
24 24 --ignore=IPython/external/qt_for_kernel.py
25 25 --ignore=IPython/html/widgets/widget_link.py
26 26 --ignore=IPython/html/widgets/widget_output.py
27 27 --ignore=IPython/terminal/console.py
28 28 --ignore=IPython/terminal/ptshell.py
29 29 --ignore=IPython/utils/_process_cli.py
30 30 --ignore=IPython/utils/_process_posix.py
31 31 --ignore=IPython/utils/_process_win32.py
32 32 --ignore=IPython/utils/_process_win32_controller.py
33 33 --ignore=IPython/utils/daemonize.py
34 34 --ignore=IPython/utils/eventful.py
35 35
36 36 --ignore=IPython/kernel
37 37 --ignore=IPython/consoleapp.py
38 38 --ignore=IPython/core/inputsplitter.py
39 39 --ignore-glob=IPython/lib/inputhook*.py
40 40 --ignore=IPython/lib/kernel.py
41 41 --ignore=IPython/utils/jsonutil.py
42 42 --ignore=IPython/utils/localinterfaces.py
43 43 --ignore=IPython/utils/log.py
44 44 --ignore=IPython/utils/signatures.py
45 45 --ignore=IPython/utils/traitlets.py
46 --ignore=IPython/utils/version.py
46 47 doctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS
47 48 ipdoctest_optionflags = NORMALIZE_WHITESPACE ELLIPSIS
General Comments 0
You need to be logged in to leave comments. Login now