Source code for taurus.external.qt

# -*- 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 # --------------------------------------------------------------------------