From 5616a99eff34f7074641d1391ed77d6b4b743529 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 14 Jun 2022 15:26:36 +0200 Subject: Add a MessageInfo data class Preparation for #7246 --- qutebrowser/mainwindow/messageview.py | 34 +++++++++-------- qutebrowser/utils/message.py | 51 +++++++++++++++++++------- tests/helpers/messagemock.py | 24 ++++-------- tests/unit/mainwindow/test_messageview.py | 61 ++++++++++++++++++------------- 4 files changed, 100 insertions(+), 70 deletions(-) diff --git a/qutebrowser/mainwindow/messageview.py b/qutebrowser/mainwindow/messageview.py index 58fcb3683..482d9f5e7 100644 --- a/qutebrowser/mainwindow/messageview.py +++ b/qutebrowser/mainwindow/messageview.py @@ -25,7 +25,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QTimer, Qt from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel, QSizePolicy from qutebrowser.config import config, stylesheet -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, message class Message(QLabel): @@ -74,6 +74,15 @@ class Message(QLabel): raise ValueError("Invalid level {!r}".format(level)) stylesheet.set_register(self, qss, update=False) + @classmethod + def from_info(cls, info: message.MessageInfo, parent: QWidget = None) -> "Message": + return cls( + level=info.level, + text=info.text, + replace=info.replace, + parent=parent, + ) + class MessageView(QWidget): @@ -119,31 +128,26 @@ class MessageView(QWidget): self.hide() self._clear_timer.stop() - @pyqtSlot(usertypes.MessageLevel, str, str) - def show_message( - self, - level: usertypes.MessageLevel, - text: str, - replace: str = None, - ) -> None: + @pyqtSlot(message.MessageInfo) + def show_message(self, info: message.MessageInfo) -> None: """Show the given message with the given MessageLevel.""" - if text == self._last_text: + if info.text == self._last_text: return - if replace: # None -> QString() -> '' - existing = [msg for msg in self._messages if msg.replace == replace] + if info.replace is not None: + existing = [msg for msg in self._messages if msg.replace == info.replace] if existing: assert len(existing) == 1, existing - assert existing[0].level == level, (existing, level) - existing[0].setText(text) + assert existing[0].level == info.level, (existing, info) + existing[0].setText(info.text) self.update_geometry.emit() return - widget = Message(level, text, replace=replace, parent=self) + widget = Message.from_info(info) self._vbox.addWidget(widget) widget.show() self._messages.append(widget) - self._last_text = text + self._last_text = info.text self.show() self.update_geometry.emit() if config.val.messages.timeout != 0: diff --git a/qutebrowser/utils/message.py b/qutebrowser/utils/message.py index c490aa4e8..48dcc9945 100644 --- a/qutebrowser/utils/message.py +++ b/qutebrowser/utils/message.py @@ -23,14 +23,23 @@ """Message singleton so we don't have to define unneeded signals.""" +import dataclasses import traceback -from typing import Any, Callable, Iterable, List, Tuple, Union, Optional +from typing import Any, Callable, Iterable, List, Union, Optional from PyQt5.QtCore import pyqtSignal, pyqtBoundSignal, QObject from qutebrowser.utils import usertypes, log +@dataclasses.dataclass +class MessageInfo: + + level: usertypes.MessageLevel + text: str + replace: Optional[str] = None + + def _log_stack(typ: str, stack: str) -> None: """Log the given message stacktrace. @@ -58,7 +67,11 @@ def error(message: str, *, stack: str = None, replace: str = None) -> None: typ = 'error (from exception)' _log_stack(typ, stack) log.message.error(message) - global_bridge.show(usertypes.MessageLevel.error, message, replace) + global_bridge.show( + level=usertypes.MessageLevel.error, + text=message, + replace=replace, + ) def warning(message: str, *, replace: str = None) -> None: @@ -70,7 +83,11 @@ def warning(message: str, *, replace: str = None) -> None: """ _log_stack('warning', ''.join(traceback.format_stack())) log.message.warning(message) - global_bridge.show(usertypes.MessageLevel.warning, message, replace) + global_bridge.show( + level=usertypes.MessageLevel.warning, + text=message, + replace=replace, + ) def info(message: str, *, replace: str = None) -> None: @@ -81,7 +98,11 @@ def info(message: str, *, replace: str = None) -> None: replace: Replace existing messages which are still being shown. """ log.message.info(message) - global_bridge.show(usertypes.MessageLevel.info, message, replace) + global_bridge.show( + level=usertypes.MessageLevel.info, + text=message, + replace=replace, + ) def _build_question(title: str, @@ -210,7 +231,7 @@ class GlobalMessageBridge(QObject): mode_left: Emitted when a keymode was left in any window. """ - show_message = pyqtSignal(usertypes.MessageLevel, str, str) + show_message = pyqtSignal(MessageInfo) prompt_done = pyqtSignal(usertypes.KeyMode) ask_question = pyqtSignal(usertypes.Question, bool) mode_left = pyqtSignal(usertypes.KeyMode) @@ -219,7 +240,7 @@ class GlobalMessageBridge(QObject): def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._connected = False - self._cache: List[Tuple[usertypes.MessageLevel, str, Optional[str]]] = [] + self._cache: List[MessageInfo] = [] def ask(self, question: usertypes.Question, blocking: bool, *, @@ -237,14 +258,18 @@ class GlobalMessageBridge(QObject): """ self.ask_question.emit(question, blocking) - def show(self, level: usertypes.MessageLevel, - text: str, - replace: str = None) -> None: + def show( + self, + level: usertypes.MessageLevel, + text: str, + replace: str = None, + ) -> None: """Show the given message.""" + info = MessageInfo(level=level, text=text, replace=replace) if self._connected: - self.show_message.emit(level, text, replace) + self.show_message.emit(info) else: - self._cache.append((level, text, replace)) + self._cache.append(info) def flush(self) -> None: """Flush messages which accumulated while no handler was connected. @@ -253,8 +278,8 @@ class GlobalMessageBridge(QObject): It needs to be called once the show_message signal is connected. """ self._connected = True - for args in self._cache: - self.show(*args) + for info in self._cache: + self.show(**dataclasses.asdict(info)) self._cache = [] diff --git a/tests/helpers/messagemock.py b/tests/helpers/messagemock.py index 41f0bac64..6f020618b 100644 --- a/tests/helpers/messagemock.py +++ b/tests/helpers/messagemock.py @@ -20,7 +20,6 @@ """pytest helper to monkeypatch the message module.""" import logging -import dataclasses import pytest from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject @@ -28,15 +27,6 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject from qutebrowser.utils import usertypes, message -@dataclasses.dataclass -class Message: - - """Information about a shown message.""" - - level: usertypes.MessageLevel - text: str - - class MessageMock(QObject): """Helper object for message_mock. @@ -47,7 +37,7 @@ class MessageMock(QObject): _logger: The logger to use for messages/questions. """ - got_message = pyqtSignal(usertypes.MessageLevel, str) + got_message = pyqtSignal(message.MessageInfo) got_question = pyqtSignal(usertypes.Question) def __init__(self, parent=None): @@ -56,18 +46,18 @@ class MessageMock(QObject): self.questions = [] self._logger = logging.getLogger('messagemock') - @pyqtSlot(usertypes.MessageLevel, str) - def _record_message(self, level, text): - self.got_message.emit(level, text) + @pyqtSlot(message.MessageInfo) + def _record_message(self, info): + self.got_message.emit(info) log_levels = { usertypes.MessageLevel.error: logging.ERROR, usertypes.MessageLevel.info: logging.INFO, usertypes.MessageLevel.warning: logging.WARNING, } - log_level = log_levels[level] + log_level = log_levels[info.level] - self._logger.log(log_level, text) - self.messages.append(Message(level, text)) + self._logger.log(log_level, info.text) + self.messages.append(info) @pyqtSlot(usertypes.Question) def _record_question(self, question): diff --git a/tests/unit/mainwindow/test_messageview.py b/tests/unit/mainwindow/test_messageview.py index 9e7627635..0f9dc9ea9 100644 --- a/tests/unit/mainwindow/test_messageview.py +++ b/tests/unit/mainwindow/test_messageview.py @@ -21,7 +21,7 @@ import pytest from PyQt5.QtCore import Qt from qutebrowser.mainwindow import messageview -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, message @pytest.fixture @@ -38,23 +38,23 @@ def view(qtbot, config_stub): @pytest.mark.flaky # on macOS def test_single_message(qtbot, view, level): with qtbot.wait_exposed(view, timeout=5000): - view.show_message(level, 'test') + view.show_message(message.MessageInfo(level, 'test')) assert view._messages[0].isVisible() def test_message_hiding(qtbot, view): """Messages should be hidden after the timer times out.""" with qtbot.wait_signal(view._clear_timer.timeout): - view.show_message(usertypes.MessageLevel.info, 'test') + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'test')) assert not view._messages def test_size_hint(view): """The message height should increase with more messages.""" - view.show_message(usertypes.MessageLevel.info, 'test1') + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'test1')) height1 = view.sizeHint().height() assert height1 > 0 - view.show_message(usertypes.MessageLevel.info, 'test2') + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'test2')) height2 = view.sizeHint().height() assert height2 == height1 * 2 @@ -62,7 +62,7 @@ def test_size_hint(view): def test_word_wrap(view, qtbot): """A long message should be wrapped.""" with qtbot.wait_signal(view._clear_timer.timeout): - view.show_message(usertypes.MessageLevel.info, 'short') + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'short')) height1 = view.sizeHint().height() assert height1 > 0 @@ -72,7 +72,7 @@ def test_word_wrap(view, qtbot): "for Odysseus, wise but ill fated, who suffers far from his friends on an " "island deep in the sea.") - view.show_message(usertypes.MessageLevel.info, text) + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, text)) height2 = view.sizeHint().height() assert height2 > height1 @@ -81,26 +81,26 @@ def test_word_wrap(view, qtbot): def test_show_message_twice(view): """Show the same message twice -> only one should be shown.""" - view.show_message(usertypes.MessageLevel.info, 'test') - view.show_message(usertypes.MessageLevel.info, 'test') + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'test')) + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'test')) assert len(view._messages) == 1 def test_show_message_twice_after_first_disappears(qtbot, view): """Show the same message twice after the first is gone.""" with qtbot.wait_signal(view._clear_timer.timeout): - view.show_message(usertypes.MessageLevel.info, 'test') + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'test')) # Just a sanity check assert not view._messages - view.show_message(usertypes.MessageLevel.info, 'test') + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'test')) assert len(view._messages) == 1 def test_changing_timer_with_messages_shown(qtbot, view, config_stub): """When we change messages.timeout, the timer should be restarted.""" config_stub.val.messages.timeout = 900000 # 15s - view.show_message(usertypes.MessageLevel.info, 'test') + view.show_message(message.MessageInfo(usertypes.MessageLevel.info, 'test')) with qtbot.wait_signal(view._clear_timer.timeout): config_stub.val.messages.timeout = 100 @@ -113,8 +113,8 @@ def test_show_multiple_messages_longer(view, count, expected): There is an upper maximum to avoid messages never disappearing. """ for message_number in range(1, count+1): - view.show_message(usertypes.MessageLevel.info, - 'test ' + str(message_number)) + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, f"test {message_number}")) assert view._clear_timer.interval() == expected @@ -127,31 +127,40 @@ def test_show_multiple_messages_longer(view, count, expected): ]) def test_replaced_messages(view, replace1, replace2, length): """Show two stack=False messages which should replace each other.""" - view.show_message(usertypes.MessageLevel.info, 'test', replace=replace1) - view.show_message(usertypes.MessageLevel.info, 'test 2', replace=replace2) + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test', replace=replace1)) + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test 2', replace=replace2)) assert len(view._messages) == length def test_replacing_different_severity(view): - view.show_message(usertypes.MessageLevel.info, 'test', replace='testid') + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test', replace='testid')) with pytest.raises(AssertionError): - view.show_message(usertypes.MessageLevel.error, 'test 2', replace='testid') + view.show_message(message.MessageInfo( + usertypes.MessageLevel.error, 'test 2', replace='testid')) def test_replacing_changed_text(view): - view.show_message(usertypes.MessageLevel.info, 'test', replace='testid') - view.show_message(usertypes.MessageLevel.info, 'test 2') - view.show_message(usertypes.MessageLevel.info, 'test 3', replace='testid') + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test', replace='testid')) + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test 2')) + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test 3', replace='testid')) assert len(view._messages) == 2 assert view._messages[0].text() == 'test 3' assert view._messages[1].text() == 'test 2' def test_replacing_geometry(qtbot, view): - view.show_message(usertypes.MessageLevel.info, 'test', replace='testid') + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test', replace='testid')) with qtbot.wait_signal(view.update_geometry): - view.show_message(usertypes.MessageLevel.info, 'test 2', replace='testid') + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test 2', replace='testid')) @pytest.mark.parametrize('button, count', [ @@ -162,7 +171,9 @@ def test_replacing_geometry(qtbot, view): ]) def test_click_messages(qtbot, view, button, count): """Messages should disappear when we click on them.""" - view.show_message(usertypes.MessageLevel.info, 'test mouse click') - view.show_message(usertypes.MessageLevel.info, 'test mouse click 2') + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test mouse click')) + view.show_message(message.MessageInfo( + usertypes.MessageLevel.info, 'test mouse click 2')) qtbot.mousePress(view, button) assert len(view._messages) == count -- cgit v1.2.3-54-g00ecf