qt_loaders.py
334 lines
| 9.5 KiB
| text/x-python
|
PythonLexer
Chris Beaumont
|
r9722 | """ | ||
This module contains factory functions that attempt | ||||
to return Qt submodules from the various python Qt bindings. | ||||
It also protects against double-importing Qt with different | ||||
bindings, which is unstable and likely to crash | ||||
This is used primarily by qt and qt_for_kernel, and shouldn't | ||||
be accessed directly from the outside | ||||
""" | ||||
import sys | ||||
Peter Würtz
|
r16414 | import types | ||
Chris Beaumont
|
r9722 | from functools import partial | ||
Diego Garcia
|
r22954 | from importlib import import_module | ||
Chris Beaumont
|
r9722 | |||
from IPython.utils.version import check_version | ||||
# Available APIs. | ||||
Zachary Pincus
|
r21679 | QT_API_PYQT = 'pyqt' # Force version 2 | ||
Peter Würtz
|
r16414 | QT_API_PYQT5 = 'pyqt5' | ||
Zachary Pincus
|
r21679 | QT_API_PYQTv1 = 'pyqtv1' # Force version 2 | ||
QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2 | ||||
Chris Beaumont
|
r9722 | QT_API_PYSIDE = 'pyside' | ||
Thomas Kluyver
|
r23162 | QT_API_PYSIDE2 = 'pyside2' | ||
api_to_module = {QT_API_PYSIDE2: 'PySide2', | ||||
QT_API_PYSIDE: 'PySide', | ||||
QT_API_PYQT: 'PyQt4', | ||||
QT_API_PYQTv1: 'PyQt4', | ||||
QT_API_PYQT5: 'PyQt5', | ||||
QT_API_PYQT_DEFAULT: 'PyQt4', | ||||
} | ||||
Chris Beaumont
|
r9722 | |||
class ImportDenier(object): | ||||
"""Import Hook that will guard against bad Qt imports | ||||
once IPython commits to a specific binding | ||||
""" | ||||
def __init__(self): | ||||
Peter Würtz
|
r16414 | self.__forbidden = set() | ||
Chris Beaumont
|
r9722 | |||
def forbid(self, module_name): | ||||
sys.modules.pop(module_name, None) | ||||
Peter Würtz
|
r16414 | self.__forbidden.add(module_name) | ||
Chris Beaumont
|
r9722 | |||
MinRK
|
r16814 | def find_module(self, fullname, path=None): | ||
if path: | ||||
Chris Beaumont
|
r9722 | return | ||
MinRK
|
r16814 | if fullname in self.__forbidden: | ||
Chris Beaumont
|
r9722 | return self | ||
MinRK
|
r16814 | def load_module(self, fullname): | ||
Chris Beaumont
|
r9722 | raise ImportError(""" | ||
Importing %s disabled by IPython, which has | ||||
already imported an Incompatible QT Binding: %s | ||||
MinRK
|
r16814 | """ % (fullname, loaded_api())) | ||
Chris Beaumont
|
r9722 | |||
ID = ImportDenier() | ||||
Thomas Kluyver
|
r23172 | sys.meta_path.insert(0, ID) | ||
Chris Beaumont
|
r9722 | |||
def commit_api(api): | ||||
"""Commit to a particular API, and trigger ImportErrors on subsequent | ||||
dangerous imports""" | ||||
Thomas Kluyver
|
r23163 | if api == QT_API_PYSIDE2: | ||
ID.forbid('PySide') | ||||
ID.forbid('PyQt4') | ||||
ID.forbid('PyQt5') | ||||
Martin Bergtholdt
|
r24792 | elif api == QT_API_PYSIDE: | ||
Thomas Kluyver
|
r23163 | ID.forbid('PySide2') | ||
Chris Beaumont
|
r9722 | ID.forbid('PyQt4') | ||
Peter Würtz
|
r16414 | ID.forbid('PyQt5') | ||
Thomas Kluyver
|
r18405 | elif api == QT_API_PYQT5: | ||
Thomas Kluyver
|
r23163 | ID.forbid('PySide2') | ||
Peter Würtz
|
r16414 | ID.forbid('PySide') | ||
ID.forbid('PyQt4') | ||||
Thomas Kluyver
|
r18405 | else: # There are three other possibilities, all representing PyQt4 | ||
ID.forbid('PyQt5') | ||||
Thomas Kluyver
|
r23163 | ID.forbid('PySide2') | ||
Chris Beaumont
|
r9722 | ID.forbid('PySide') | ||
def loaded_api(): | ||||
"""Return which API is loaded, if any | ||||
If this returns anything besides None, | ||||
importing any other Qt binding is unsafe. | ||||
Returns | ||||
------- | ||||
Thomas Kluyver
|
r23163 | None, 'pyside2', 'pyside', 'pyqt', 'pyqt5', or 'pyqtv1' | ||
Chris Beaumont
|
r9722 | """ | ||
if 'PyQt4.QtCore' in sys.modules: | ||||
if qtapi_version() == 2: | ||||
return QT_API_PYQT | ||||
else: | ||||
return QT_API_PYQTv1 | ||||
elif 'PySide.QtCore' in sys.modules: | ||||
return QT_API_PYSIDE | ||||
Thomas Kluyver
|
r23163 | elif 'PySide2.QtCore' in sys.modules: | ||
return QT_API_PYSIDE2 | ||||
Peter Würtz
|
r16414 | elif 'PyQt5.QtCore' in sys.modules: | ||
return QT_API_PYQT5 | ||||
Chris Beaumont
|
r9722 | return None | ||
def has_binding(api): | ||||
Thomas Kluyver
|
r23163 | """Safely check for PyQt4/5, PySide or PySide2, without importing submodules | ||
Thomas Kluyver
|
r23162 | |||
Parameters | ||||
---------- | ||||
Thomas Kluyver
|
r23163 | api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault'] | ||
Thomas Kluyver
|
r23162 | Which module to check for | ||
Returns | ||||
------- | ||||
True if the relevant module appears to be importable | ||||
""" | ||||
module_name = api_to_module[api] | ||||
from importlib.util import find_spec | ||||
required = ['QtCore', 'QtGui', 'QtSvg'] | ||||
if api in (QT_API_PYQT5, QT_API_PYSIDE2): | ||||
# QT5 requires QtWidgets too | ||||
required.append('QtWidgets') | ||||
for submod in required: | ||||
try: | ||||
spec = find_spec('%s.%s' % (module_name, submod)) | ||||
except ImportError: | ||||
# Package (e.g. PyQt5) not found | ||||
return False | ||||
else: | ||||
if spec is None: | ||||
# Submodule (e.g. PyQt5.QtCore) not found | ||||
return False | ||||
if api == QT_API_PYSIDE: | ||||
# We can also safely check PySide version | ||||
import PySide | ||||
return check_version(PySide.__version__, '1.0.3') | ||||
return True | ||||
Chris Beaumont
|
r9722 | |||
def qtapi_version(): | ||||
"""Return which QString API has been set, if any | ||||
Returns | ||||
------- | ||||
The QString API version (1 or 2), or None if not set | ||||
""" | ||||
try: | ||||
import sip | ||||
except ImportError: | ||||
Gordon Ball
|
r26224 | # as of PyQt5 5.11, sip is no longer available as a top-level | ||
# module and needs to be imported from the PyQt5 namespace | ||||
try: | ||||
from PyQt5 import sip | ||||
except ImportError: | ||||
return | ||||
Chris Beaumont
|
r9722 | try: | ||
return sip.getapi('QString') | ||||
except ValueError: | ||||
return | ||||
def can_import(api): | ||||
"""Safely query whether an API is importable, without importing it""" | ||||
Chris Beaumont
|
r9815 | if not has_binding(api): | ||
return False | ||||
Chris Beaumont
|
r9722 | current = loaded_api() | ||
Chris Beaumont
|
r9815 | if api == QT_API_PYQT_DEFAULT: | ||
return current in [QT_API_PYQT, QT_API_PYQTv1, None] | ||||
else: | ||||
return current in [api, None] | ||||
Chris Beaumont
|
r9722 | |||
def import_pyqt4(version=2): | ||||
""" | ||||
Import PyQt4 | ||||
Chris Beaumont
|
r9815 | Parameters | ||
---------- | ||||
version : 1, 2, or None | ||||
Which QString/QVariant API to use. Set to None to use the system | ||||
default | ||||
Chris Beaumont
|
r9722 | ImportErrors rasied within this function are non-recoverable | ||
""" | ||||
# The new-style string API (version=2) automatically | ||||
# converts QStrings to Unicode Python strings. Also, automatically unpacks | ||||
# QVariants to their underlying objects. | ||||
import sip | ||||
Chris Beaumont
|
r9815 | |||
if version is not None: | ||||
sip.setapi('QString', version) | ||||
sip.setapi('QVariant', version) | ||||
Chris Beaumont
|
r9722 | |||
from PyQt4 import QtGui, QtCore, QtSvg | ||||
if not check_version(QtCore.PYQT_VERSION_STR, '4.7'): | ||||
raise ImportError("IPython requires PyQt4 >= 4.7, found %s" % | ||||
QtCore.PYQT_VERSION_STR) | ||||
# Alias PyQt-specific functions for PySide compatibility. | ||||
QtCore.Signal = QtCore.pyqtSignal | ||||
QtCore.Slot = QtCore.pyqtSlot | ||||
Chris Beaumont
|
r9815 | # query for the API version (in case version == None) | ||
version = sip.getapi('QString') | ||||
Chris Beaumont
|
r9722 | api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT | ||
return QtCore, QtGui, QtSvg, api | ||||
Peter Würtz
|
r16414 | def import_pyqt5(): | ||
""" | ||||
Import PyQt5 | ||||
ImportErrors rasied within this function are non-recoverable | ||||
""" | ||||
from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui | ||||
bnables
|
r24949 | |||
Peter Würtz
|
r16414 | # Alias PyQt-specific functions for PySide compatibility. | ||
QtCore.Signal = QtCore.pyqtSignal | ||||
QtCore.Slot = QtCore.pyqtSlot | ||||
# Join QtGui and QtWidgets for Qt4 compatibility. | ||||
QtGuiCompat = types.ModuleType('QtGuiCompat') | ||||
QtGuiCompat.__dict__.update(QtGui.__dict__) | ||||
QtGuiCompat.__dict__.update(QtWidgets.__dict__) | ||||
api = QT_API_PYQT5 | ||||
return QtCore, QtGuiCompat, QtSvg, api | ||||
Chris Beaumont
|
r9722 | def import_pyside(): | ||
""" | ||||
Import PySide | ||||
ImportErrors raised within this function are non-recoverable | ||||
""" | ||||
from PySide import QtGui, QtCore, QtSvg | ||||
return QtCore, QtGui, QtSvg, QT_API_PYSIDE | ||||
Thomas Kluyver
|
r23163 | def import_pyside2(): | ||
""" | ||||
Import PySide2 | ||||
ImportErrors raised within this function are non-recoverable | ||||
""" | ||||
from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport | ||||
# Join QtGui and QtWidgets for Qt4 compatibility. | ||||
QtGuiCompat = types.ModuleType('QtGuiCompat') | ||||
QtGuiCompat.__dict__.update(QtGui.__dict__) | ||||
QtGuiCompat.__dict__.update(QtWidgets.__dict__) | ||||
QtGuiCompat.__dict__.update(QtPrintSupport.__dict__) | ||||
return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2 | ||||
Chris Beaumont
|
r9722 | |||
def load_qt(api_options): | ||||
""" | ||||
Attempt to import Qt, given a preference list | ||||
of permissible bindings | ||||
It is safe to call this function multiple times. | ||||
Parameters | ||||
---------- | ||||
api_options: List of strings | ||||
Thomas Kluyver
|
r23163 | The order of APIs to try. Valid items are 'pyside', 'pyside2', | ||
Thomas Kluyver
|
r18405 | 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault' | ||
Chris Beaumont
|
r9722 | |||
Returns | ||||
------- | ||||
A tuple of QtCore, QtGui, QtSvg, QT_API | ||||
The first three are the Qt modules. The last is the | ||||
string indicating which module was loaded. | ||||
Raises | ||||
------ | ||||
ImportError, if it isn't possible to import any requested | ||||
Min ho Kim
|
r25167 | bindings (either because they aren't installed, or because | ||
Chris Beaumont
|
r9722 | an incompatible library has already been installed) | ||
""" | ||||
Thomas Kluyver
|
r23163 | loaders = { | ||
QT_API_PYSIDE2: import_pyside2, | ||||
QT_API_PYSIDE: import_pyside, | ||||
Chris Beaumont
|
r9722 | QT_API_PYQT: import_pyqt4, | ||
Peter Würtz
|
r16414 | QT_API_PYQT5: import_pyqt5, | ||
Chris Beaumont
|
r9815 | QT_API_PYQTv1: partial(import_pyqt4, version=1), | ||
QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None) | ||||
Thomas Kluyver
|
r23163 | } | ||
Chris Beaumont
|
r9722 | |||
for api in api_options: | ||||
if api not in loaders: | ||||
raise RuntimeError( | ||||
Peter Würtz
|
r16414 | "Invalid Qt API %r, valid values are: %s" % | ||
(api, ", ".join(["%r" % k for k in loaders.keys()]))) | ||||
Chris Beaumont
|
r9722 | |||
if not can_import(api): | ||||
continue | ||||
#cannot safely recover from an ImportError during this | ||||
result = loaders[api]() | ||||
Chris Beaumont
|
r9815 | api = result[-1] # changed if api = QT_API_PYQT_DEFAULT | ||
Chris Beaumont
|
r9722 | commit_api(api) | ||
return result | ||||
else: | ||||
raise ImportError(""" | ||||
Could not load requested Qt binding. Please ensure that | ||||
Thomas Kluyver
|
r23163 | PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available, | ||
Chris Beaumont
|
r9722 | and only one is imported per session. | ||
Jan-Philip Gehrcke
|
r21879 | Currently-imported Qt library: %r | ||
PyQt4 available (requires QtCore, QtGui, QtSvg): %s | ||||
PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s | ||||
PySide >= 1.0.3 installed: %s | ||||
Thomas Kluyver
|
r23163 | PySide2 installed: %s | ||
Jan-Philip Gehrcke
|
r21879 | Tried to load: %r | ||
Chris Beaumont
|
r9722 | """ % (loaded_api(), | ||
has_binding(QT_API_PYQT), | ||||
Peter Würtz
|
r16414 | has_binding(QT_API_PYQT5), | ||
Chris Beaumont
|
r9722 | has_binding(QT_API_PYSIDE), | ||
Thomas Kluyver
|
r23163 | has_binding(QT_API_PYSIDE2), | ||
Chris Beaumont
|
r9722 | api_options)) | ||