##// END OF EJS Templates
Statically type OInfo....
Statically type OInfo. In view of working with #13860, some cleanup inspect to be properly typed, and using stricter datastructure. Instead of dict we now use dataclasses, this will make sure that fields type and access can be stricter and verified not only at runtime, but by mypy

File last commit:

r28021:b332fd1a
r28165:0fef298a
Show More
qt_loaders.py
405 lines | 11.1 KiB | text/x-python | PythonLexer
Chris Beaumont
Refactor qt import logic. Fixes #2955
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
ImportDenier: implement modern interface
r27079 import importlib.abc
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 import sys
Peter Würtz
Add support for PyQt5.
r16414 import types
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 from functools import partial, lru_cache
import operator
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 # ### Available APIs.
# Qt6
QT_API_PYQT6 = "pyqt6"
QT_API_PYSIDE6 = "pyside6"
# Qt5
Peter Würtz
Add support for PyQt5.
r16414 QT_API_PYQT5 = 'pyqt5'
Thomas Kluyver
New implementation of has_binding Qt check for Python 3.4+...
r23162 QT_API_PYSIDE2 = 'pyside2'
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 # Qt4
Emilio Graff
Remove direct support for Qt4...
r28021 # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side.
Thomas A Caswell
STY: apply darker
r26684 QT_API_PYQT = "pyqt" # Force version 2
QT_API_PYQTv1 = "pyqtv1" # Force version 2
QT_API_PYSIDE = "pyside"
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683
Thomas A Caswell
STY: apply darker
r26684 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683
api_to_module = {
# Qt6
QT_API_PYQT6: "PyQt6",
QT_API_PYSIDE6: "PySide6",
# Qt5
Thomas A Caswell
STY: apply darker
r26684 QT_API_PYQT5: "PyQt5",
QT_API_PYSIDE2: "PySide2",
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 # Qt4
Thomas A Caswell
STY: apply darker
r26684 QT_API_PYSIDE: "PySide",
QT_API_PYQT: "PyQt4",
QT_API_PYQTv1: "PyQt4",
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 # default
Thomas A Caswell
STY: apply darker
r26684 QT_API_PYQT_DEFAULT: "PyQt6",
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 }
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
Nikita Kniazev
ImportDenier: implement modern interface
r27079 class ImportDenier(importlib.abc.MetaPathFinder):
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 """Import Hook that will guard against bad Qt imports
once IPython commits to a specific binding
"""
def __init__(self):
Peter Würtz
Add support for PyQt5.
r16414 self.__forbidden = set()
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
def forbid(self, module_name):
sys.modules.pop(module_name, None)
Peter Würtz
Add support for PyQt5.
r16414 self.__forbidden.add(module_name)
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
Nikita Kniazev
ImportDenier: implement modern interface
r27079 def find_spec(self, fullname, path, target=None):
MinRK
fix qt_loader import hook signature...
r16814 if path:
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 return
MinRK
fix qt_loader import hook signature...
r16814 if fullname in self.__forbidden:
Nikita Kniazev
ImportDenier: implement modern interface
r27079 raise ImportError(
"""
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 Importing %s disabled by IPython, which has
already imported an Incompatible QT Binding: %s
Matthias Bussonnier
typo and reformat
r27639 """
% (fullname, loaded_api())
)
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 ID = ImportDenier()
Thomas Kluyver
Move ImportDenier to the front of sys.meta_path...
r23172 sys.meta_path.insert(0, ID)
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
def commit_api(api):
"""Commit to a particular API, and trigger ImportErrors on subsequent
Matthias Bussonnier
reformat rest of ipython
r27295 dangerous imports"""
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 modules = set(api_to_module.values())
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 modules.remove(api_to_module[api])
for mod in modules:
ID.forbid(mod)
Chris Beaumont
Refactor qt import logic. Fixes #2955
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
ENH: add support for Qt6 input hooks...
r26683 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 """
Thomas A Caswell
ENH: add support for Qt6 input hooks...
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
STY: apply darker
r26684 elif sys.modules.get("PyQt4.QtCore"):
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 if qtapi_version() == 2:
return QT_API_PYQT
else:
return QT_API_PYQTv1
Thomas A Caswell
STY: apply darker
r26684 elif sys.modules.get("PySide.QtCore"):
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 return QT_API_PYSIDE
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 return None
def has_binding(api):
Thomas Kluyver
Copy across PySide2 support from Qtconsole
r23163 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
Thomas Kluyver
New implementation of has_binding Qt check for Python 3.4+...
r23162
Matthias Bussonnier
reformat rest of ipython
r27295 Parameters
----------
api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
Which module to check for
Thomas Kluyver
New implementation of has_binding Qt check for Python 3.4+...
r23162
Matthias Bussonnier
reformat rest of ipython
r27295 Returns
-------
True if the relevant module appears to be importable
"""
Thomas Kluyver
New implementation of has_binding Qt check for Python 3.4+...
r23162 module_name = api_to_module[api]
from importlib.util import find_spec
required = ['QtCore', 'QtGui', 'QtSvg']
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
Thomas Kluyver
New implementation of has_binding Qt check for Python 3.4+...
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
Deprecate `IPython.utils.version`...
r27086
return PySide.__version_info__ >= (1, 0, 3)
Thomas Kluyver
New implementation of has_binding Qt check for Python 3.4+...
r23162
return True
Chris Beaumont
Refactor qt import logic. Fixes #2955
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
qt: sip changes for PyQt5 >= 5.11...
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
Refactor qt import logic. Fixes #2955
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
added QT_API_DEFAULT Qt option. Fixed reversed MPL qt logic test.
r9815 if not has_binding(api):
return False
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 current = loaded_api()
Chris Beaumont
added QT_API_DEFAULT Qt option. Fixed reversed MPL qt logic test.
r9815 if api == QT_API_PYQT_DEFAULT:
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 return current in [QT_API_PYQT6, None]
Chris Beaumont
added QT_API_DEFAULT Qt option. Fixed reversed MPL qt logic test.
r9815 else:
return current in [api, None]
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
def import_pyqt4(version=2):
"""
Import PyQt4
Chris Beaumont
added QT_API_DEFAULT Qt option. Fixed reversed MPL qt logic test.
r9815 Parameters
----------
version : 1, 2, or None
Matthias Bussonnier
reformat rest of ipython
r27295 Which QString/QVariant API to use. Set to None to use the system
default
Dimitri Papadopoulos
Typos found by codespell
r26875 ImportErrors raised within this function are non-recoverable
Chris Beaumont
Refactor qt import logic. Fixes #2955
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
added QT_API_DEFAULT Qt option. Fixed reversed MPL qt logic test.
r9815
if version is not None:
sip.setapi('QString', version)
sip.setapi('QVariant', version)
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
from PyQt4 import QtGui, QtCore, QtSvg
Nikita Kniazev
Deprecate `IPython.utils.version`...
r27086 if QtCore.PYQT_VERSION < 0x040700:
Chris Beaumont
Refactor qt import logic. Fixes #2955
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
added QT_API_DEFAULT Qt option. Fixed reversed MPL qt logic test.
r9815 # query for the API version (in case version == None)
version = sip.getapi('QString')
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
return QtCore, QtGui, QtSvg, api
Peter Würtz
Add support for PyQt5.
r16414 def import_pyqt5():
"""
Import PyQt5
Dimitri Papadopoulos
Typos found by codespell
r26875 ImportErrors raised within this function are non-recoverable
Peter Würtz
Add support for PyQt5.
r16414 """
from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683
Peter Würtz
Add support for PyQt5.
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
STY: apply darker
r26684
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 def import_pyqt6():
"""
Import PyQt6
Dimitri Papadopoulos
Typos found by codespell
r26875 ImportErrors raised within this function are non-recoverable
Thomas A Caswell
ENH: add support for Qt6 input hooks...
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
STY: apply darker
r26684 QtGuiCompat = types.ModuleType("QtGuiCompat")
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 QtGuiCompat.__dict__.update(QtGui.__dict__)
QtGuiCompat.__dict__.update(QtWidgets.__dict__)
api = QT_API_PYQT6
return QtCore, QtGuiCompat, QtSvg, api
Peter Würtz
Add support for PyQt5.
r16414
Chris Beaumont
Refactor qt import logic. Fixes #2955
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
Copy across PySide2 support from Qtconsole
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
STY: apply darker
r26684
Thomas A Caswell
ENH: add support for Qt6 input hooks...
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
STY: apply darker
r26684 QtGuiCompat = types.ModuleType("QtGuiCompat")
Thomas A Caswell
ENH: add support for Qt6 input hooks...
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
Refactor qt import logic. Fixes #2955
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
reformat rest of ipython
r27295 api_options : List of strings
Thomas Kluyver
Copy across PySide2 support from Qtconsole
r23163 The order of APIs to try. Valid items are 'pyside', 'pyside2',
Thomas Kluyver
Fix Qt loader commit_api() for 'pyqtv1' or 'pyqtdefault'...
r18405 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
Chris Beaumont
Refactor qt import logic. Fixes #2955
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
Fixed typos
r25167 bindings (either because they aren't installed, or because
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 an incompatible library has already been installed)
"""
Thomas Kluyver
Copy across PySide2 support from Qtconsole
r23163 loaders = {
Thomas A Caswell
ENH: add support for Qt6 input hooks...
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
Refactor qt import logic. Fixes #2955
r9722
for api in api_options:
if api not in loaders:
raise RuntimeError(
Peter Würtz
Add support for PyQt5.
r16414 "Invalid Qt API %r, valid values are: %s" %
(api, ", ".join(["%r" % k for k in loaders.keys()])))
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
if not can_import(api):
continue
#cannot safely recover from an ImportError during this
result = loaders[api]()
Chris Beaumont
added QT_API_DEFAULT Qt option. Fixed reversed MPL qt logic test.
r9815 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 commit_api(api)
return result
else:
Emilio Graff
Formatting
r27994 raise ImportError(
"""
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722 Could not load requested Qt binding. Please ensure that
Emilio Graff
Fix typo
r27997 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
PySide6 is available, and only one is imported per session.
Chris Beaumont
Refactor qt import logic. Fixes #2955
r9722
Jan-Philip Gehrcke
external/qt_loaders: list required Qt modules in import error message
r21879 Currently-imported Qt library: %r
PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
Emilio Graff
Update error message to include `PyQt6`
r27988 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
Thomas Kluyver
Copy across PySide2 support from Qtconsole
r23163 PySide2 installed: %s
Emilio Graff
Add `PySide6` to list of installed Qt versions
r27985 PySide6 installed: %s
Jan-Philip Gehrcke
external/qt_loaders: list required Qt modules in import error message
r21879 Tried to load: %r
Emilio Graff
Formatting
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
ENH: add support for Qt6 input hooks...
r26683
Emilio Graff
More formatting
r27995
Thomas A Caswell
ENH: add support for Qt6 input hooks...
r26683 def enum_factory(QT_API, QtCore):
"""Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
Thomas A Caswell
STY: apply darker
r26684
Thomas A Caswell
ENH: add support for Qt6 input hooks...
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