summaryrefslogtreecommitdiff
path: root/qutebrowser/qt/machinery.py
blob: 1eef24f0b49fa1d15bb31af02eeb9bfbcbd20d97 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# pyright: reportConstantRedefinition=false

"""Qt wrapper selection.

Contains selection logic and globals for Qt wrapper selection.
"""

import os
import sys
import argparse
import importlib
from typing import Union

# Packagers: Patch the line below to change the default wrapper for Qt 6 packages, e.g.:
# sed -i 's/_DEFAULT_WRAPPER = "PyQt5"/_DEFAULT_WRAPPER = "PyQt6"/' qutebrowser/qt/machinery.py
#
# Users: Set the QUTE_QT_WRAPPER environment variable to change the default wrapper.
_DEFAULT_WRAPPER = "PyQt5"

WRAPPERS = [
    "PyQt6",
    "PyQt5",
    # Needs more work
    # "PySide6",
]


class Error(Exception):
    """Base class for all exceptions in this module."""


class Unavailable(Error, ImportError):

    """Raised when a module is unavailable with the given wrapper."""

    def __init__(self) -> None:
        super().__init__(f"Unavailable with {WRAPPER}")


class UnknownWrapper(Error):
    """Raised when an Qt module is imported but the wrapper values are unknown.

    Should never happen (unless a new wrapper is added).
    """


def _autoselect_wrapper() -> str:
    """Autoselect a Qt wrapper.

    This goes through all wrappers defined in WRAPPER.
    The first one which can be imported is returned.
    """
    for wrapper in WRAPPERS:
        try:
            importlib.import_module(wrapper)
        except ImportError:
            # FIXME:qt6 show/log this somewhere?
            continue
        return wrapper

    wrappers = ", ".join(WRAPPERS)
    raise Error(f"No Qt wrapper found, tried {wrappers}")


def _select_wrapper(args: Union[argparse.Namespace, None]) -> str:
    """Select a Qt wrapper.

    - If --qt-wrapper is given, use that.
    - Otherwise, if the QUTE_QT_WRAPPER environment variable is set, use that.
    - Otherwise, use PyQt5 (FIXME:qt6 autoselect).
    """
    if args is not None and args.qt_wrapper is not None:
        assert args.qt_wrapper in WRAPPERS, args.qt_wrapper  # ensured by argparse
        return args.qt_wrapper

    env_var = "QUTE_QT_WRAPPER"
    env_wrapper = os.environ.get(env_var)
    if env_wrapper is not None:
        if env_wrapper not in WRAPPERS:
            raise Error(f"Unknown wrapper {env_wrapper} set via {env_var}, "
                        f"allowed: {', '.join(WRAPPERS)}")
        return env_wrapper

    # FIXME:qt6 Go back to the auto-detection once ready
    # return _autoselect_wrapper()
    return _DEFAULT_WRAPPER


# Values are set in init(). If you see a NameError here, it means something tried to
# import Qt (or check for its availability) before machinery.init() was called.
WRAPPER: str
USE_PYQT5: bool
USE_PYQT6: bool
USE_PYSIDE6: bool
IS_QT5: bool
IS_QT6: bool
IS_PYQT: bool
IS_PYSIDE: bool
PACKAGE: str

_initialized = False


def init(args: Union[argparse.Namespace, None] = None) -> None:
    """Initialize Qt wrapper globals.

    There is two ways how this function can be called:

    - Explicitly, during qutebrowser startup, where it gets called before
      earlyinit.early_init() in qutebrowser.py (i.e. after we have an argument
      parser, but before any kinds of Qt usage). This allows `args` to be passed,
      which is used to select the Qt wrapper (if --qt-wrapper is given).

    - Implicitly, when any of the qutebrowser.qt.* modules in this package is imported.
      This should never happen during normal qutebrowser usage, but means that any
      qutebrowser module can be imported without having to worry about machinery.init().
      This is useful for e.g. tests or manual interactive usage of the qutebrowser code.
      In this case, `args` will be None.
    """
    global WRAPPER, USE_PYQT5, USE_PYQT6, USE_PYSIDE6, IS_QT5, IS_QT6, \
        IS_PYQT, IS_PYSIDE, PACKAGE, _initialized

    if args is None:
        # Implicit initialization can happen multiple times
        # (all subsequent calls are a no-op)
        if _initialized:
            return
    else:
        # Explicit initialization can happen exactly once, and if it's used, there
        # should not be any implicit initialization (qutebrowser.qt imports) before it.
        assert not _initialized, "init() already called before application init"
    _initialized = True

    for name in WRAPPERS:
        # If any Qt wrapper has been imported before this, all hope is lost.
        assert name not in sys.modules, f"{name} already imported"

    WRAPPER = _select_wrapper(args)
    USE_PYQT5 = WRAPPER == "PyQt5"
    USE_PYQT6 = WRAPPER == "PyQt6"
    USE_PYSIDE6 = WRAPPER == "PySide6"
    assert USE_PYQT5 ^ USE_PYQT6 ^ USE_PYSIDE6

    IS_QT5 = USE_PYQT5
    IS_QT6 = USE_PYQT6 or USE_PYSIDE6
    IS_PYQT = USE_PYQT5 or USE_PYQT6
    IS_PYSIDE = USE_PYSIDE6
    assert IS_QT5 ^ IS_QT6
    assert IS_PYQT ^ IS_PYSIDE

    if USE_PYQT5:
        PACKAGE = "PyQt5"
    elif USE_PYQT6:
        PACKAGE = "PyQt6"
    elif USE_PYSIDE6:
        PACKAGE = "PySide6"