qt_loaders.py
410 lines
| 11.2 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 | ||||
""" | ||||
Nikita Kniazev
|
r27079 | import importlib.abc | ||
Chris Beaumont
|
r9722 | import sys | ||
Emilio Graff
|
r28138 | import os | ||
Peter Würtz
|
r16414 | import types | ||
Thomas A Caswell
|
r26683 | from functools import partial, lru_cache | ||
import operator | ||||
Chris Beaumont
|
r9722 | |||
Thomas A Caswell
|
r26683 | # ### Available APIs. | ||
# Qt6 | ||||
QT_API_PYQT6 = "pyqt6" | ||||
QT_API_PYSIDE6 = "pyside6" | ||||
# Qt5 | ||||
Peter Würtz
|
r16414 | QT_API_PYQT5 = 'pyqt5' | ||
Thomas Kluyver
|
r23162 | QT_API_PYSIDE2 = 'pyside2' | ||
Thomas A Caswell
|
r26683 | # Qt4 | ||
Emilio Graff
|
r28021 | # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side. | ||
Thomas A Caswell
|
r26684 | QT_API_PYQT = "pyqt" # Force version 2 | ||
QT_API_PYQTv1 = "pyqtv1" # Force version 2 | ||||
QT_API_PYSIDE = "pyside" | ||||
Thomas A Caswell
|
r26683 | |||
Thomas A Caswell
|
r26684 | QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2 | ||
Thomas A Caswell
|
r26683 | |||
api_to_module = { | ||||
# Qt6 | ||||
QT_API_PYQT6: "PyQt6", | ||||
QT_API_PYSIDE6: "PySide6", | ||||
# Qt5 | ||||
Thomas A Caswell
|
r26684 | QT_API_PYQT5: "PyQt5", | ||
QT_API_PYSIDE2: "PySide2", | ||||
Thomas A Caswell
|
r26683 | # Qt4 | ||
Thomas A Caswell
|
r26684 | QT_API_PYSIDE: "PySide", | ||
QT_API_PYQT: "PyQt4", | ||||
QT_API_PYQTv1: "PyQt4", | ||||
Thomas A Caswell
|
r26683 | # default | ||
Thomas A Caswell
|
r26684 | QT_API_PYQT_DEFAULT: "PyQt6", | ||
Thomas A Caswell
|
r26683 | } | ||
Chris Beaumont
|
r9722 | |||
Nikita Kniazev
|
r27079 | class ImportDenier(importlib.abc.MetaPathFinder): | ||
Chris Beaumont
|
r9722 | """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 | |||
Nikita Kniazev
|
r27079 | def find_spec(self, fullname, path, target=None): | ||
MinRK
|
r16814 | if path: | ||
Chris Beaumont
|
r9722 | return | ||
MinRK
|
r16814 | if fullname in self.__forbidden: | ||
Nikita Kniazev
|
r27079 | raise ImportError( | ||
""" | ||||
Chris Beaumont
|
r9722 | Importing %s disabled by IPython, which has | ||
already imported an Incompatible QT Binding: %s | ||||
Matthias Bussonnier
|
r27639 | """ | ||
% (fullname, loaded_api()) | ||||
) | ||||
Chris Beaumont
|
r9722 | |||
Thomas A Caswell
|
r26683 | |||
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 | ||||
Matthias Bussonnier
|
r27295 | dangerous imports""" | ||
Thomas A Caswell
|
r26683 | modules = set(api_to_module.values()) | ||
Chris Beaumont
|
r9722 | |||
Thomas A Caswell
|
r26683 | modules.remove(api_to_module[api]) | ||
for mod in modules: | ||||
ID.forbid(mod) | ||||
Chris Beaumont
|
r9722 | |||
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 A Caswell
|
r26683 | None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1' | ||
Chris Beaumont
|
r9722 | """ | ||
Thomas A Caswell
|
r26683 | 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 | ||||
Thomas A Caswell
|
r26684 | elif sys.modules.get("PyQt4.QtCore"): | ||
Chris Beaumont
|
r9722 | if qtapi_version() == 2: | ||
return QT_API_PYQT | ||||
else: | ||||
return QT_API_PYQTv1 | ||||
Thomas A Caswell
|
r26684 | elif sys.modules.get("PySide.QtCore"): | ||
Chris Beaumont
|
r9722 | return QT_API_PYSIDE | ||
Thomas A Caswell
|
r26683 | |||
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 | |||
Matthias Bussonnier
|
r27295 | Parameters | ||
---------- | ||||
api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault'] | ||||
Which module to check for | ||||
Thomas Kluyver
|
r23162 | |||
Matthias Bussonnier
|
r27295 | Returns | ||
------- | ||||
True if the relevant module appears to be importable | ||||
""" | ||||
Thomas Kluyver
|
r23162 | module_name = api_to_module[api] | ||
from importlib.util import find_spec | ||||
required = ['QtCore', 'QtGui', 'QtSvg'] | ||||
Thomas A Caswell
|
r26683 | if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6): | ||
Thomas Kluyver
|
r23162 | # 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 | ||||
Nikita Kniazev
|
r27086 | |||
return PySide.__version_info__ >= (1, 0, 3) | ||||
Thomas Kluyver
|
r23162 | |||
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: | ||
Thomas A Caswell
|
r26683 | return current in [QT_API_PYQT6, None] | ||
Chris Beaumont
|
r9815 | 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 | ||||
Matthias Bussonnier
|
r27295 | Which QString/QVariant API to use. Set to None to use the system | ||
default | ||||
Dimitri Papadopoulos
|
r26875 | ImportErrors raised within this function are non-recoverable | ||
Chris Beaumont
|
r9722 | """ | ||
# 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 | ||||
Nikita Kniazev
|
r27086 | if QtCore.PYQT_VERSION < 0x040700: | ||
Chris Beaumont
|
r9722 | 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 | ||||
Dimitri Papadopoulos
|
r26875 | ImportErrors raised within this function are non-recoverable | ||
Peter Würtz
|
r16414 | """ | ||
from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui | ||||
Thomas A Caswell
|
r26683 | |||
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 | ||||
Thomas A Caswell
|
r26684 | |||
Thomas A Caswell
|
r26683 | def import_pyqt6(): | ||
""" | ||||
Import PyQt6 | ||||
Dimitri Papadopoulos
|
r26875 | ImportErrors raised within this function are non-recoverable | ||
Thomas A Caswell
|
r26683 | """ | ||
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. | ||||
Thomas A Caswell
|
r26684 | QtGuiCompat = types.ModuleType("QtGuiCompat") | ||
Thomas A Caswell
|
r26683 | QtGuiCompat.__dict__.update(QtGui.__dict__) | ||
QtGuiCompat.__dict__.update(QtWidgets.__dict__) | ||||
api = QT_API_PYQT6 | ||||
return QtCore, QtGuiCompat, QtSvg, api | ||||
Peter Würtz
|
r16414 | |||
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 | ||||
Thomas A Caswell
|
r26684 | |||
Thomas A Caswell
|
r26683 | 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. | ||||
Thomas A Caswell
|
r26684 | QtGuiCompat = types.ModuleType("QtGuiCompat") | ||
Thomas A Caswell
|
r26683 | QtGuiCompat.__dict__.update(QtGui.__dict__) | ||
QtGuiCompat.__dict__.update(QtWidgets.__dict__) | ||||
QtGuiCompat.__dict__.update(QtPrintSupport.__dict__) | ||||
return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6 | ||||
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 | ||||
---------- | ||||
Matthias Bussonnier
|
r27295 | 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 = { | ||
Thomas A Caswell
|
r26683 | # 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, | ||||
} | ||||
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: | ||||
Emilio Graff
|
r28138 | # Clear the environment variable since it doesn't work. | ||
if "QT_API" in os.environ: | ||||
del os.environ["QT_API"] | ||||
Emilio Graff
|
r27994 | raise ImportError( | ||
""" | ||||
Chris Beaumont
|
r9722 | Could not load requested Qt binding. Please ensure that | ||
Emilio Graff
|
r27997 | PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or | ||
PySide6 is available, and only one is imported per session. | ||||
Chris Beaumont
|
r9722 | |||
Jan-Philip Gehrcke
|
r21879 | Currently-imported Qt library: %r | ||
PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s | ||||
Emilio Graff
|
r27988 | PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s | ||
Thomas Kluyver
|
r23163 | PySide2 installed: %s | ||
Emilio Graff
|
r27985 | PySide6 installed: %s | ||
Jan-Philip Gehrcke
|
r21879 | Tried to load: %r | ||
Emilio Graff
|
r27994 | """ | ||
% ( | ||||
loaded_api(), | ||||
has_binding(QT_API_PYQT5), | ||||
has_binding(QT_API_PYQT6), | ||||
has_binding(QT_API_PYSIDE2), | ||||
has_binding(QT_API_PYSIDE6), | ||||
api_options, | ||||
) | ||||
) | ||||
Thomas A Caswell
|
r26683 | |||
Emilio Graff
|
r27995 | |||
Thomas A Caswell
|
r26683 | def enum_factory(QT_API, QtCore): | ||
"""Construct an enum helper to account for PyQt5 <-> PyQt6 changes.""" | ||||
Thomas A Caswell
|
r26684 | |||
Thomas A Caswell
|
r26683 | @lru_cache(None) | ||
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 | ||||