# -*- coding: utf-8 -*-
# ###########################################################################
#
# This file is part of Taurus
#
# http://taurus-scada.org
#
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
#
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Taurus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus. If not, see <http://www.gnu.org/licenses/>.
#
# ###########################################################################
"""This module exposes PyQt4/PyQt5/PySide module
"""
__all__ = ["initialize", "API_NAME", "getQtName", "getQt", "requires"]
import os
import sys
import platform
from packaging.version import Version
import taurus.core.util.log as __log
from taurus import tauruscustomsettings as __config
class PythonQtError(RuntimeError):
"""Error raised if no bindings could be selected."""
pass
#: Qt API environment variable name
QT_API = "QT_API"
#: names of the PyQt5 api
PYQT5_API = ["pyqt5"]
#: names of the PyQt4 api (not supported in taurus >=5)
PYQT4_API = [
"pyqt", # name used in IPython.qt
"pyqt4", # pyqode.qt original name
]
#: names of the PySide api (not supported since taurus>=5)
PYSIDE_API = ["pyside"]
#: names of the PySide2 api
PYSIDE2_API = ["pyside2"]
#: The constants PYQT5, PYSIDE2, PYSIDE_VERSION and PYQT_VERSION
#: will be updated depending on the selected binding
PYQT5 = PYSIDE2 = False
PYSIDE_VERSION = PYQT_VERSION = None
#: Note: PYQT4 and PYSIDE are declared for backwards-compatibility but
#: are always False in taurus>=5
PYQT4 = PYSIDE = False
# First, check if some binding is already in use (and, if so, select it)
if "PyQt5" in sys.modules:
API = "pyqt5"
elif "PySide2" in sys.modules:
API = "pyside2"
else:
# if no binding is already loaded, use (in this order):
# - QT_API environment variable
# - tauruscustomsettings.DEFAULT_QT_API
# - first successful import of 'pyqt5', 'pyside2'
API = os.environ.get(QT_API, getattr(__config, "DEFAULT_QT_API", ""))
API = API.lower()
if API not in (PYQT5_API + PYSIDE2_API + [""]):
raise ImportError("Unknown Qt API '{}'".format(API))
if not API or API in PYQT5_API:
try:
from PyQt5.QtCore import PYQT_VERSION_STR as PYQT_VERSION
from PyQt5.QtCore import QT_VERSION_STR as QT_VERSION
PYSIDE_VERSION = None
PYQT4 = PYSIDE = PYSIDE2 = False
PYQT5 = True
API = os.environ["QT_API"] = "pyqt5"
if sys.platform == "darwin":
macos_version = Version(platform.mac_ver()[0])
if macos_version < Version("10.10"):
if Version(QT_VERSION) >= Version("5.9"):
raise PythonQtError(
"Qt 5.9 or higher only works in "
+ "macOS 10.10 or higher. Your "
+ "program will fail in this "
+ "system."
)
elif macos_version < Version("10.11"):
if Version(QT_VERSION) >= Version("5.11"):
raise PythonQtError(
"Qt 5.11 or higher only works in "
+ "macOS 10.11 or higher. Your "
+ "program will fail in this "
+ "system."
)
del macos_version
except ImportError:
if API: # if an specific API was requested, fail with import error
raise ImportError("Cannot import PyQt5")
# if no specific API was requested, allow trying other bindings
__log.debug("Cannot import PyQt5")
if not API or API in PYSIDE2_API:
try:
from PySide2 import __version__ as PYSIDE_VERSION # analysis:ignore
from PySide2.QtCore import __version__ as QT_VERSION # analysis:ignore
PYQT_VERSION = None # noqa: F811
PYQT5 = PYQT4 = PYSIDE = False
PYSIDE2 = True
API = os.environ["QT_API"] = "pyside2"
if sys.platform == "darwin":
macos_version = Version(platform.mac_ver()[0])
if macos_version < Version("10.11"):
if Version(QT_VERSION) >= Version("5.11"):
raise PythonQtError(
"Qt 5.11 or higher only works in "
+ "macOS 10.11 or higher. Your "
+ "program will fail in this "
+ "system."
)
del macos_version
except ImportError:
if API: # if an specific API was requested, fail with import error
raise ImportError("Cannot import PySide2")
# if no specific API was requested, allow trying other bindings
__log.debug("Cannot import PySide2")
if not API:
raise ImportError("No Qt bindings could be imported")
API_NAME = {"pyqt5": "PyQt5", "pyside2": "PySide2"}[API]
# Update the environment so that other libraries that also use the same
# convention (such as guidata or spyder) do a consistent choice
os.environ["QT_API"] = API
def __initializeQtLogging():
from importlib import import_module
QtCore = import_module(API_NAME + ".QtCore")
QT_LEVEL_MATCHER = {
QtCore.QtDebugMsg: __log.debug,
QtCore.QtWarningMsg: __log.warning,
QtCore.QtCriticalMsg: __log.critical,
QtCore.QtFatalMsg: __log.fatal,
QtCore.QtSystemMsg: __log.critical,
}
if hasattr(QtCore, "QtInfoMsg"):
QT_LEVEL_MATCHER[QtCore.QtInfoMsg] = __log.info
if hasattr(QtCore, "qInstallMessageHandler"):
# Qt5
def taurusMessageHandler(msg_type, log_ctx, msg):
f = QT_LEVEL_MATCHER.get(msg_type)
return f(
"Qt%s %s.%s[%s]: %s",
log_ctx.category,
log_ctx.file,
log_ctx.function,
log_ctx.line,
msg,
)
QtCore.qInstallMessageHandler(taurusMessageHandler)
def __removePyQtInputHook():
from importlib import import_module
QtCore = import_module(API_NAME + ".QtCore")
if hasattr(QtCore, "pyqtRemoveInputHook"):
QtCore.pyqtRemoveInputHook()
def __addExceptHook():
"""
Since PyQt 5.5 , unhandled python exceptions cause the application to
abort:
http://pyqt.sf.net/Docs/PyQt5/incompatibilities.html#unhandled-python-exceptions
By calling __addExceptHook, we restore the old behaviour (just print the
exception trace).
"""
import traceback
sys.excepthook = traceback.print_exception
if getattr(__config, "QT_AUTO_INIT_LOG", True):
__initializeQtLogging()
if getattr(__config, "QT_AUTO_REMOVE_INPUTHOOK", True):
__removePyQtInputHook()
if PYQT5 and getattr(__config, "QT_AVOID_ABORT_ON_EXCEPTION", True):
# TODO: check if we also want to do this for PySide(2)
__addExceptHook()
__log.info(
"Using %s (v%s with Qt %s and Python %s)",
API_NAME,
PYQT_VERSION or PYSIDE_VERSION,
QT_VERSION,
sys.version.split()[0],
)
# --------------------------------------------------------------------------
# Deprecated (in Jul17) pending to be removed later on
[docs]
def getQt(name=None, strict=True):
__log.deprecated(dep="taurus.external.qt.getQt", rel="4.0.4")
from importlib import import_module
return import_module(API_NAME)
[docs]
def getQtName(name=None, strict=True):
__log.deprecated(
dep="taurus.external.qt.getQtName",
alt="taurus.external.qt.API_NAME",
rel="4.0.4",
)
return API_NAME
[docs]
def initialize(
name=None, strict=True, logging=True, resources=True, remove_inputhook=True
):
__log.deprecated(dep="taurus.external.qt.initialize", rel="4.0.4")
return getQt()
[docs]
def requires(origin=None, exc_class=ImportError, **kwargs):
__log.deprecated(dep="taurus.external.qt.requires", rel="4.0.4")
return True
# Handle rename of DEFAULT_QT_AUTO_API --> DEFAULT_QT_API
if hasattr(__config, "DEFAULT_QT_AUTO_API"):
__log.deprecated(
dep="DEFAULT_QT_AUTO_API", alt="DEFAULT_QT_API", rel="4.0.4"
)
if not hasattr(__config, "DEFAULT_QT_API"):
__config.DEFAULT_QT_API = __config.DEFAULT_QT_AUTO_API
# --------------------------------------------------------------------------