374 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			374 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| """
 | |
| 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
 | |
| import types
 | |
| from functools import partial
 | |
| from importlib import import_module
 | |
| 
 | |
| from yap_ipython.utils.version import check_version
 | |
| 
 | |
| # Available APIs.
 | |
| QT_API_PYQT = 'pyqt' # Force version 2
 | |
| QT_API_PYQT5 = 'pyqt5'
 | |
| QT_API_PYQTv1 = 'pyqtv1' # Force version 2
 | |
| QT_API_PYQT_DEFAULT = 'pyqtdefault' # use system default for version 1 vs. 2
 | |
| QT_API_PYSIDE = 'pyside'
 | |
| 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',
 | |
|                 }
 | |
| 
 | |
| 
 | |
| class ImportDenier(object):
 | |
|     """Import Hook that will guard against bad Qt imports
 | |
|     once yap_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_module(self, fullname, path=None):
 | |
|         if path:
 | |
|             return
 | |
|         if fullname in self.__forbidden:
 | |
|             return self
 | |
| 
 | |
|     def load_module(self, fullname):
 | |
|         raise ImportError("""
 | |
|     Importing %s disabled by yap_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"""
 | |
| 
 | |
|     if api == QT_API_PYSIDE2:
 | |
|         ID.forbid('PySide')
 | |
|         ID.forbid('PyQt4')
 | |
|         ID.forbid('PyQt5')
 | |
|     if api == QT_API_PYSIDE:
 | |
|         ID.forbid('PySide2')
 | |
|         ID.forbid('PyQt4')
 | |
|         ID.forbid('PyQt5')
 | |
|     elif api == QT_API_PYQT5:
 | |
|         ID.forbid('PySide2')
 | |
|         ID.forbid('PySide')
 | |
|         ID.forbid('PyQt4')
 | |
|     else:   # There are three other possibilities, all representing PyQt4
 | |
|         ID.forbid('PyQt5')
 | |
|         ID.forbid('PySide2')
 | |
|         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
 | |
|     -------
 | |
|     None, 'pyside2', 'pyside', 'pyqt', 'pyqt5', or 'pyqtv1'
 | |
|     """
 | |
|     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
 | |
|     elif 'PySide2.QtCore' in sys.modules:
 | |
|         return QT_API_PYSIDE2
 | |
|     elif 'PyQt5.QtCore' in sys.modules:
 | |
|         return QT_API_PYQT5
 | |
|     return None
 | |
| 
 | |
| 
 | |
| def has_binding(api):
 | |
|     """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
 | |
| 
 | |
|     Supports Python <= 3.3
 | |
| 
 | |
|        Parameters
 | |
|        ----------
 | |
|        api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
 | |
|             Which module to check for
 | |
| 
 | |
|        Returns
 | |
|        -------
 | |
|        True if the relevant module appears to be importable
 | |
|     """
 | |
|     # we can't import an incomplete pyside and pyqt4
 | |
|     # this will cause a crash in sip (#1431)
 | |
|     # check for complete presence before importing
 | |
|     module_name = api_to_module[api]
 | |
| 
 | |
|     import imp
 | |
|     try:
 | |
|         #importing top level PyQt4/PySide module is ok...
 | |
|         mod = import_module(module_name)
 | |
|         #...importing submodules is not
 | |
|         imp.find_module('QtCore', mod.__path__)
 | |
|         imp.find_module('QtGui', mod.__path__)
 | |
|         imp.find_module('QtSvg', mod.__path__)
 | |
|         if api in (QT_API_PYQT5, QT_API_PYSIDE2):
 | |
|             # QT5 requires QtWidgets too
 | |
|             imp.find_module('QtWidgets', mod.__path__)
 | |
| 
 | |
|         #we can also safely check PySide version
 | |
|         if api == QT_API_PYSIDE:
 | |
|             return check_version(mod.__version__, '1.0.3')
 | |
|         else:
 | |
|             return True
 | |
|     except ImportError:
 | |
|         return False
 | |
| 
 | |
| def has_binding_new(api):
 | |
|     """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
 | |
| 
 | |
|     Supports Python >= 3.4
 | |
| 
 | |
|         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):
 | |
|         # 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
 | |
| 
 | |
| if sys.version_info >= (3, 4):
 | |
|     has_binding = has_binding_new
 | |
| 
 | |
| 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:
 | |
|         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_PYQT, QT_API_PYQTv1, 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 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
 | |
| 
 | |
|     if version is not None:
 | |
|         sip.setapi('QString', version)
 | |
|         sip.setapi('QVariant', version)
 | |
| 
 | |
|     from PyQt4 import QtGui, QtCore, QtSvg
 | |
| 
 | |
|     if not check_version(QtCore.PYQT_VERSION_STR, '4.7'):
 | |
|         raise ImportError("yap_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 rasied within this function are non-recoverable
 | |
|     """
 | |
|     import sip
 | |
| 
 | |
|     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_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 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 becaues they aren't installed, or because
 | |
|     an incompatible library has already been installed)
 | |
|     """
 | |
|     loaders = {
 | |
|                QT_API_PYSIDE2: import_pyside2,
 | |
|                QT_API_PYSIDE: import_pyside,
 | |
|                QT_API_PYQT: import_pyqt4,
 | |
|                QT_API_PYQT5: import_pyqt5,
 | |
|                QT_API_PYQTv1: partial(import_pyqt4, version=1),
 | |
|                QT_API_PYQT_DEFAULT: partial(import_pyqt4, version=None)
 | |
|               }
 | |
| 
 | |
|     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:
 | |
|         raise ImportError("""
 | |
|     Could not load requested Qt binding. Please ensure that
 | |
|     PyQt4 >= 4.7, PyQt5, PySide >= 1.0.3 or PySide2 is available,
 | |
|     and only one is imported per session.
 | |
| 
 | |
|     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
 | |
|     PySide2 installed:                                          %s
 | |
|     Tried to load:                                              %r
 | |
|     """ % (loaded_api(),
 | |
|            has_binding(QT_API_PYQT),
 | |
|            has_binding(QT_API_PYQT5),
 | |
|            has_binding(QT_API_PYSIDE),
 | |
|            has_binding(QT_API_PYSIDE2),
 | |
|            api_options))
 |