From 83bef2ad4bdc10113bb9e5ed12c32d92bc1247af Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 13 Jun 2023 12:09:48 +0200 Subject: qt: Add machinery.SelectionInfo --- qutebrowser/misc/earlyinit.py | 5 ++-- qutebrowser/qt/machinery.py | 63 ++++++++++++++++++++++++++++++---------- qutebrowser/utils/version.py | 2 ++ tests/conftest.py | 4 +-- tests/unit/test_qt_machinery.py | 10 ++++--- tests/unit/utils/test_version.py | 7 +++++ 6 files changed, 67 insertions(+), 24 deletions(-) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 288d42f77..08d0af474 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -140,7 +140,8 @@ def check_pyqt(): """Check if PyQt core modules (QtCore/QtWidgets) are installed.""" from qutebrowser.qt import machinery - packages = [f'{machinery.WRAPPER}.QtCore', f'{machinery.WRAPPER}.QtWidgets'] + wrapper = machinery.INFO.wrapper + packages = [f'{wrapper}.QtCore', f'{wrapper}.QtWidgets'] for name in packages: try: importlib.import_module(name) @@ -247,7 +248,7 @@ def check_libraries(): } for subpkg in ['QtQml', 'QtOpenGL', 'QtDBus']: - package = f'{machinery.WRAPPER}.{subpkg}' + package = f'{machinery.INFO.wrapper}.{subpkg}' modules[package] = _missing_str(package) if sys.version_info < (3, 9): diff --git a/qutebrowser/qt/machinery.py b/qutebrowser/qt/machinery.py index 255eb24af..eb943b4fc 100644 --- a/qutebrowser/qt/machinery.py +++ b/qutebrowser/qt/machinery.py @@ -10,6 +10,7 @@ import os import sys import argparse import importlib +import dataclasses from typing import Optional # Packagers: Patch the line below to change the default wrapper for Qt 6 packages, e.g.: @@ -35,7 +36,7 @@ class Unavailable(Error, ImportError): """Raised when a module is unavailable with the given wrapper.""" def __init__(self) -> None: - super().__init__(f"Unavailable with {WRAPPER}") + super().__init__(f"Unavailable with {INFO.wrapper}") class UnknownWrapper(Error): @@ -45,25 +46,53 @@ class UnknownWrapper(Error): """ -def _autoselect_wrapper() -> str: +@dataclasses.dataclass +class SelectionInfo: + """Information about outcomes of importing Qt wrappers.""" + + pyqt5: str = "not tried" + pyqt6: str = "not tried" + wrapper: Optional[str] = None + reason: Optional[str] = None + + def set_module(self, name: str, outcome: str) -> None: + """Set the outcome for a module import.""" + setattr(self, name.lower(), outcome) + + def __str__(self) -> str: + return ( + "Qt wrapper:\n" + f"PyQt5: {self.pyqt5}\n" + f"PyQt6: {self.pyqt6}\n" + f"selected: {self.wrapper} (via {self.reason})" + ) + + +def _autoselect_wrapper() -> SelectionInfo: """Autoselect a Qt wrapper. This goes through all wrappers defined in WRAPPER. The first one which can be imported is returned. """ + info = SelectionInfo(reason="autoselect") + for wrapper in WRAPPERS: try: importlib.import_module(wrapper) - except ImportError: - # FIXME:qt6 show/log this somewhere? + except ImportError as e: + info.set_module(wrapper, str(e)) continue - return wrapper + info.set_module(wrapper, "success") + info.wrapper = wrapper + return info + + # FIXME return a SelectionInfo here instead so we can handle this in earlyinit? wrappers = ", ".join(WRAPPERS) raise Error(f"No Qt wrapper found, tried {wrappers}") -def _select_wrapper(args: Optional[argparse.Namespace]) -> str: +def _select_wrapper(args: Optional[argparse.Namespace]) -> SelectionInfo: """Select a Qt wrapper. - If --qt-wrapper is given, use that. @@ -72,7 +101,7 @@ def _select_wrapper(args: Optional[argparse.Namespace]) -> str: """ 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 + return SelectionInfo(wrapper=args.qt_wrapper, reason="--qt-wrapper") env_var = "QUTE_QT_WRAPPER" env_wrapper = os.environ.get(env_var) @@ -80,20 +109,22 @@ def _select_wrapper(args: Optional[argparse.Namespace]) -> str: if env_wrapper not in WRAPPERS: raise Error(f"Unknown wrapper {env_wrapper} set via {env_var}, " f"allowed: {', '.join(WRAPPERS)}") - return env_wrapper + return SelectionInfo(wrapper=env_wrapper, reason="QUTE_QT_WRAPPER") # FIXME:qt6 Go back to the auto-detection once ready + # FIXME:qt6 Make sure to still consider _DEFAULT_WRAPPER for packagers + # (rename to _WRAPPER_OVERRIDE since our sed command is broken anyways then?) # return _autoselect_wrapper() - return _DEFAULT_WRAPPER + return SelectionInfo(wrapper=_DEFAULT_WRAPPER, reason="default") # 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. -#: The name of the wrapper to be used, one of WRAPPERS. +#: Information about the wrapper that ended up being selected. #: Should not be used directly, use one of the USE_* or IS_* constants below #: instead, as those are supported by type checking. -WRAPPER: str +INFO: SelectionInfo #: Whether we're using PyQt5. Consider using IS_QT5 or IS_PYQT instead. USE_PYQT5: bool @@ -135,7 +166,7 @@ def init(args: Optional[argparse.Namespace] = None) -> None: 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, \ + global INFO, USE_PYQT5, USE_PYQT6, USE_PYSIDE6, IS_QT5, IS_QT6, \ IS_PYQT, IS_PYSIDE, _initialized if args is None: @@ -155,10 +186,10 @@ def init(args: Optional[argparse.Namespace] = None) -> None: if name in sys.modules: raise Error(f"{name} already imported") - WRAPPER = _select_wrapper(args) - USE_PYQT5 = WRAPPER == "PyQt5" - USE_PYQT6 = WRAPPER == "PyQt6" - USE_PYSIDE6 = WRAPPER == "PySide6" + INFO = _select_wrapper(args) + USE_PYQT5 = INFO.wrapper == "PyQt5" + USE_PYQT6 = INFO.wrapper == "PyQt6" + USE_PYSIDE6 = INFO.wrapper == "PySide6" assert USE_PYQT5 ^ USE_PYQT6 ^ USE_PYSIDE6 IS_QT5 = USE_PYQT5 diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 045c2478e..0cebead58 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -882,6 +882,8 @@ def version_info() -> str: platform.python_version()), 'PyQt: {}'.format(PYQT_VERSION_STR), '', + str(machinery.INFO), + '', ] lines += _module_versions() diff --git a/tests/conftest.py b/tests/conftest.py index 622286a54..0da3a1f81 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -116,11 +116,11 @@ def _apply_platform_markers(config, item): ('qt5_only', pytest.mark.skipif, not machinery.IS_QT5, - f"Only runs on Qt 5, not {machinery.WRAPPER}"), + f"Only runs on Qt 5, not {machinery.INFO.wrapper}"), ('qt6_only', pytest.mark.skipif, not machinery.IS_QT6, - f"Only runs on Qt 6, not {machinery.WRAPPER}"), + f"Only runs on Qt 6, not {machinery.INFO.wrapper}"), ('qt5_xfail', pytest.mark.xfail, machinery.IS_QT5, "Fails on Qt 5"), ('qt6_xfail', pytest.mark.skipif, machinery.IS_QT6, "Fails on Qt 6"), ('qtwebkit_openssl3_skip', diff --git a/tests/unit/test_qt_machinery.py b/tests/unit/test_qt_machinery.py index 7ae5576d1..00c42233e 100644 --- a/tests/unit/test_qt_machinery.py +++ b/tests/unit/test_qt_machinery.py @@ -152,17 +152,19 @@ def test_init_properly( "IS_PYQT", "IS_PYSIDE", ] + all_vars = bool_vars + ["INFO"] # Make sure we didn't forget anything that's declared in the module. # Not sure if this is a good idea. Might remove it in the future if it breaks. - assert set(typing.get_type_hints(machinery).keys()) == set(bool_vars) | {"WRAPPER"} + assert set(typing.get_type_hints(machinery).keys()) == set(all_vars) - for var in ["WRAPPER"] + bool_vars: + for var in all_vars: monkeypatch.delattr(machinery, var) - monkeypatch.setattr(machinery, "_select_wrapper", lambda args: selected_wrapper) + info = machinery.SelectionInfo(wrapper=selected_wrapper, reason="fake") + monkeypatch.setattr(machinery, "_select_wrapper", lambda args: info) machinery.init() - assert machinery.WRAPPER == selected_wrapper + assert machinery.INFO == info expected_vars = dict.fromkeys(bool_vars, False) expected_vars.update(dict.fromkeys(true_vars, True)) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 056cd8e4c..4d42c5dc1 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -33,6 +33,7 @@ import dataclasses import pytest import hypothesis import hypothesis.strategies +from qutebrowser.qt import machinery from qutebrowser.qt.core import PYQT_VERSION_STR import qutebrowser @@ -1269,6 +1270,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub): 'sql.version': lambda: 'SQLITE VERSION', '_uptime': lambda: datetime.timedelta(hours=1, minutes=23, seconds=45), 'config.instance.yaml_loaded': params.autoconfig_loaded, + 'machinery.INFO': machinery.SelectionInfo(wrapper="QT WRAPPER", reason="fake"), } version.opengl_info.cache_clear() @@ -1340,6 +1342,11 @@ def test_version_info(params, stubs, monkeypatch, config_stub): PYTHON IMPLEMENTATION: PYTHON VERSION PyQt: PYQT VERSION + Qt wrapper: + PyQt5: not tried + PyQt6: not tried + selected: QT WRAPPER (via fake) + MODULE VERSION 1 MODULE VERSION 2 pdf.js: PDFJS VERSION -- cgit v1.2.3-54-g00ecf