Spaces:
Runtime error
Runtime error
""" | |
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 importlib.abc | |
import sys | |
import os | |
import types | |
from functools import partial, lru_cache | |
import operator | |
# ### Available APIs. | |
# Qt6 | |
QT_API_PYQT6 = "pyqt6" | |
QT_API_PYSIDE6 = "pyside6" | |
# Qt5 | |
QT_API_PYQT5 = 'pyqt5' | |
QT_API_PYSIDE2 = 'pyside2' | |
# Qt4 | |
# NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side. | |
QT_API_PYQT = "pyqt" # Force version 2 | |
QT_API_PYQTv1 = "pyqtv1" # Force version 2 | |
QT_API_PYSIDE = "pyside" | |
QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2 | |
api_to_module = { | |
# Qt6 | |
QT_API_PYQT6: "PyQt6", | |
QT_API_PYSIDE6: "PySide6", | |
# Qt5 | |
QT_API_PYQT5: "PyQt5", | |
QT_API_PYSIDE2: "PySide2", | |
# Qt4 | |
QT_API_PYSIDE: "PySide", | |
QT_API_PYQT: "PyQt4", | |
QT_API_PYQTv1: "PyQt4", | |
# default | |
QT_API_PYQT_DEFAULT: "PyQt6", | |
} | |
class ImportDenier(importlib.abc.MetaPathFinder): | |
"""Import Hook that will guard against bad Qt imports | |
once IPython commits to a specific binding | |
""" | |
def __init__(self): | |
self.__forbidden = set() | |
def forbid(self, module_name): | |
sys.modules.pop(module_name, None) | |
self.__forbidden.add(module_name) | |
def find_spec(self, fullname, path, target=None): | |
if path: | |
return | |
if fullname in self.__forbidden: | |
raise ImportError( | |
""" | |
Importing %s disabled by IPython, which has | |
already imported an Incompatible QT Binding: %s | |
""" | |
% (fullname, loaded_api()) | |
) | |
ID = ImportDenier() | |
sys.meta_path.insert(0, ID) | |
def commit_api(api): | |
"""Commit to a particular API, and trigger ImportErrors on subsequent | |
dangerous imports""" | |
modules = set(api_to_module.values()) | |
modules.remove(api_to_module[api]) | |
for mod in modules: | |
ID.forbid(mod) | |
def loaded_api(): | |
"""Return which API is loaded, if any | |
If this returns anything besides None, | |
importing any other Qt binding is unsafe. | |
Returns | |
------- | |
None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1' | |
""" | |
if sys.modules.get("PyQt6.QtCore"): | |
return QT_API_PYQT6 | |
elif sys.modules.get("PySide6.QtCore"): | |
return QT_API_PYSIDE6 | |
elif sys.modules.get("PyQt5.QtCore"): | |
return QT_API_PYQT5 | |
elif sys.modules.get("PySide2.QtCore"): | |
return QT_API_PYSIDE2 | |
elif sys.modules.get("PyQt4.QtCore"): | |
if qtapi_version() == 2: | |
return QT_API_PYQT | |
else: | |
return QT_API_PYQTv1 | |
elif sys.modules.get("PySide.QtCore"): | |
return QT_API_PYSIDE | |
return None | |
def has_binding(api): | |
"""Safely check for PyQt4/5, PySide or PySide2, without importing submodules | |
Parameters | |
---------- | |
api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault'] | |
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, QT_API_PYQT6, QT_API_PYSIDE6): | |
# 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 PySide.__version_info__ >= (1, 0, 3) | |
return True | |
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: | |
# 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 | |
try: | |
return sip.getapi('QString') | |
except ValueError: | |
return | |
def can_import(api): | |
"""Safely query whether an API is importable, without importing it""" | |
if not has_binding(api): | |
return False | |
current = loaded_api() | |
if api == QT_API_PYQT_DEFAULT: | |
return current in [QT_API_PYQT6, None] | |
else: | |
return current in [api, None] | |
def import_pyqt4(version=2): | |
""" | |
Import PyQt4 | |
Parameters | |
---------- | |
version : 1, 2, or None | |
Which QString/QVariant API to use. Set to None to use the system | |
default | |
ImportErrors raised 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 | |
if version is not None: | |
sip.setapi('QString', version) | |
sip.setapi('QVariant', version) | |
from PyQt4 import QtGui, QtCore, QtSvg | |
if QtCore.PYQT_VERSION < 0x040700: | |
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 | |
# query for the API version (in case version == None) | |
version = sip.getapi('QString') | |
api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT | |
return QtCore, QtGui, QtSvg, api | |
def import_pyqt5(): | |
""" | |
Import PyQt5 | |
ImportErrors raised within this function are non-recoverable | |
""" | |
from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui | |
# 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 | |
def import_pyqt6(): | |
""" | |
Import PyQt6 | |
ImportErrors raised within this function are non-recoverable | |
""" | |
from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui | |
# 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_PYQT6 | |
return QtCore, QtGuiCompat, QtSvg, api | |
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 | |
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 | |
def import_pyside6(): | |
""" | |
Import PySide6 | |
ImportErrors raised within this function are non-recoverable | |
""" | |
from PySide6 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_PYSIDE6 | |
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 | |
The order of APIs to try. Valid items are 'pyside', 'pyside2', | |
'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault' | |
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 | |
bindings (either because they aren't installed, or because | |
an incompatible library has already been installed) | |
""" | |
loaders = { | |
# Qt6 | |
QT_API_PYQT6: import_pyqt6, | |
QT_API_PYSIDE6: import_pyside6, | |
# Qt5 | |
QT_API_PYQT5: import_pyqt5, | |
QT_API_PYSIDE2: import_pyside2, | |
# Qt4 | |
QT_API_PYSIDE: import_pyside, | |
QT_API_PYQT: import_pyqt4, | |
QT_API_PYQTv1: partial(import_pyqt4, version=1), | |
# default | |
QT_API_PYQT_DEFAULT: import_pyqt6, | |
} | |
for api in api_options: | |
if api not in loaders: | |
raise RuntimeError( | |
"Invalid Qt API %r, valid values are: %s" % | |
(api, ", ".join(["%r" % k for k in loaders.keys()]))) | |
if not can_import(api): | |
continue | |
#cannot safely recover from an ImportError during this | |
result = loaders[api]() | |
api = result[-1] # changed if api = QT_API_PYQT_DEFAULT | |
commit_api(api) | |
return result | |
else: | |
# Clear the environment variable since it doesn't work. | |
if "QT_API" in os.environ: | |
del os.environ["QT_API"] | |
raise ImportError( | |
""" | |
Could not load requested Qt binding. Please ensure that | |
PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or | |
PySide6 is available, and only one is imported per session. | |
Currently-imported Qt library: %r | |
PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s | |
PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s | |
PySide2 installed: %s | |
PySide6 installed: %s | |
Tried to load: %r | |
""" | |
% ( | |
loaded_api(), | |
has_binding(QT_API_PYQT5), | |
has_binding(QT_API_PYQT6), | |
has_binding(QT_API_PYSIDE2), | |
has_binding(QT_API_PYSIDE6), | |
api_options, | |
) | |
) | |
def enum_factory(QT_API, QtCore): | |
"""Construct an enum helper to account for PyQt5 <-> PyQt6 changes.""" | |
def _enum(name): | |
# foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6). | |
return operator.attrgetter( | |
name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0] | |
)(sys.modules[QtCore.__package__]) | |
return _enum | |