From 2c4bb064e69d7e49a70090fd3df84c66277a1405 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 17 Dec 2020 17:25:00 -0500 Subject: Show set-cmd-text command bindings in completion. Fixes #5942. For bindings like `o -> :set-cmd-text -s :open`, the user conceptually expects that `o` maps to `open`, yet they do not show in the command completion UI. With this patch, a binding to a command of form `set-cmd-text [flags...] : [cmdargs]` will be treated as if it were bound to ` [cmdargs]` instead for the purpose of completion. Bindings to `set-cmd-text --append` are ignored. --- qutebrowser/config/config.py | 31 +++++++++++++++++++++++++++++-- tests/unit/config/test_config.py | 20 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index d8e8d612e..21616cb56 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -162,13 +162,40 @@ class KeyConfig: bindings[key] = binding return bindings + def _strip_leading_flags(self, cmdline: str) -> List[str]: + """Split cmdline at whitespace until the first non-flag.""" + first, _, rest = cmdline.partition(" ") + if first.startswith("-"): + return [first] + self._strip_leading_flags(rest) + return [cmdline] + + def _implied_cmd(self, cmdline: str) -> str: + """Return cmdline, or the implied cmd if cmdline is a set-cmd-text.""" + if not cmdline.startswith("set-cmd-text "): + return cmdline + cmdline = cmdline[len("set-cmd-text "):] + *flags, cmd = self._strip_leading_flags(cmdline) + if "-a" in flags or "--append" in flags or not cmd.startswith(":"): + return "" # doesn't look like this sets a command + return cmd.lstrip(":") + def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings': - """Get a dict of commands to a list of bindings for the mode.""" + """Get a dict of commands to a list of bindings for the mode. + + This is intented for user-facing display of keybindings. + As such, bindings for 'set-cmd-text [flags] : ...' are translated + to ' ...', as from the user's perspective these keys behave like + bindings for '' (that allow for further input before running). + + See #5942. + """ cmd_to_keys: KeyConfig._ReverseBindings = {} bindings = self.get_bindings_for(mode) for seq, full_cmd in sorted(bindings.items()): for cmd in full_cmd.split(';;'): - cmd = cmd.strip() + cmd = self._implied_cmd(cmd.strip()) + if not cmd: + continue cmd_to_keys.setdefault(cmd, []) # Put bindings involving modifiers last if any(info.modifiers for info in seq): diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index b30ab4bee..5e97cf714 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -198,6 +198,26 @@ class TestKeyConfig: # Chained command ({'a': 'message-info foo ;; message-info bar'}, {'message-info foo': ['a'], 'message-info bar': ['a']}), + # Command using set-cmd-text (#5942) + ( + { + "o": "set-cmd-text -s :open", + "O": "set-cmd-text -s :open -t", + "go": "set-cmd-text :open {url:pretty}", + # all of these should be ignored + "/": "set-cmd-text /", + "?": "set-cmd-text ?", + ":": "set-cmd-text :", + "a": "set-cmd-text no_leading_colon", + "b": "set-cmd-text -s -a :skip_cuz_append", + "c": "set-cmd-text --append :skip_cuz_append", + }, + { + "open": ["o"], + "open -t": ["O"], + "open {url:pretty}": ["go"], + } + ), ]) def test_get_reverse_bindings_for(self, key_config_stub, config_stub, no_bindings, bindings, expected): -- cgit v1.2.3-54-g00ecf From 4cf6d910498f8a4cf7636a0d278c8bd8458f058b Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 27 Dec 2020 12:08:40 -0500 Subject: Add CommandParser tests. All the existing tests just verify whether parsing suceeds or not, but don't verify the result. These tests make it easier to see what the intended behavior of CommandParser is. --- tests/unit/commands/test_runners.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/unit/commands/test_runners.py b/tests/unit/commands/test_runners.py index 497a99663..0d7f27fa7 100644 --- a/tests/unit/commands/test_runners.py +++ b/tests/unit/commands/test_runners.py @@ -67,6 +67,30 @@ class TestCommandParser: with pytest.raises(cmdexc.NoSuchCommandError): parser.parse_all(command) + @pytest.mark.parametrize('command, name, args', [ + ("set-cmd-text -s :open", "set-cmd-text", ["-s", ":open"]), + ("set-cmd-text :open {url:pretty}", "set-cmd-text", + [":open {url:pretty}"]), + ("set-cmd-text -s :open -t", "set-cmd-text", ["-s", ":open -t"]), + ("set-cmd-text :open -t -r {url:pretty}", "set-cmd-text", + [":open -t -r {url:pretty}"]), + ("set-cmd-text -s :open -b", "set-cmd-text", ["-s", ":open -b"]), + ("set-cmd-text :open -b -r {url:pretty}", "set-cmd-text", + [":open -b -r {url:pretty}"]), + ("set-cmd-text -s :open -w", "set-cmd-text", + ["-s", ":open -w"]), + ("set-cmd-text :open -w {url:pretty}", "set-cmd-text", + [":open -w {url:pretty}"]), + ("set-cmd-text /", "set-cmd-text", ["/"]), + ("set-cmd-text ?", "set-cmd-text", ["?"]), + ("set-cmd-text :", "set-cmd-text", [":"]), + ]) + def test_parse_result(self, command, name, args): + parser = runners.CommandParser() + result = parser.parse_all(command, aliases=False)[0] + assert result.cmd.name == name + assert result.args == args + class TestCompletions: -- cgit v1.2.3-54-g00ecf From 3b7d459e0df635fa513472a8a00e84dc67c48aa1 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 27 Dec 2020 13:11:44 -0500 Subject: Use CommandParser for binding completion. Reduce the amount of custom parsing logic in config.py. Turns out circular import isn't a problem here. Some of the test cases in `test_get_reverse_bindings_for` had to be changed from `message-info` to `open`, as CommandParser expects commands to actually exist, and `message-info` isn't registered during those tests. --- qutebrowser/config/config.py | 20 ++++++++++---------- tests/unit/config/test_config.py | 16 ++++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 21616cb56..b2ba1a6a5 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -27,6 +27,7 @@ from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping, from PyQt5.QtCore import pyqtSignal, QObject, QUrl +from qutebrowser.commands import cmdexc, runners from qutebrowser.config import configdata, configexc, configutils from qutebrowser.utils import utils, log, urlmatch from qutebrowser.misc import objects @@ -162,19 +163,18 @@ class KeyConfig: bindings[key] = binding return bindings - def _strip_leading_flags(self, cmdline: str) -> List[str]: - """Split cmdline at whitespace until the first non-flag.""" - first, _, rest = cmdline.partition(" ") - if first.startswith("-"): - return [first] + self._strip_leading_flags(rest) - return [cmdline] - def _implied_cmd(self, cmdline: str) -> str: """Return cmdline, or the implied cmd if cmdline is a set-cmd-text.""" - if not cmdline.startswith("set-cmd-text "): + try: + results = runners.CommandParser().parse_all(cmdline, aliases=False) + if len(results) == 0: + return "" + result = results[0] + except cmdexc.NoSuchCommandError: + return "" + if result.cmd.name != "set-cmd-text": return cmdline - cmdline = cmdline[len("set-cmd-text "):] - *flags, cmd = self._strip_leading_flags(cmdline) + *flags, cmd = result.args if "-a" in flags or "--append" in flags or not cmd.startswith(":"): return "" # doesn't look like this sets a command return cmd.lstrip(":") diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 5e97cf714..b0c32c6fb 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -187,17 +187,17 @@ class TestKeyConfig: @pytest.mark.parametrize('bindings, expected', [ # Simple - ({'a': 'message-info foo', 'b': 'message-info bar'}, - {'message-info foo': ['a'], 'message-info bar': ['b']}), + ({'a': 'open foo', 'b': 'open bar'}, + {'open foo': ['a'], 'open bar': ['b']}), # Multiple bindings - ({'a': 'message-info foo', 'b': 'message-info foo'}, - {'message-info foo': ['b', 'a']}), + ({'a': 'open foo', 'b': 'open foo'}, + {'open foo': ['b', 'a']}), # With modifier keys (should be listed last and normalized) - ({'a': 'message-info foo', '': 'message-info foo'}, - {'message-info foo': ['a', '']}), + ({'a': 'open foo', '': 'open foo'}, + {'open foo': ['a', '']}), # Chained command - ({'a': 'message-info foo ;; message-info bar'}, - {'message-info foo': ['a'], 'message-info bar': ['a']}), + ({'a': 'open foo ;; open bar'}, + {'open foo': ['a'], 'open bar': ['a']}), # Command using set-cmd-text (#5942) ( { -- cgit v1.2.3-54-g00ecf From e16e67baeb59024cad52a33844cccb81045865cf Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Sun, 3 Jan 2021 13:41:11 -0500 Subject: Fix PR issues for set-cmd-text completions. - Prefer None to "" - Prefer implicit bool checking - Try to fix mypy warnings --- qutebrowser/config/config.py | 13 +++++++------ qutebrowser/keyinput/modeparsers.py | 4 ++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index b2ba1a6a5..9af764253 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -163,20 +163,21 @@ class KeyConfig: bindings[key] = binding return bindings - def _implied_cmd(self, cmdline: str) -> str: + def _implied_cmd(self, cmdline: str) -> Optional[str]: """Return cmdline, or the implied cmd if cmdline is a set-cmd-text.""" try: results = runners.CommandParser().parse_all(cmdline, aliases=False) - if len(results) == 0: - return "" - result = results[0] except cmdexc.NoSuchCommandError: - return "" + return None + + if not results: + return None + result = results[0] if result.cmd.name != "set-cmd-text": return cmdline *flags, cmd = result.args if "-a" in flags or "--append" in flags or not cmd.startswith(":"): - return "" # doesn't look like this sets a command + return None # doesn't look like this sets a command return cmd.lstrip(":") def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings': diff --git a/qutebrowser/keyinput/modeparsers.py b/qutebrowser/keyinput/modeparsers.py index 48f3594a5..fe03a9db6 100644 --- a/qutebrowser/keyinput/modeparsers.py +++ b/qutebrowser/keyinput/modeparsers.py @@ -86,6 +86,8 @@ class NormalKeyParser(CommandKeyParser): _partial_timer: Timer to clear partial keypresses. """ + _sequence: keyutils.KeySequence + def __init__(self, *, win_id: int, commandrunner: 'runners.CommandRunner', parent: QObject = None) -> None: @@ -154,6 +156,8 @@ class HintKeyParser(basekeyparser.BaseKeyParser): _last_press: The nature of the last keypress, a LastPress member. """ + _sequence: keyutils.KeySequence + def __init__(self, *, win_id: int, commandrunner: 'runners.CommandRunner', hintmanager: hints.HintManager, -- cgit v1.2.3-54-g00ecf From 8d1c5b690ee421144b499d14d62be644310bc29d Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Wed, 6 Jan 2021 09:15:13 -0500 Subject: Eliminate parser dependency on config. commands.runners depends on config.config to get aliases and the best_match setting, while config.config depends on commands.runners to split commands. Eliminate this circular dependency by: - Moving CommandParser from commands.runners into its a new commands.parser module - Pass aliases/config settings into CommandParser as arguments --- qutebrowser/commands/parser.py | 211 +++++++++++++++++++++++++++ qutebrowser/commands/runners.py | 194 +----------------------- qutebrowser/completion/completer.py | 5 +- qutebrowser/completion/models/configmodel.py | 8 +- qutebrowser/config/config.py | 9 +- tests/unit/commands/test_parser.py | 137 +++++++++++++++++ tests/unit/commands/test_runners.py | 137 ----------------- 7 files changed, 362 insertions(+), 339 deletions(-) create mode 100644 qutebrowser/commands/parser.py create mode 100644 tests/unit/commands/test_parser.py delete mode 100644 tests/unit/commands/test_runners.py diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py new file mode 100644 index 000000000..562b48922 --- /dev/null +++ b/qutebrowser/commands/parser.py @@ -0,0 +1,211 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2021 Ryan Roden-Corrent (rcorre) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Module for parsing commands entered into the browser.""" + +import attr +from PyQt5.QtCore import pyqtSlot, QUrl, QObject + +from qutebrowser.commands import cmdexc +from qutebrowser.misc import split, objects + + +@attr.s +class ParseResult: + + """The result of parsing a commandline.""" + + cmd = attr.ib() + args = attr.ib() + cmdline = attr.ib() + + +class CommandParser: + + """Parse qutebrowser commandline commands. + + Attributes: + _partial_match: Whether to allow partial command matches. + """ + + def __init__(self, partial_match=False): + self._partial_match = partial_match + + def _get_alias(self, text, aliases, default=None): + """Get an alias from the config. + + Args: + text: The text to parse. + aliases: A map of aliases to commands. + default : Default value to return when alias was not found. + + Return: + The new command string if an alias was found. Default value + otherwise. + """ + parts = text.strip().split(maxsplit=1) + if parts[0] not in aliases: + return default + alias = aliases[parts[0]] + + try: + new_cmd = '{} {}'.format(alias, parts[1]) + except IndexError: + new_cmd = alias + if text.endswith(' '): + new_cmd += ' ' + return new_cmd + + def _parse_all_gen(self, text, *args, aliases=None, **kwargs): + """Split a command on ;; and parse all parts. + + If the first command in the commandline is a non-split one, it only + returns that. + + Args: + text: Text to parse. + aliases: A map of aliases to commands. + *args/**kwargs: Passed to parse(). + + Yields: + ParseResult tuples. + """ + text = text.strip().lstrip(':').strip() + if not text: + raise cmdexc.NoSuchCommandError("No command given") + + if aliases: + text = self._get_alias(text, aliases, text) + + if ';;' in text: + # Get the first command and check if it doesn't want to have ;; + # split. + first = text.split(';;')[0] + result = self.parse(first, *args, **kwargs) + if result.cmd.no_cmd_split: + sub_texts = [text] + else: + sub_texts = [e.strip() for e in text.split(';;')] + else: + sub_texts = [text] + for sub in sub_texts: + yield self.parse(sub, *args, **kwargs) + + def parse_all(self, *args, **kwargs): + """Wrapper over _parse_all_gen.""" + return list(self._parse_all_gen(*args, **kwargs)) + + def parse(self, text, *, fallback=False, keep=False, best_match=False): + """Split the commandline text into command and arguments. + + Args: + text: Text to parse. + fallback: Whether to do a fallback splitting when the command was + unknown. + keep: Whether to keep special chars and whitespace + + Return: + A ParseResult tuple. + """ + cmdstr, sep, argstr = text.partition(' ') + + if not cmdstr and not fallback: + raise cmdexc.NoSuchCommandError("No command given") + + if self._partial_match: + cmdstr = self._completion_match(cmdstr, best_match) + + try: + cmd = objects.commands[cmdstr] + except KeyError: + if not fallback: + raise cmdexc.NoSuchCommandError( + '{}: no such command'.format(cmdstr)) + cmdline = split.split(text, keep=keep) + return ParseResult(cmd=None, args=None, cmdline=cmdline) + + args = self._split_args(cmd, argstr, keep) + if keep and args: + cmdline = [cmdstr, sep + args[0]] + args[1:] + elif keep: + cmdline = [cmdstr, sep] + else: + cmdline = [cmdstr] + args[:] + + return ParseResult(cmd=cmd, args=args, cmdline=cmdline) + + def _completion_match(self, cmdstr, best): + """Replace cmdstr with a matching completion if there's only one match. + + Args: + cmdstr: The string representing the entered command so far + + Return: + cmdstr modified to the matching completion or unmodified + """ + matches = [cmd for cmd in sorted(objects.commands, key=len) + if cmdstr in cmd] + if len(matches) == 1: + cmdstr = matches[0] + elif len(matches) > 1 and best: + cmdstr = matches[0] + return cmdstr + + def _split_args(self, cmd, argstr, keep): + """Split the arguments from an arg string. + + Args: + cmd: The command we're currently handling. + argstr: An argument string. + keep: Whether to keep special chars and whitespace + + Return: + A list containing the split strings. + """ + if not argstr: + return [] + elif cmd.maxsplit is None: + return split.split(argstr, keep=keep) + else: + # If split=False, we still want to split the flags, but not + # everything after that. + # We first split the arg string and check the index of the first + # non-flag args, then we re-split again properly. + # example: + # + # input: "--foo -v bar baz" + # first split: ['--foo', '-v', 'bar', 'baz'] + # 0 1 2 3 + # second split: ['--foo', '-v', 'bar baz'] + # (maxsplit=2) + split_args = split.simple_split(argstr, keep=keep) + flag_arg_count = 0 + for i, arg in enumerate(split_args): + arg = arg.strip() + if arg.startswith('-'): + if arg in cmd.flags_with_args: + flag_arg_count += 1 + else: + maxsplit = i + cmd.maxsplit + flag_arg_count + return split.simple_split(argstr, keep=keep, + maxsplit=maxsplit) + + # If there are only flags, we got it right on the first try + # already. + return split_args diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index c195a8be9..7d32c7f50 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -24,14 +24,12 @@ import re import contextlib from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping -import attr from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.api import cmdutils from qutebrowser.config import config -from qutebrowser.commands import cmdexc +from qutebrowser.commands import cmdexc, parser from qutebrowser.utils import message, objreg, qtutils, usertypes, utils -from qutebrowser.misc import split, objects from qutebrowser.keyinput import macros, modeman if TYPE_CHECKING: @@ -42,16 +40,6 @@ _ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str] last_command = {} -@attr.s -class ParseResult: - - """The result of parsing a commandline.""" - - cmd = attr.ib() - args = attr.ib() - cmdline = attr.ib() - - def _url(tabbed_browser): """Convenience method to get the current url.""" try: @@ -129,181 +117,6 @@ def replace_variables(win_id, arglist): return args -class CommandParser: - - """Parse qutebrowser commandline commands. - - Attributes: - _partial_match: Whether to allow partial command matches. - """ - - def __init__(self, partial_match=False): - self._partial_match = partial_match - - def _get_alias(self, text, default=None): - """Get an alias from the config. - - Args: - text: The text to parse. - default : Default value to return when alias was not found. - - Return: - The new command string if an alias was found. Default value - otherwise. - """ - parts = text.strip().split(maxsplit=1) - aliases = config.cache['aliases'] - if parts[0] not in aliases: - return default - alias = aliases[parts[0]] - - try: - new_cmd = '{} {}'.format(alias, parts[1]) - except IndexError: - new_cmd = alias - if text.endswith(' '): - new_cmd += ' ' - return new_cmd - - def _parse_all_gen(self, text, *args, aliases=True, **kwargs): - """Split a command on ;; and parse all parts. - - If the first command in the commandline is a non-split one, it only - returns that. - - Args: - text: Text to parse. - aliases: Whether to handle aliases. - *args/**kwargs: Passed to parse(). - - Yields: - ParseResult tuples. - """ - text = text.strip().lstrip(':').strip() - if not text: - raise cmdexc.NoSuchCommandError("No command given") - - if aliases: - text = self._get_alias(text, text) - - if ';;' in text: - # Get the first command and check if it doesn't want to have ;; - # split. - first = text.split(';;')[0] - result = self.parse(first, *args, **kwargs) - if result.cmd.no_cmd_split: - sub_texts = [text] - else: - sub_texts = [e.strip() for e in text.split(';;')] - else: - sub_texts = [text] - for sub in sub_texts: - yield self.parse(sub, *args, **kwargs) - - def parse_all(self, *args, **kwargs): - """Wrapper over _parse_all_gen.""" - return list(self._parse_all_gen(*args, **kwargs)) - - def parse(self, text, *, fallback=False, keep=False): - """Split the commandline text into command and arguments. - - Args: - text: Text to parse. - fallback: Whether to do a fallback splitting when the command was - unknown. - keep: Whether to keep special chars and whitespace - - Return: - A ParseResult tuple. - """ - cmdstr, sep, argstr = text.partition(' ') - - if not cmdstr and not fallback: - raise cmdexc.NoSuchCommandError("No command given") - - if self._partial_match: - cmdstr = self._completion_match(cmdstr) - - try: - cmd = objects.commands[cmdstr] - except KeyError: - if not fallback: - raise cmdexc.NoSuchCommandError( - '{}: no such command'.format(cmdstr)) - cmdline = split.split(text, keep=keep) - return ParseResult(cmd=None, args=None, cmdline=cmdline) - - args = self._split_args(cmd, argstr, keep) - if keep and args: - cmdline = [cmdstr, sep + args[0]] + args[1:] - elif keep: - cmdline = [cmdstr, sep] - else: - cmdline = [cmdstr] + args[:] - - return ParseResult(cmd=cmd, args=args, cmdline=cmdline) - - def _completion_match(self, cmdstr): - """Replace cmdstr with a matching completion if there's only one match. - - Args: - cmdstr: The string representing the entered command so far - - Return: - cmdstr modified to the matching completion or unmodified - """ - matches = [cmd for cmd in sorted(objects.commands, key=len) - if cmdstr in cmd] - if len(matches) == 1: - cmdstr = matches[0] - elif len(matches) > 1 and config.val.completion.use_best_match: - cmdstr = matches[0] - return cmdstr - - def _split_args(self, cmd, argstr, keep): - """Split the arguments from an arg string. - - Args: - cmd: The command we're currently handling. - argstr: An argument string. - keep: Whether to keep special chars and whitespace - - Return: - A list containing the split strings. - """ - if not argstr: - return [] - elif cmd.maxsplit is None: - return split.split(argstr, keep=keep) - else: - # If split=False, we still want to split the flags, but not - # everything after that. - # We first split the arg string and check the index of the first - # non-flag args, then we re-split again properly. - # example: - # - # input: "--foo -v bar baz" - # first split: ['--foo', '-v', 'bar', 'baz'] - # 0 1 2 3 - # second split: ['--foo', '-v', 'bar baz'] - # (maxsplit=2) - split_args = split.simple_split(argstr, keep=keep) - flag_arg_count = 0 - for i, arg in enumerate(split_args): - arg = arg.strip() - if arg.startswith('-'): - if arg in cmd.flags_with_args: - flag_arg_count += 1 - else: - maxsplit = i + cmd.maxsplit + flag_arg_count - return split.simple_split(argstr, keep=keep, - maxsplit=maxsplit) - - # If there are only flags, we got it right on the first try - # already. - return split_args - - class AbstractCommandRunner(QObject): """Abstract base class for CommandRunner.""" @@ -328,7 +141,7 @@ class CommandRunner(AbstractCommandRunner): def __init__(self, win_id, partial_match=False, parent=None): super().__init__(parent) - self._parser = CommandParser(partial_match=partial_match) + self._parser = parser.CommandParser(partial_match=partial_match) self._win_id = win_id @contextlib.contextmanager @@ -358,7 +171,8 @@ class CommandRunner(AbstractCommandRunner): parsed = None with self._handle_error(safely): - parsed = self._parser.parse_all(text) + parsed = self._parser.parse_all( + text, best_match=config.val.completion.use_best_match) if parsed is None: return diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index b06611bc0..1b8e44bc1 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -23,7 +23,7 @@ import attr from PyQt5.QtCore import pyqtSlot, QObject, QTimer from qutebrowser.config import config -from qutebrowser.commands import runners +from qutebrowser.commands import parser from qutebrowser.misc import objects from qutebrowser.utils import log, utils, debug, objreg from qutebrowser.completion.models import miscmodels @@ -135,8 +135,7 @@ class Completer(QObject): if not text or not text.strip(): # Only ":", empty part under the cursor with nothing before/after return [], '', [] - parser = runners.CommandParser() - result = parser.parse(text, fallback=True, keep=True) + result = parser.CommandParser().parse(text, fallback=True, keep=True) parts = [x for x in result.cmdline if x] pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index b6337bdbd..2fbdae2ad 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -21,7 +21,7 @@ from qutebrowser.config import configdata, configexc from qutebrowser.completion.models import completionmodel, listcategory, util -from qutebrowser.commands import runners, cmdexc +from qutebrowser.commands import parser, cmdexc from qutebrowser.keyinput import keyutils @@ -117,9 +117,8 @@ def _bind_current_default(key, info): cmd_text = info.keyconf.get_command(seq, 'normal') if cmd_text: - parser = runners.CommandParser() try: - cmd = parser.parse(cmd_text).cmd + cmd = parser.CommandParser().parse(cmd_text).cmd except cmdexc.NoSuchCommandError: data.append((cmd_text, '(Current) Invalid command!', key)) else: @@ -127,8 +126,7 @@ def _bind_current_default(key, info): cmd_text = info.keyconf.get_command(seq, 'normal', default=True) if cmd_text: - parser = runners.CommandParser() - cmd = parser.parse(cmd_text).cmd + cmd = parser.CommandParser().parse(cmd_text).cmd data.append((cmd_text, '(Default) {}'.format(cmd.desc), key)) return data diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 9af764253..fee3fec47 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -27,7 +27,7 @@ from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping, from PyQt5.QtCore import pyqtSignal, QObject, QUrl -from qutebrowser.commands import cmdexc, runners +from qutebrowser.commands import cmdexc, parser from qutebrowser.config import configdata, configexc, configutils from qutebrowser.utils import utils, log, urlmatch from qutebrowser.misc import objects @@ -166,7 +166,8 @@ class KeyConfig: def _implied_cmd(self, cmdline: str) -> Optional[str]: """Return cmdline, or the implied cmd if cmdline is a set-cmd-text.""" try: - results = runners.CommandParser().parse_all(cmdline, aliases=False) + results = parser.CommandParser().parse_all( + cmdline, aliases=cache['aliases']) except cmdexc.NoSuchCommandError: return None @@ -193,8 +194,8 @@ class KeyConfig: cmd_to_keys: KeyConfig._ReverseBindings = {} bindings = self.get_bindings_for(mode) for seq, full_cmd in sorted(bindings.items()): - for cmd in full_cmd.split(';;'): - cmd = self._implied_cmd(cmd.strip()) + for cmdtext in full_cmd.split(';;'): + cmd = self._implied_cmd(cmdtext.strip()) if not cmd: continue cmd_to_keys.setdefault(cmd, []) diff --git a/tests/unit/commands/test_parser.py b/tests/unit/commands/test_parser.py new file mode 100644 index 000000000..011c1ca02 --- /dev/null +++ b/tests/unit/commands/test_parser.py @@ -0,0 +1,137 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2015-2020 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Tests for qutebrowser.commands.parser.""" + +import pytest + +from qutebrowser.misc import objects +from qutebrowser.commands import parser, cmdexc + + +class TestCommandParser: + + def test_parse_all(self, cmdline_test): + """Test parsing of commands. + + See https://github.com/qutebrowser/qutebrowser/issues/615 + + Args: + cmdline_test: A pytest fixture which provides testcases. + """ + p = parser.CommandParser() + if cmdline_test.valid: + p.parse_all(cmdline_test.cmd) + else: + with pytest.raises(cmdexc.NoSuchCommandError): + p.parse_all(cmdline_test.cmd) + + def test_parse_all_with_alias(self, cmdline_test, monkeypatch, + config_stub): + if not cmdline_test.cmd: + pytest.skip("Empty command") + + aliases = {'alias_name': cmdline_test.cmd} + + p = parser.CommandParser() + if cmdline_test.valid: + assert len(p.parse_all("alias_name", aliases=aliases)) > 0 + else: + with pytest.raises(cmdexc.NoSuchCommandError): + p.parse_all("alias_name", aliases=aliases) + + @pytest.mark.parametrize('command', ['', ' ']) + def test_parse_empty_with_alias(self, command): + """An empty command should not crash. + + See https://github.com/qutebrowser/qutebrowser/issues/1690 + and https://github.com/qutebrowser/qutebrowser/issues/1773 + """ + p = parser.CommandParser() + with pytest.raises(cmdexc.NoSuchCommandError): + p.parse_all(command, aliases={"foo": "bar"}) + + @pytest.mark.parametrize('command, name, args', [ + ("set-cmd-text -s :open", "set-cmd-text", ["-s", ":open"]), + ("set-cmd-text :open {url:pretty}", "set-cmd-text", + [":open {url:pretty}"]), + ("set-cmd-text -s :open -t", "set-cmd-text", ["-s", ":open -t"]), + ("set-cmd-text :open -t -r {url:pretty}", "set-cmd-text", + [":open -t -r {url:pretty}"]), + ("set-cmd-text -s :open -b", "set-cmd-text", ["-s", ":open -b"]), + ("set-cmd-text :open -b -r {url:pretty}", "set-cmd-text", + [":open -b -r {url:pretty}"]), + ("set-cmd-text -s :open -w", "set-cmd-text", + ["-s", ":open -w"]), + ("set-cmd-text :open -w {url:pretty}", "set-cmd-text", + [":open -w {url:pretty}"]), + ("set-cmd-text /", "set-cmd-text", ["/"]), + ("set-cmd-text ?", "set-cmd-text", ["?"]), + ("set-cmd-text :", "set-cmd-text", [":"]), + ]) + def test_parse_result(self, command, name, args): + p = parser.CommandParser() + result = p.parse_all(command)[0] + assert result.cmd.name == name + assert result.args == args + + +class TestCompletions: + + """Tests for completions.use_best_match.""" + + @pytest.fixture(autouse=True) + def cmdutils_stub(self, monkeypatch, stubs): + """Patch the cmdutils module to provide fake commands.""" + monkeypatch.setattr(objects, 'commands', { + 'one': stubs.FakeCommand(name='one'), + 'two': stubs.FakeCommand(name='two'), + 'two-foo': stubs.FakeCommand(name='two-foo'), + }) + + def test_partial_parsing(self, config_stub): + """Test partial parsing with a runner where it's enabled. + + The same with it being disabled is tested by test_parse_all. + """ + p = parser.CommandParser(partial_match=True) + result = p.parse('on') + assert result.cmd.name == 'one' + + def test_dont_use_best_match(self, config_stub): + """Test multiple completion options with use_best_match set to false. + + Should raise NoSuchCommandError + """ + config_stub.val.completion.use_best_match = False + p = parser.CommandParser(partial_match=True) + + with pytest.raises(cmdexc.NoSuchCommandError): + p.parse('tw') + + def test_use_best_match(self, config_stub): + """Test multiple completion options with use_best_match set to true. + + The resulting command should be the best match + """ + config_stub.val.completion.use_best_match = True + p = parser.CommandParser(partial_match=True) + + result = p.parse('tw', best_match=True) + assert result.cmd.name == 'two' diff --git a/tests/unit/commands/test_runners.py b/tests/unit/commands/test_runners.py deleted file mode 100644 index 0d7f27fa7..000000000 --- a/tests/unit/commands/test_runners.py +++ /dev/null @@ -1,137 +0,0 @@ -# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: - -# Copyright 2015-2020 Florian Bruhin (The Compiler) -# -# This file is part of qutebrowser. -# -# qutebrowser is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# qutebrowser 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . - -"""Tests for qutebrowser.commands.runners.""" - -import pytest - -from qutebrowser.misc import objects -from qutebrowser.commands import runners, cmdexc - - -class TestCommandParser: - - def test_parse_all(self, cmdline_test): - """Test parsing of commands. - - See https://github.com/qutebrowser/qutebrowser/issues/615 - - Args: - cmdline_test: A pytest fixture which provides testcases. - """ - parser = runners.CommandParser() - if cmdline_test.valid: - parser.parse_all(cmdline_test.cmd, aliases=False) - else: - with pytest.raises(cmdexc.NoSuchCommandError): - parser.parse_all(cmdline_test.cmd, aliases=False) - - def test_parse_all_with_alias(self, cmdline_test, monkeypatch, - config_stub): - if not cmdline_test.cmd: - pytest.skip("Empty command") - - config_stub.val.aliases = {'alias_name': cmdline_test.cmd} - - parser = runners.CommandParser() - if cmdline_test.valid: - assert len(parser.parse_all("alias_name")) > 0 - else: - with pytest.raises(cmdexc.NoSuchCommandError): - parser.parse_all("alias_name") - - @pytest.mark.parametrize('command', ['', ' ']) - def test_parse_empty_with_alias(self, command): - """An empty command should not crash. - - See https://github.com/qutebrowser/qutebrowser/issues/1690 - and https://github.com/qutebrowser/qutebrowser/issues/1773 - """ - parser = runners.CommandParser() - with pytest.raises(cmdexc.NoSuchCommandError): - parser.parse_all(command) - - @pytest.mark.parametrize('command, name, args', [ - ("set-cmd-text -s :open", "set-cmd-text", ["-s", ":open"]), - ("set-cmd-text :open {url:pretty}", "set-cmd-text", - [":open {url:pretty}"]), - ("set-cmd-text -s :open -t", "set-cmd-text", ["-s", ":open -t"]), - ("set-cmd-text :open -t -r {url:pretty}", "set-cmd-text", - [":open -t -r {url:pretty}"]), - ("set-cmd-text -s :open -b", "set-cmd-text", ["-s", ":open -b"]), - ("set-cmd-text :open -b -r {url:pretty}", "set-cmd-text", - [":open -b -r {url:pretty}"]), - ("set-cmd-text -s :open -w", "set-cmd-text", - ["-s", ":open -w"]), - ("set-cmd-text :open -w {url:pretty}", "set-cmd-text", - [":open -w {url:pretty}"]), - ("set-cmd-text /", "set-cmd-text", ["/"]), - ("set-cmd-text ?", "set-cmd-text", ["?"]), - ("set-cmd-text :", "set-cmd-text", [":"]), - ]) - def test_parse_result(self, command, name, args): - parser = runners.CommandParser() - result = parser.parse_all(command, aliases=False)[0] - assert result.cmd.name == name - assert result.args == args - - -class TestCompletions: - - """Tests for completions.use_best_match.""" - - @pytest.fixture(autouse=True) - def cmdutils_stub(self, monkeypatch, stubs): - """Patch the cmdutils module to provide fake commands.""" - monkeypatch.setattr(objects, 'commands', { - 'one': stubs.FakeCommand(name='one'), - 'two': stubs.FakeCommand(name='two'), - 'two-foo': stubs.FakeCommand(name='two-foo'), - }) - - def test_partial_parsing(self, config_stub): - """Test partial parsing with a runner where it's enabled. - - The same with it being disabled is tested by test_parse_all. - """ - parser = runners.CommandParser(partial_match=True) - result = parser.parse('on') - assert result.cmd.name == 'one' - - def test_dont_use_best_match(self, config_stub): - """Test multiple completion options with use_best_match set to false. - - Should raise NoSuchCommandError - """ - config_stub.val.completion.use_best_match = False - parser = runners.CommandParser(partial_match=True) - - with pytest.raises(cmdexc.NoSuchCommandError): - parser.parse('tw') - - def test_use_best_match(self, config_stub): - """Test multiple completion options with use_best_match set to true. - - The resulting command should be the best match - """ - config_stub.val.completion.use_best_match = True - parser = runners.CommandParser(partial_match=True) - - result = parser.parse('tw') - assert result.cmd.name == 'two' -- cgit v1.2.3-54-g00ecf From 0b979666b6584db2ec28c0977097af60ab3a63af Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Wed, 6 Jan 2021 09:47:17 -0500 Subject: Remove unused imports in commands.parser --- qutebrowser/commands/parser.py | 1 - 1 file changed, 1 deletion(-) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index 562b48922..1f0fb01b2 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -20,7 +20,6 @@ """Module for parsing commands entered into the browser.""" import attr -from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.commands import cmdexc from qutebrowser.misc import split, objects -- cgit v1.2.3-54-g00ecf From a7178298efe371be292d85ab0a2f1cf7943365e0 Mon Sep 17 00:00:00 2001 From: Ryan Roden-Corrent Date: Thu, 7 Jan 2021 08:44:15 -0500 Subject: Restore coverage of config.py to 100%. Test an empty/unknown command, and remove the check for an empty parse_all result (as parse_all will either throw or return a result. --- qutebrowser/config/config.py | 2 -- tests/unit/config/test_config.py | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index fee3fec47..1c1de0f82 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -171,8 +171,6 @@ class KeyConfig: except cmdexc.NoSuchCommandError: return None - if not results: - return None result = results[0] if result.cmd.name != "set-cmd-text": return cmdline diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index b0c32c6fb..3e81833e3 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -218,6 +218,8 @@ class TestKeyConfig: "open {url:pretty}": ["go"], } ), + # Empty/unknown commands + ({"a": "", "b": "notreal"}, {}), ]) def test_get_reverse_bindings_for(self, key_config_stub, config_stub, no_bindings, bindings, expected): -- cgit v1.2.3-54-g00ecf From 260f91de5677215b23aa9a85eb1e036202b2205e Mon Sep 17 00:00:00 2001 From: Lembrun Date: Wed, 3 Mar 2021 15:35:40 +0100 Subject: Replaced os.path by the pathlib equivalent in unit/javascript folder --- tests/unit/javascript/conftest.py | 10 +++++----- tests/unit/javascript/stylesheet/test_stylesheet_js.py | 3 ++- tests/unit/javascript/test_greasemonkey.py | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 47884687d..2cddfce24 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -20,7 +20,7 @@ """pytest conftest file for javascript tests.""" import os -import os.path +import pathlib import pytest import jinja2 @@ -43,7 +43,7 @@ class JSTester: def __init__(self, tab, qtbot, config_stub): self.tab = tab self.qtbot = qtbot - loader = jinja2.FileSystemLoader(os.path.dirname(__file__)) + loader = jinja2.FileSystemLoader(pathlib.Path(__file__).parent) self._jinja_env = jinja2.Environment(loader=loader, autoescape=True) # Make sure error logging via JS fails tests config_stub.val.content.javascript.log = { @@ -86,7 +86,7 @@ class JSTester: force: Whether to force loading even if the file is invalid. """ self.load_url(QUrl.fromLocalFile( - os.path.join(os.path.dirname(__file__), path)), force) + str(pathlib.Path(__file__).parent.joinpath(path))), force) def load_url(self, url: QUrl, force: bool = False): """Load a given QUrl. @@ -108,8 +108,8 @@ class JSTester: path: The path to the JS file, relative to the qutebrowser package. expected: The value expected return from the javascript execution """ - base_path = os.path.dirname(os.path.abspath(qutebrowser.__file__)) - with open(os.path.join(base_path, path), 'r', encoding='utf-8') as f: + base_path = pathlib.Path(qutebrowser.__file__).resolve().parent + with open(pathlib.Path(base_path).joinpath(path), 'r', encoding='utf-8') as f: source = f.read() self.run(source, expected) diff --git a/tests/unit/javascript/stylesheet/test_stylesheet_js.py b/tests/unit/javascript/stylesheet/test_stylesheet_js.py index 13ec85cd5..17e46aee2 100644 --- a/tests/unit/javascript/stylesheet/test_stylesheet_js.py +++ b/tests/unit/javascript/stylesheet/test_stylesheet_js.py @@ -20,6 +20,7 @@ """Tests for stylesheet.js.""" import os +import pathlib import pytest QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets") @@ -49,7 +50,7 @@ class StylesheetTester: def init_stylesheet(self, css_file="green.css"): """Initialize the stylesheet with a provided css file.""" - css_path = os.path.join(os.path.dirname(__file__), css_file) + css_path = str(pathlib.Path(__file__).parent.joinpath(css_file)) self.config_stub.val.content.user_stylesheets = css_path def set_css(self, css): diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py index c28b9c8f7..a523a6bce 100644 --- a/tests/unit/javascript/test_greasemonkey.py +++ b/tests/unit/javascript/test_greasemonkey.py @@ -200,7 +200,7 @@ class TestForceDocumentEnd: assert script.needs_document_end_workaround() == force -def test_required_scripts_are_included(download_stub, tmpdir): +def test_required_scripts_are_included(download_stub, tmp_path): test_require_script = textwrap.dedent(""" // ==UserScript== // @name qutebrowser test userscript @@ -214,7 +214,7 @@ def test_required_scripts_are_included(download_stub, tmpdir): console.log("Script is running."); """) _save_script(test_require_script, 'requiring.user.js') - (tmpdir / 'test.js').write_text('REQUIRED SCRIPT', encoding='UTF-8') + (tmp_path / 'test.js').write_text('REQUIRED SCRIPT', encoding='UTF-8') gm_manager = greasemonkey.GreasemonkeyManager() assert len(gm_manager._in_progress_dls) == 1 -- cgit v1.2.3-54-g00ecf From cf30285b40fbac965e458efb51f376ad2812a8b4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 Mar 2021 18:20:34 +0100 Subject: Adjust imports --- qutebrowser/commands/parser.py | 2 +- qutebrowser/commands/runners.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index 0927d1940..e8efbfb42 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -19,7 +19,7 @@ """Module for parsing commands entered into the browser.""" -import attr +import dataclasses from qutebrowser.commands import cmdexc, command from qutebrowser.misc import split, objects diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 105887b0b..80822d625 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -22,7 +22,6 @@ import traceback import re import contextlib -import dataclasses from typing import (TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping, List, Optional) -- cgit v1.2.3-54-g00ecf From f72caf745185e661b08890692c3b6a03c932612a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 Mar 2021 18:24:53 +0100 Subject: Document and rename use_best_match --- qutebrowser/commands/parser.py | 12 +++++++----- qutebrowser/commands/runners.py | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index e8efbfb42..85e974965 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -110,7 +110,7 @@ class CommandParser: """Wrapper over _parse_all_gen.""" return list(self._parse_all_gen(*args, **kwargs)) - def parse(self, text, *, fallback=False, keep=False, best_match=False): + def parse(self, text, *, fallback=False, keep=False, use_best_match=False): """Split the commandline text into command and arguments. Args: @@ -118,6 +118,7 @@ class CommandParser: fallback: Whether to do a fallback splitting when the command was unknown. keep: Whether to keep special chars and whitespace + use_best_match: Whether to use the best match even if incomplete. Return: A ParseResult tuple. @@ -128,7 +129,7 @@ class CommandParser: raise cmdexc.NoSuchCommandError("No command given") if self._partial_match: - cmdstr = self._completion_match(cmdstr, best_match) + cmdstr = self._completion_match(cmdstr, use_best_match) try: cmd = objects.commands[cmdstr] @@ -149,11 +150,12 @@ class CommandParser: return ParseResult(cmd=cmd, args=args, cmdline=cmdline) - def _completion_match(self, cmdstr, best): + def _completion_match(self, cmdstr, use_best_match): """Replace cmdstr with a matching completion if there's only one match. Args: - cmdstr: The string representing the entered command so far + cmdstr: The string representing the entered command so far. + use_best_match: Whether to use the best match even if incomplete. Return: cmdstr modified to the matching completion or unmodified @@ -162,7 +164,7 @@ class CommandParser: if cmdstr in cmd] if len(matches) == 1: cmdstr = matches[0] - elif len(matches) > 1 and best: + elif len(matches) > 1 and use_best_match: cmdstr = matches[0] return cmdstr diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 80822d625..6d4ccdfc2 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -173,7 +173,7 @@ class CommandRunner(AbstractCommandRunner): parsed = None with self._handle_error(safely): parsed = self._parser.parse_all( - text, best_match=config.val.completion.use_best_match) + text, use_best_match=config.val.completion.use_best_match) if parsed is None: return -- cgit v1.2.3-54-g00ecf From ea70b091753c7b836a314d7b8a1e3ac2390c8f16 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 Mar 2021 18:25:46 +0100 Subject: Fix imports --- qutebrowser/commands/parser.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index 85e974965..bcc075608 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -20,6 +20,7 @@ """Module for parsing commands entered into the browser.""" import dataclasses +from typing import Optional, List from qutebrowser.commands import cmdexc, command from qutebrowser.misc import split, objects -- cgit v1.2.3-54-g00ecf From 570e5fac671f89e1107ec500f3e0cf740bcd5226 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 3 Mar 2021 18:42:13 +0100 Subject: WIP: Add type annotations for parser --- qutebrowser/commands/parser.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index bcc075608..ceebad57e 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -20,7 +20,7 @@ """Module for parsing commands entered into the browser.""" import dataclasses -from typing import Optional, List +from typing import Optional, List, Mapping, Iterator, List, Union from qutebrowser.commands import cmdexc, command from qutebrowser.misc import split, objects @@ -44,10 +44,10 @@ class CommandParser: _partial_match: Whether to allow partial command matches. """ - def __init__(self, partial_match=False): + def __init__(self, partial_match: bool = False) -> None: self._partial_match = partial_match - def _get_alias(self, text, aliases, default=None): + def _get_alias(self, text: str, aliases: Mapping[str, str], *, default: str) -> str: """Get an alias from the config. Args: @@ -72,7 +72,12 @@ class CommandParser: new_cmd += ' ' return new_cmd - def _parse_all_gen(self, text, *args, aliases=None, **kwargs): + def _parse_all_gen( + self, + text: str, + aliases: Mapping[str, str] = None, + **kwargs: bool, + ) -> Iterator[ParseResult]: """Split a command on ;; and parse all parts. If the first command in the commandline is a non-split one, it only @@ -81,7 +86,7 @@ class CommandParser: Args: text: Text to parse. aliases: A map of aliases to commands. - *args/**kwargs: Passed to parse(). + **kwargs: Passed to parse(). Yields: ParseResult tuples. @@ -91,13 +96,13 @@ class CommandParser: raise cmdexc.NoSuchCommandError("No command given") if aliases: - text = self._get_alias(text, aliases, text) + text = self._get_alias(text, aliases, default=text) if ';;' in text: # Get the first command and check if it doesn't want to have ;; # split. first = text.split(';;')[0] - result = self.parse(first, *args, **kwargs) + result = self.parse(first, **kwargs) if result.cmd.no_cmd_split: sub_texts = [text] else: @@ -105,13 +110,20 @@ class CommandParser: else: sub_texts = [text] for sub in sub_texts: - yield self.parse(sub, *args, **kwargs) + yield self.parse(first, **kwargs) - def parse_all(self, *args, **kwargs): + def parse_all(self, text: str, **kwargs: bool) -> List[ParseResult]: """Wrapper over _parse_all_gen.""" - return list(self._parse_all_gen(*args, **kwargs)) - - def parse(self, text, *, fallback=False, keep=False, use_best_match=False): + return list(self._parse_all_gen(text, **kwargs)) + + def parse( + self, + text: str, + *, + fallback: bool = False, + keep: bool = False, + use_best_match: bool = False, + ) -> ParseResult: """Split the commandline text into command and arguments. Args: @@ -151,7 +163,7 @@ class CommandParser: return ParseResult(cmd=cmd, args=args, cmdline=cmdline) - def _completion_match(self, cmdstr, use_best_match): + def _completion_match(self, cmdstr: str, use_best_match: bool) -> str: """Replace cmdstr with a matching completion if there's only one match. Args: @@ -169,7 +181,7 @@ class CommandParser: cmdstr = matches[0] return cmdstr - def _split_args(self, cmd, argstr, keep): + def _split_args(self, cmd: command.Command, argstr: str, keep: bool) -> List[str]: """Split the arguments from an arg string. Args: -- cgit v1.2.3-54-g00ecf From 744cd94469de77f52905bebb123597c040ac07b6 Mon Sep 17 00:00:00 2001 From: Lembrun Date: Wed, 10 Mar 2021 10:49:21 +0100 Subject: Removed unused import os in tests/unit/javascript --- tests/unit/javascript/conftest.py | 2 -- tests/unit/javascript/stylesheet/test_stylesheet_js.py | 1 - 2 files changed, 3 deletions(-) diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 2c93cf9ef..a62ddf2d5 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -19,9 +19,7 @@ """pytest conftest file for javascript tests.""" -import os import pathlib - import pytest import jinja2 diff --git a/tests/unit/javascript/stylesheet/test_stylesheet_js.py b/tests/unit/javascript/stylesheet/test_stylesheet_js.py index 17e46aee2..636af56d1 100644 --- a/tests/unit/javascript/stylesheet/test_stylesheet_js.py +++ b/tests/unit/javascript/stylesheet/test_stylesheet_js.py @@ -19,7 +19,6 @@ """Tests for stylesheet.js.""" -import os import pathlib import pytest -- cgit v1.2.3-54-g00ecf From c947488dd7894b64e7e6379808c038737fbdc8c4 Mon Sep 17 00:00:00 2001 From: Lembrun Date: Thu, 11 Mar 2021 08:36:57 +0100 Subject: Changed joinpath to / --- tests/unit/javascript/conftest.py | 4 ++-- tests/unit/javascript/stylesheet/test_stylesheet_js.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index a62ddf2d5..1da3d8b5e 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -85,7 +85,7 @@ class JSTester: force: Whether to force loading even if the file is invalid. """ self.load_url(QUrl.fromLocalFile( - str(pathlib.Path(__file__).parent.joinpath(path))), force) + str(pathlib.Path(__file__).parent / path)), force) def load_url(self, url: QUrl, force: bool = False): """Load a given QUrl. @@ -108,7 +108,7 @@ class JSTester: expected: The value expected return from the javascript execution """ base_path = pathlib.Path(qutebrowser.__file__).resolve().parent - with open(pathlib.Path(base_path).joinpath(path), 'r', encoding='utf-8') as f: + with (base_path / path).open('r', encoding='utf-8') as f: source = f.read() self.run(source, expected) diff --git a/tests/unit/javascript/stylesheet/test_stylesheet_js.py b/tests/unit/javascript/stylesheet/test_stylesheet_js.py index 636af56d1..ce0f0830d 100644 --- a/tests/unit/javascript/stylesheet/test_stylesheet_js.py +++ b/tests/unit/javascript/stylesheet/test_stylesheet_js.py @@ -49,7 +49,7 @@ class StylesheetTester: def init_stylesheet(self, css_file="green.css"): """Initialize the stylesheet with a provided css file.""" - css_path = str(pathlib.Path(__file__).parent.joinpath(css_file)) + css_path = str(pathlib.Path(__file__).parent / css_file) self.config_stub.val.content.user_stylesheets = css_path def set_css(self, css): -- cgit v1.2.3-54-g00ecf From 148d5f25ef21f7ca888faf71dae914784de1e4dd Mon Sep 17 00:00:00 2001 From: Lembrun Date: Thu, 11 Mar 2021 20:13:34 +0100 Subject: Created JS_DIR and moved str(css_path) --- tests/unit/javascript/conftest.py | 5 +++-- tests/unit/javascript/stylesheet/test_stylesheet_js.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 1da3d8b5e..7cd3204c4 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -28,6 +28,7 @@ from PyQt5.QtCore import QUrl import qutebrowser from qutebrowser.utils import usertypes +JS_DIR = pathlib.Path(__file__).parent class JSTester: @@ -42,7 +43,7 @@ class JSTester: def __init__(self, tab, qtbot, config_stub): self.tab = tab self.qtbot = qtbot - loader = jinja2.FileSystemLoader(pathlib.Path(__file__).parent) + loader = jinja2.FileSystemLoader(JS_DIR) self._jinja_env = jinja2.Environment(loader=loader, autoescape=True) # Make sure error logging via JS fails tests config_stub.val.content.javascript.log = { @@ -85,7 +86,7 @@ class JSTester: force: Whether to force loading even if the file is invalid. """ self.load_url(QUrl.fromLocalFile( - str(pathlib.Path(__file__).parent / path)), force) + str(JS_DIR / path)), force) def load_url(self, url: QUrl, force: bool = False): """Load a given QUrl. diff --git a/tests/unit/javascript/stylesheet/test_stylesheet_js.py b/tests/unit/javascript/stylesheet/test_stylesheet_js.py index ce0f0830d..1eebe3b7f 100644 --- a/tests/unit/javascript/stylesheet/test_stylesheet_js.py +++ b/tests/unit/javascript/stylesheet/test_stylesheet_js.py @@ -49,8 +49,8 @@ class StylesheetTester: def init_stylesheet(self, css_file="green.css"): """Initialize the stylesheet with a provided css file.""" - css_path = str(pathlib.Path(__file__).parent / css_file) - self.config_stub.val.content.user_stylesheets = css_path + css_path = pathlib.Path(__file__).parent / css_file + self.config_stub.val.content.user_stylesheets = str(css_path) def set_css(self, css): """Set document style to `css` via stylesheet.js.""" -- cgit v1.2.3-54-g00ecf From 59e0449471cef1c227af6f2b4c9d50a3c62299c7 Mon Sep 17 00:00:00 2001 From: Lembrun Date: Thu, 11 Mar 2021 20:19:18 +0100 Subject: Added blank line --- tests/unit/javascript/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index 7cd3204c4..d63a702f4 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -30,6 +30,7 @@ from qutebrowser.utils import usertypes JS_DIR = pathlib.Path(__file__).parent + class JSTester: """Common subclass providing basic functionality for all JS testers. -- cgit v1.2.3-54-g00ecf From 880dd82be811baa34c5d390caf5796b869b2ab64 Mon Sep 17 00:00:00 2001 From: Lembrun Date: Fri, 12 Mar 2021 22:29:50 +0100 Subject: Changing path.open to read_text --- tests/unit/javascript/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py index d63a702f4..4a7f09204 100644 --- a/tests/unit/javascript/conftest.py +++ b/tests/unit/javascript/conftest.py @@ -110,8 +110,7 @@ class JSTester: expected: The value expected return from the javascript execution """ base_path = pathlib.Path(qutebrowser.__file__).resolve().parent - with (base_path / path).open('r', encoding='utf-8') as f: - source = f.read() + source = (base_path / path).read_text(encoding='utf-8') self.run(source, expected) def run(self, source: str, expected=usertypes.UNSET, world=None) -> None: -- cgit v1.2.3-54-g00ecf From 2ef375ac784985212b1805e1d0431dc8f1b3c171 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Mar 2021 10:56:17 +0100 Subject: Add input.media_keys setting --- doc/changelog.asciidoc | 10 ++++++++++ doc/help/settings.asciidoc | 16 ++++++++++++++++ qutebrowser/config/configdata.yml | 12 ++++++++++++ qutebrowser/config/qtargs.py | 3 +++ 4 files changed, 41 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 2d399ad6d..f6604f192 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -15,6 +15,16 @@ breaking changes (such as renamed commands) can happen in minor releases. // `Fixed` for any bug fixes. // `Security` to invite users to upgrade in case of vulnerabilities. +[[v2.2.0]] +v2.2.0 (unreleased) +------------------- + +Added +~~~~~ + +- New `input.media_keys` setting which can be used to disable Chromium's + handling of media keys. + [[v2.1.1]] v2.1.1 (unreleased) ------------------- diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 0ecd7d753..8b2964f4f 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -261,6 +261,7 @@ |<>|Leave insert mode when starting a new page load. |<>|Switch to insert mode when clicking flash and other plugins. |<>|Include hyperlinks in the keyboard focus chain when tabbing. +|<>|Whether the underlying Chromium should handle media keys. |<>|Enable back and forward buttons on the mouse. |<>|Enable Opera-like mouse rocker gestures. |<>|Timeout (in milliseconds) for partially typed key bindings. @@ -3392,6 +3393,21 @@ Type: <> Default: +pass:[true]+ +[[input.media_keys]] +=== input.media_keys +Whether the underlying Chromium should handle media keys. +On Linux, disabling this also disables Chromium's MPRIS integration. + +This setting requires a restart. + +On QtWebEngine, this setting requires Qt 5.14 or newer. + +On QtWebKit, this setting is unavailable. + +Type: <> + +Default: +pass:[true]+ + [[input.mouse.back_forward_buttons]] === input.mouse.back_forward_buttons Enable back and forward buttons on the mouse. diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 22a4b2151..45d8d1a7c 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -1613,6 +1613,18 @@ input.spatial_navigation: Right key, heuristics determine whether there is an element he might be trying to reach towards the right and which element he probably wants. +input.media_keys: + default: true + type: Bool + backend: + QtWebEngine: Qt 5.14 + QtWebKit: false + restart: true + desc: >- + Whether the underlying Chromium should handle media keys. + + On Linux, disabling this also disables Chromium's MPRIS integration. + ## keyhint keyhint.blacklist: diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py index 407ccb37e..d9564556a 100644 --- a/qutebrowser/config/qtargs.py +++ b/qutebrowser/config/qtargs.py @@ -157,6 +157,9 @@ def _qtwebengine_features( # WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89740 disabled_features.append('InstalledApp') + if not config.val.input.media_keys: + disabled_features.append('HardwareMediaKeyHandling') + return (enabled_features, disabled_features) -- cgit v1.2.3-54-g00ecf From a2531d5b5229fb2c658fa8c4ff2cea9284806746 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 16 Mar 2021 19:13:38 +0100 Subject: Revive requirements-tests-git See #6298 --- misc/requirements/requirements-tests-git.txt | 45 +++++++++++++++------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt index 6fc4bb460..cbefb99f3 100644 --- a/misc/requirements/requirements-tests-git.txt +++ b/misc/requirements/requirements-tests-git.txt @@ -1,34 +1,37 @@ -bzr+lp:beautifulsoup +# Problematic: needs bzr +# bzr+lp:beautifulsoup +beautifulsoup4 git+https://github.com/cherrypy/cheroot.git -hg+https://bitbucket.org/ned/coveragepy -git+https://github.com/micheles/decorator.git +git+https://github.com/nedbat/coveragepy.git git+https://github.com/pallets/flask.git -git+https://github.com/miracle2k/python-glob2.git -git+https://github.com/HypothesisWorks/hypothesis-python.git -git+https://github.com/pallets/itsdangerous.git -git+https://bitbucket.org/zzzeek/mako.git -git+https://github.com/r1chardj0n3s/parse.git -git+https://github.com/jenisys/parse_type.git -hg+https://bitbucket.org/pytest-dev/py -git+https://github.com/pytest-dev/pytest.git@features +git+https://github.com/pallets/werkzeug.git # transitive dep, but needed to work +git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-python +git+https://github.com/pytest-dev/pytest.git git+https://github.com/pytest-dev/pytest-bdd.git -git+https://github.com/pytest-dev/pytest-cov.git +git+https://github.com/ionelmc/pytest-benchmark.git git+https://github.com/pytest-dev/pytest-instafail.git git+https://github.com/pytest-dev/pytest-mock.git git+https://github.com/pytest-dev/pytest-qt.git -git+https://github.com/pytest-dev/pytest-repeat.git git+https://github.com/pytest-dev/pytest-rerunfailures.git -git+https://github.com/The-Compiler/pytest-xvfb.git -hg+https://bitbucket.org/gutworth/six -hg+https://bitbucket.org/jendrikseipp/vulture -git+https://github.com/pallets/werkzeug.git +git+https://github.com/ionelmc/python-hunter.git +git+https://github.com/jendrikseipp/vulture.git +git+https://github.com/pygments/pygments.git +git+https://github.com/pytest-dev/pytest-repeat.git +git+https://github.com/pytest-dev/pytest-cov.git +git+https://github.com/The-Compiler/pytest-xvfb.git +git+https://github.com/pytest-dev/pytest-xdist.git +git+https://github.com/hjwp/pytest-icdiff.git +git+https://github.com/john-kurkowski/tldextract +# Problematic: needs rust (and some time to build) +# git+https://github.com/ArniDagur/python-adblock.git +adblock ; python_version!="3.10" ## qutebrowser dependencies -git+https://github.com/tartley/colorama.git git+https://github.com/pallets/jinja.git -git+https://github.com/pallets/markupsafe.git -git+https://github.com/pygments/pygments.git -git+https://github.com/python-attrs/attrs.git git+https://github.com/yaml/pyyaml.git +git+https://github.com/tartley/colorama.git + +# https://github.com/pyparsing/pyparsing/issues/271 +pyparsing!=3.0.0b2,!=3.0.0b1 -- cgit v1.2.3-54-g00ecf From 32d3f1d256f41a5a4af6fee2a442922a47930fd3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 13:20:00 +0100 Subject: tests: Also accept PARTIAL_CONTENT for favicon Seems to be used with bleeding-edge Flask/Werkzeug versions See #6298 --- tests/end2end/fixtures/webserver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/end2end/fixtures/webserver.py b/tests/end2end/fixtures/webserver.py index 81a864c8e..658ff0e56 100644 --- a/tests/end2end/fixtures/webserver.py +++ b/tests/end2end/fixtures/webserver.py @@ -62,7 +62,11 @@ class Request(testprocess.Line): def _check_status(self): """Check if the http status is what we expected.""" path_to_statuses = { - '/favicon.ico': [HTTPStatus.OK, HTTPStatus.PARTIAL_CONTENT], + '/favicon.ico': [ + HTTPStatus.OK, + HTTPStatus.PARTIAL_CONTENT, + HTTPStatus.NOT_MODIFIED, + ], '/does-not-exist': [HTTPStatus.NOT_FOUND], '/does-not-exist-2': [HTTPStatus.NOT_FOUND], -- cgit v1.2.3-54-g00ecf From 7b24ac9df77a5601836ceecb1cc39bd54f7792f8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 13:56:01 +0100 Subject: tox: Add a bleeding environment See #6298 --- misc/requirements/requirements-tests-bleeding.txt | 37 +++++++++++++++++++++++ misc/requirements/requirements-tests-git.txt | 37 ----------------------- tox.ini | 10 ++++++ 3 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 misc/requirements/requirements-tests-bleeding.txt delete mode 100644 misc/requirements/requirements-tests-git.txt diff --git a/misc/requirements/requirements-tests-bleeding.txt b/misc/requirements/requirements-tests-bleeding.txt new file mode 100644 index 000000000..cbefb99f3 --- /dev/null +++ b/misc/requirements/requirements-tests-bleeding.txt @@ -0,0 +1,37 @@ +# Problematic: needs bzr +# bzr+lp:beautifulsoup +beautifulsoup4 +git+https://github.com/cherrypy/cheroot.git +git+https://github.com/nedbat/coveragepy.git +git+https://github.com/pallets/flask.git +git+https://github.com/pallets/werkzeug.git # transitive dep, but needed to work +git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-python +git+https://github.com/pytest-dev/pytest.git +git+https://github.com/pytest-dev/pytest-bdd.git +git+https://github.com/ionelmc/pytest-benchmark.git +git+https://github.com/pytest-dev/pytest-instafail.git +git+https://github.com/pytest-dev/pytest-mock.git +git+https://github.com/pytest-dev/pytest-qt.git +git+https://github.com/pytest-dev/pytest-rerunfailures.git + +git+https://github.com/ionelmc/python-hunter.git +git+https://github.com/jendrikseipp/vulture.git +git+https://github.com/pygments/pygments.git +git+https://github.com/pytest-dev/pytest-repeat.git +git+https://github.com/pytest-dev/pytest-cov.git +git+https://github.com/The-Compiler/pytest-xvfb.git +git+https://github.com/pytest-dev/pytest-xdist.git +git+https://github.com/hjwp/pytest-icdiff.git +git+https://github.com/john-kurkowski/tldextract +# Problematic: needs rust (and some time to build) +# git+https://github.com/ArniDagur/python-adblock.git +adblock ; python_version!="3.10" + +## qutebrowser dependencies + +git+https://github.com/pallets/jinja.git +git+https://github.com/yaml/pyyaml.git +git+https://github.com/tartley/colorama.git + +# https://github.com/pyparsing/pyparsing/issues/271 +pyparsing!=3.0.0b2,!=3.0.0b1 diff --git a/misc/requirements/requirements-tests-git.txt b/misc/requirements/requirements-tests-git.txt deleted file mode 100644 index cbefb99f3..000000000 --- a/misc/requirements/requirements-tests-git.txt +++ /dev/null @@ -1,37 +0,0 @@ -# Problematic: needs bzr -# bzr+lp:beautifulsoup -beautifulsoup4 -git+https://github.com/cherrypy/cheroot.git -git+https://github.com/nedbat/coveragepy.git -git+https://github.com/pallets/flask.git -git+https://github.com/pallets/werkzeug.git # transitive dep, but needed to work -git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-python -git+https://github.com/pytest-dev/pytest.git -git+https://github.com/pytest-dev/pytest-bdd.git -git+https://github.com/ionelmc/pytest-benchmark.git -git+https://github.com/pytest-dev/pytest-instafail.git -git+https://github.com/pytest-dev/pytest-mock.git -git+https://github.com/pytest-dev/pytest-qt.git -git+https://github.com/pytest-dev/pytest-rerunfailures.git - -git+https://github.com/ionelmc/python-hunter.git -git+https://github.com/jendrikseipp/vulture.git -git+https://github.com/pygments/pygments.git -git+https://github.com/pytest-dev/pytest-repeat.git -git+https://github.com/pytest-dev/pytest-cov.git -git+https://github.com/The-Compiler/pytest-xvfb.git -git+https://github.com/pytest-dev/pytest-xdist.git -git+https://github.com/hjwp/pytest-icdiff.git -git+https://github.com/john-kurkowski/tldextract -# Problematic: needs rust (and some time to build) -# git+https://github.com/ArniDagur/python-adblock.git -adblock ; python_version!="3.10" - -## qutebrowser dependencies - -git+https://github.com/pallets/jinja.git -git+https://github.com/yaml/pyyaml.git -git+https://github.com/tartley/colorama.git - -# https://github.com/pyparsing/pyparsing/issues/271 -pyparsing!=3.0.0b2,!=3.0.0b1 diff --git a/tox.ini b/tox.ini index 70329f9e6..e305e5c4d 100644 --- a/tox.ini +++ b/tox.ini @@ -38,6 +38,16 @@ commands = {envpython} -bb -m pytest {posargs:tests} cov: {envpython} scripts/dev/check_coverage.py {posargs} +[testenv:bleeding] +basepython = {env:PYTHON:python3} +setenv = + PYTEST_QT_API=pyqt5 + QUTE_BDD_WEBENGINE=true +pip_pre = true +deps = -r{toxinidir}/misc/requirements/requirements-tests-bleeding.txt +commands_pre = pip install --index-url https://www.riverbankcomputing.com/pypi/simple/ --pre --upgrade PyQt5 PyQtWebEngine +commands = {envpython} -bb -m pytest {posargs:tests} + # other envs [testenv:misc] -- cgit v1.2.3-54-g00ecf From b062d7364cc9cae43cc25762517fb8fc3d1e97ba Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 17:23:45 +0100 Subject: Warn about mismatching QtWebEngine versions See https://github.com/flathub/org.qutebrowser.qutebrowser/issues/11 --- qutebrowser/browser/webengine/webenginesettings.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 830c818fc..090cdfd4c 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -38,7 +38,7 @@ from qutebrowser.browser.webengine import (spell, webenginequtescheme, cookies, from qutebrowser.config import config, websettings from qutebrowser.config.websettings import AttributeInfo as Attr from qutebrowser.utils import (standarddir, qtutils, message, log, - urlmatch, usertypes, objreg) + urlmatch, usertypes, objreg, version) if TYPE_CHECKING: from qutebrowser.browser.webengine import interceptor @@ -374,7 +374,17 @@ def _init_default_profile(): default_profile = QWebEngineProfile.defaultProfile() + assert parsed_user_agent is None # avoid earlier profile initialization + non_ua_version = version.qtwebengine_versions(avoid_init=True) + init_user_agent() + ua_version = version.qtwebengine_versions() + if ua_version.webengine != non_ua_version.webengine: + log.init.warning( + "QtWebEngine version mismatch - unexpected behavior might occur, " + "please open a bug about this.\n" + f" Early version: {non_ua_version}\n" + f" Real version: {ua_version}") default_profile.setCachePath( os.path.join(standarddir.cache(), 'webengine')) -- cgit v1.2.3-54-g00ecf From 7ae7b6ea1a20c8379ae072eea6bd1449788852a6 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 17:25:52 +0100 Subject: Fix version parsing with Flatpak See https://github.com/flathub/org.qutebrowser.qutebrowser/issues/11 --- qutebrowser/misc/elf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py index 1da4709af..4d44e56ac 100644 --- a/qutebrowser/misc/elf.py +++ b/qutebrowser/misc/elf.py @@ -69,7 +69,7 @@ from typing import IO, ClassVar, Dict, Optional, Tuple, cast from PyQt5.QtCore import QLibraryInfo -from qutebrowser.utils import log +from qutebrowser.utils import log, version class ParseError(Exception): @@ -310,7 +310,11 @@ def _parse_from_file(f: IO[bytes]) -> Versions: def parse_webenginecore() -> Optional[Versions]: """Parse the QtWebEngineCore library file.""" - library_path = pathlib.Path(QLibraryInfo.location(QLibraryInfo.LibrariesPath)) + if version.is_sandboxed(): + # Flatpak has Qt in /usr/lib/x86_64-linux-gnu, but QtWebEngine in /app/lib. + library_path = pathlib.Path("/app/lib") + else: + library_path = pathlib.Path(QLibraryInfo.location(QLibraryInfo.LibrariesPath)) # PyQt bundles those files with a .5 suffix lib_file = library_path / 'libQt5WebEngineCore.so.5' -- cgit v1.2.3-54-g00ecf From 1a3a24d17c87fe661b7414516a9908640ad8505e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 17:27:00 +0100 Subject: Update changelog --- doc/changelog.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index f6604f192..fa6186132 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -40,6 +40,7 @@ Fixed - The workaround for black on (almost) black formula images in dark mode now also works with Qt 5.12 and 5.13. +- When running in Flatpak, the QtWebEngine version is now detected properly. [[v2.1.0]] v2.1.0 (2021-03-12) -- cgit v1.2.3-54-g00ecf From d56e01118c9272e1bd2557a88858dc5eae05d01c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 17:34:23 +0100 Subject: Simplify patching in test_qtargs.py --- tests/unit/config/test_qtargs.py | 65 +++++++++++----------------------------- 1 file changed, 17 insertions(+), 48 deletions(-) diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index 695649213..e12eeaea9 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -52,10 +52,14 @@ def version_patcher(monkeypatch): @pytest.fixture -def reduce_args(config_stub, version_patcher): +def reduce_args(config_stub, version_patcher, monkeypatch): """Make sure no --disable-shared-workers/referer argument get added.""" - version_patcher('5.15.0') + version_patcher('5.15.3') config_stub.val.content.headers.referer = 'always' + config_stub.val.scrolling.bar = 'never' + monkeypatch.setattr(qtargs.utils, 'is_mac', False) + # Avoid WebRTC pipewire feature + monkeypatch.setattr(qtargs.utils, 'is_linux', False) @pytest.mark.usefixtures('reduce_args') @@ -78,11 +82,6 @@ class TestQtArgs: ]) def test_qt_args(self, monkeypatch, config_stub, args, expected, parser): """Test commandline with no Qt arguments given.""" - # Avoid scrollbar overlay argument - config_stub.val.scrolling.bar = 'never' - # Avoid WebRTC pipewire feature - monkeypatch.setattr(qtargs.utils, 'is_linux', False) - parsed = parser.parse_args(args) assert qtargs.qt_args(parsed) == expected @@ -126,9 +125,10 @@ def test_no_webengine_available(monkeypatch, config_stub, parser, stubs): class TestWebEngineArgs: @pytest.fixture(autouse=True) - def ensure_webengine(self): + def ensure_webengine(self, monkeypatch): """Skip all tests if QtWebEngine is unavailable.""" pytest.importorskip("PyQt5.QtWebEngine") + monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) @pytest.mark.parametrize('backend, qt_version, expected', [ (usertypes.Backend.QtWebEngine, '5.13.0', False), @@ -184,7 +184,6 @@ class TestWebEngineArgs: (['--debug-flag', 'wait-renderer-process'], ['--renderer-startup-dialog']), ]) def test_chromium_flags(self, monkeypatch, parser, flags, args): - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) parsed = parser.parse_args(flags) args = qtargs.qt_args(parsed) @@ -203,7 +202,6 @@ class TestWebEngineArgs: ('chromium', True), ]) def test_disable_gpu(self, config, added, config_stub, monkeypatch, parser): - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) config_stub.val.qt.force_software_rendering = config parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -225,7 +223,6 @@ class TestWebEngineArgs: 'disable_non_proxied_udp'), ]) def test_webrtc(self, config_stub, monkeypatch, parser, policy, arg): - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) config_stub.val.content.webrtc_ip_handling_policy = policy parsed = parser.parse_args([]) @@ -241,10 +238,7 @@ class TestWebEngineArgs: (True, False), # canvas reading enabled (False, True), ]) - def test_canvas_reading(self, config_stub, monkeypatch, parser, - canvas_reading, added): - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) - + def test_canvas_reading(self, config_stub, parser, canvas_reading, added): config_stub.val.content.canvas_reading = canvas_reading parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -255,10 +249,7 @@ class TestWebEngineArgs: ('process-per-site', True), ('single-process', True), ]) - def test_process_model(self, config_stub, monkeypatch, parser, - process_model, added): - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) - + def test_process_model(self, config_stub, parser, process_model, added): config_stub.val.qt.process_model = process_model parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -276,10 +267,7 @@ class TestWebEngineArgs: ('always', '--enable-low-end-device-mode'), ('never', '--disable-low-end-device-mode'), ]) - def test_low_end_device_mode(self, config_stub, monkeypatch, parser, - low_end_device_mode, arg): - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) - + def test_low_end_device_mode(self, config_stub, parser, low_end_device_mode, arg): config_stub.val.qt.low_end_device_mode = low_end_device_mode parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -307,16 +295,10 @@ class TestWebEngineArgs: ('5.14.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'), ('5.15.0', 'same-domain', '--enable-features=ReducedReferrerGranularity'), ]) - def test_referer(self, config_stub, monkeypatch, version_patcher, parser, + def test_referer(self, config_stub, version_patcher, parser, qt_version, referer, arg): - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) version_patcher(qt_version) - # Avoid WebRTC pipewire feature - monkeypatch.setattr(qtargs.utils, 'is_linux', False) - # Avoid overlay scrollbar feature - config_stub.val.scrolling.bar = 'never' - config_stub.val.content.headers.referer = referer parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) @@ -380,10 +362,7 @@ class TestWebEngineArgs: ]) def test_overlay_scrollbar(self, config_stub, monkeypatch, parser, bar, is_mac, added): - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) monkeypatch.setattr(qtargs.utils, 'is_mac', is_mac) - # Avoid WebRTC pipewire feature - monkeypatch.setattr(qtargs.utils, 'is_linux', False) config_stub.val.scrolling.bar = bar @@ -392,15 +371,6 @@ class TestWebEngineArgs: assert ('--enable-features=OverlayScrollbar' in args) == added - @pytest.fixture - def feature_flag_patch(self, monkeypatch, config_stub, version_patcher): - """Patch away things affecting feature flags.""" - config_stub.val.scrolling.bar = 'never' - version_patcher('5.15.3') - monkeypatch.setattr(qtargs.utils, 'is_mac', False) - # Avoid WebRTC pipewire feature - monkeypatch.setattr(qtargs.utils, 'is_linux', False) - @pytest.mark.parametrize('via_commandline', [True, False]) @pytest.mark.parametrize('overlay, passed_features, expected_features', [ (True, @@ -413,7 +383,7 @@ class TestWebEngineArgs: 'CustomFeature', 'CustomFeature'), ]) - def test_overlay_features_flag(self, config_stub, parser, feature_flag_patch, + def test_overlay_features_flag(self, config_stub, parser, via_commandline, overlay, passed_features, expected_features): """If enable-features is already specified, we should combine both.""" @@ -442,7 +412,7 @@ class TestWebEngineArgs: ['CustomFeature'], ['CustomFeature1', 'CustomFeature2'], ]) - def test_disable_features_passthrough(self, config_stub, parser, feature_flag_patch, + def test_disable_features_passthrough(self, config_stub, parser, via_commandline, passed_features): flag = qtargs._DISABLE_FEATURES + ','.join(passed_features) @@ -458,7 +428,7 @@ class TestWebEngineArgs: ] assert disable_features_args == [flag] - def test_blink_settings_passthrough(self, parser, config_stub, feature_flag_patch): + def test_blink_settings_passthrough(self, parser, config_stub): config_stub.val.colors.webpage.darkmode.enabled = True flag = qtargs._BLINK_SETTINGS + 'foo=bar' @@ -518,7 +488,6 @@ class TestWebEngineArgs: def test_dark_mode_settings(self, config_stub, monkeypatch, parser, variant, expected): from qutebrowser.browser.webengine import darkmode - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) monkeypatch.setattr( darkmode, '_variant', lambda _versions: darkmode.Variant[variant]) @@ -530,7 +499,6 @@ class TestWebEngineArgs: for arg in expected: assert arg in args - @pytest.mark.linux def test_locale_workaround(self, config_stub, monkeypatch, version_patcher, parser): class FakeLocale: @@ -538,9 +506,10 @@ class TestWebEngineArgs: def bcp47Name(self): return 'de-CH' - monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) monkeypatch.setattr(qtargs, 'QLocale', FakeLocale) + monkeypatch.setattr(qtargs.utils, 'is_linux', True) version_patcher('5.15.3') + config_stub.val.qt.workarounds.locale = True parsed = parser.parse_args([]) args = qtargs.qt_args(parsed) -- cgit v1.2.3-54-g00ecf From 07687ef24c8b74da7a86049927e5fa5f89d37314 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 17:36:52 +0100 Subject: Add missing test for HardwareMediaKeyHandling --- tests/unit/config/test_qtargs.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index e12eeaea9..9d24b97a5 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -462,6 +462,15 @@ class TestWebEngineArgs: expected = ['--disable-features=InstalledApp'] if has_workaround else [] assert disable_features_args == expected + @pytest.mark.parametrize('enabled', [True, False]) + def test_media_keys(self, config_stub, parser, enabled): + config_stub.val.input.media_keys = enabled + + parsed = parser.parse_args([]) + args = qtargs.qt_args(parsed) + + assert ('--disable-features=HardwareMediaKeyHandling' in args) != enabled + @pytest.mark.parametrize('variant, expected', [ ( 'qt_515_1', -- cgit v1.2.3-54-g00ecf From 3d06fca8a61a66326cd00171241c08ad3a82c383 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 18:43:28 +0100 Subject: Only run test_locale_workaround on Linux Regressed in d56e01118c9272e1bd2557a88858dc5eae05d01c - needs to access the qtwebengine_locales dir which is probably in another location on non-Linux (especially macOS). --- tests/unit/config/test_qtargs.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index 9d24b97a5..286caa369 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -508,15 +508,14 @@ class TestWebEngineArgs: for arg in expected: assert arg in args - def test_locale_workaround(self, config_stub, monkeypatch, version_patcher, - parser): + @pytest.mark.linux + def test_locale_workaround(self, config_stub, monkeypatch, version_patcher, parser): class FakeLocale: def bcp47Name(self): return 'de-CH' monkeypatch.setattr(qtargs, 'QLocale', FakeLocale) - monkeypatch.setattr(qtargs.utils, 'is_linux', True) version_patcher('5.15.3') config_stub.val.qt.workarounds.locale = True -- cgit v1.2.3-54-g00ecf From 567ff75ca9c2a646cb21216a668e98d8721edc3a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 18:46:14 +0100 Subject: Fix Qt version for test_media_keys --- tests/unit/config/test_qtargs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index 286caa369..7307aaecf 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -463,6 +463,7 @@ class TestWebEngineArgs: assert disable_features_args == expected @pytest.mark.parametrize('enabled', [True, False]) + @testutils.qt514 def test_media_keys(self, config_stub, parser, enabled): config_stub.val.input.media_keys = enabled -- cgit v1.2.3-54-g00ecf From ff341513afa1dad95ea71b52d654bb32512a8042 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 18:50:27 +0100 Subject: Fix shadowed name --- qutebrowser/misc/elf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py index 4d44e56ac..6d35eecb4 100644 --- a/qutebrowser/misc/elf.py +++ b/qutebrowser/misc/elf.py @@ -141,7 +141,7 @@ class Ident: @classmethod def parse(cls, fobj: IO[bytes]) -> 'Ident': """Parse an ELF ident header from a file.""" - magic, klass, data, version, osabi, abiversion = _unpack(cls._FORMAT, fobj) + magic, klass, data, elfversion, osabi, abiversion = _unpack(cls._FORMAT, fobj) try: bitness = Bitness(klass) @@ -153,7 +153,7 @@ class Ident: except ValueError: raise ParseError(f"Invalid endianness {data}") - return cls(magic, bitness, endianness, version, osabi, abiversion) + return cls(magic, bitness, endianness, elfversion, osabi, abiversion) @dataclasses.dataclass -- cgit v1.2.3-54-g00ecf From 7cee566026b2bc76a280ad7610ccaa6d3ce5d838 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 18:50:38 +0100 Subject: Fix spell check regex --- scripts/dev/misc_checks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 4c913cd3d..bae51e372 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -155,7 +155,7 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: """Check commonly misspelled words.""" # Words which I often misspell words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully', - 'occur[^rs .!]', 'seperator', 'explicitely', 'auxillary', + 'occur[^rs .!,]', 'seperator', 'explicitely', 'auxillary', 'accidentaly', 'ambigious', 'loosly', 'initialis', 'convienence', 'similiar', 'uncommited', 'reproducable', 'an user', 'convienience', 'wether', 'programatically', 'splitted', -- cgit v1.2.3-54-g00ecf From 4ba0ec591aa1c7cfa36a490978ddf516d1e82bad Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 20:37:34 +0100 Subject: Fix test_locale_workaround --- tests/unit/config/test_qtargs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index 7307aaecf..d7e24ae0a 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -516,6 +516,7 @@ class TestWebEngineArgs: def bcp47Name(self): return 'de-CH' + monkeypatch.setattr(qtargs.utils, 'is_linux', True) # patched in reduce_args monkeypatch.setattr(qtargs, 'QLocale', FakeLocale) version_patcher('5.15.3') -- cgit v1.2.3-54-g00ecf From ec919b0f790e2290fb13459ed9043fcb5708143e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 20:52:04 +0100 Subject: Disable libgl workaround except on Qt 5.12.0 --- qutebrowser/misc/backendproblem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 4751e1cea..27b98777a 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -198,7 +198,11 @@ class _BackendProblemChecker: """Work around QOpenGLShaderProgram issues. See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 + and https://bugreports.qt.io/browse/QTBUG-71488 """ + # https://codereview.qt-project.org/c/qt/qtbase/+/245294 + if qtutils.version_check('5.12.1', compiled=False): + return self._assert_backend(usertypes.Backend.QtWebEngine) utils.libgl_workaround() -- cgit v1.2.3-54-g00ecf From 3309ee65ede64286eb231c9203696b736ff7bcf3 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 17 Mar 2021 21:01:20 +0100 Subject: Drop libgl workaround entirely --- doc/changelog.asciidoc | 7 +++++++ qutebrowser/misc/backendproblem.py | 13 ------------- qutebrowser/misc/earlyinit.py | 5 +++++ qutebrowser/utils/utils.py | 15 --------------- qutebrowser/utils/version.py | 3 --- tests/conftest.py | 6 ------ tests/unit/utils/test_utils.py | 7 ------- 7 files changed, 12 insertions(+), 44 deletions(-) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index fa6186132..9c7a69d3c 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -19,6 +19,13 @@ breaking changes (such as renamed commands) can happen in minor releases. v2.2.0 (unreleased) ------------------- +Deprecated +~~~~~~~~~~ + +- Running qutebrowser with Qt 5.12.0 is now unsupported and logs a warning. It + should still work, however, a workaround for issues with the Nvidia graphic + driver was dropped. Newer Qt 5.12.x versions are still fully supported. + Added ~~~~~ diff --git a/qutebrowser/misc/backendproblem.py b/qutebrowser/misc/backendproblem.py index 27b98777a..001aa3047 100644 --- a/qutebrowser/misc/backendproblem.py +++ b/qutebrowser/misc/backendproblem.py @@ -194,18 +194,6 @@ class _BackendProblemChecker: sys.exit(usertypes.Exit.err_init) - def _nvidia_shader_workaround(self) -> None: - """Work around QOpenGLShaderProgram issues. - - See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 - and https://bugreports.qt.io/browse/QTBUG-71488 - """ - # https://codereview.qt-project.org/c/qt/qtbase/+/245294 - if qtutils.version_check('5.12.1', compiled=False): - return - self._assert_backend(usertypes.Backend.QtWebEngine) - utils.libgl_workaround() - def _xwayland_options(self) -> Tuple[str, List[_Button]]: """Get buttons/text for a possible XWayland solution.""" buttons = [] @@ -439,7 +427,6 @@ class _BackendProblemChecker: self._check_backend_modules() if objects.backend == usertypes.Backend.QtWebEngine: self._handle_ssl_support() - self._nvidia_shader_workaround() self._handle_wayland_webgl() self._handle_cache_nuking() self._handle_serviceworker_nuking() diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index 420f90f9a..b154eaca8 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -185,6 +185,11 @@ def check_qt_version(): PYQT_VERSION_STR)) _die(text) + if qt_ver == QVersionNumber(5, 12, 0): + from qutebrowser.utils import log + log.init.warning("Running on Qt 5.12.0. Doing so is unsupported " + "(newer 5.12.x versions are fine).") + def check_ssl_support(): """Check if SSL support is available.""" diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 03a3c7842..afc4c4f8d 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -32,8 +32,6 @@ import functools import contextlib import shlex import mimetypes -import ctypes -import ctypes.util from typing import (Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union, TypeVar, TYPE_CHECKING) @@ -753,19 +751,6 @@ def ceil_log(number: int, base: int) -> int: return result -def libgl_workaround() -> None: - """Work around QOpenGLShaderProgram issues, especially for Nvidia. - - See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826 - """ - if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'): - return - - libgl = ctypes.util.find_library("GL") - if libgl is not None: # pragma: no branch - ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL) - - def parse_duration(duration: str) -> int: """Parse duration in format XhYmZs into milliseconds duration.""" if duration.isdigit(): diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index a1b8e6c72..97b286fba 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -882,9 +882,6 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover """ assert QApplication.instance() - # Some setups can segfault in here if we don't do this. - utils.libgl_workaround() - override = os.environ.get('QUTE_FAKE_OPENGL') if override is not None: log.init.debug("Using override {}".format(override)) diff --git a/tests/conftest.py b/tests/conftest.py index ee945ac4c..7b8cf2753 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -239,12 +239,6 @@ def set_backend(monkeypatch, request): monkeypatch.setattr(objects, 'backend', backend) -@pytest.fixture(autouse=True, scope='session') -def apply_libgl_workaround(): - """Make sure we load libGL early so QtWebEngine tests run properly.""" - utils.libgl_workaround() - - @pytest.fixture(autouse=True) def apply_fake_os(monkeypatch, request): fake_os = request.node.get_closest_marker('fake_os') diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index b43638cb3..2c726ddb6 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -892,13 +892,6 @@ def test_ceil_log_invalid(number, base): utils.ceil_log(number, base) -@pytest.mark.parametrize('skip', [True, False]) -def test_libgl_workaround(monkeypatch, skip): - if skip: - monkeypatch.setenv('QUTE_SKIP_LIBGL_WORKAROUND', '1') - utils.libgl_workaround() # Just make sure it doesn't crash. - - @pytest.mark.parametrize('duration, out', [ ("0", 0), ("0s", 0), -- cgit v1.2.3-54-g00ecf From 254b21f3ecc43d4d844e6ded55378673b913b5c8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 10:28:53 +0100 Subject: Try to recover from CompletionMetaInfo with unexpected structure Fixes #6302 --- qutebrowser/browser/history.py | 24 +++++++++++++++++++++--- qutebrowser/misc/sql.py | 8 ++++---- tests/unit/browser/test_history.py | 22 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index ef4650a35..9eaa88760 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -92,8 +92,11 @@ class CompletionMetaInfo(sql.SqlTable): } def __init__(self, parent=None): - super().__init__("CompletionMetaInfo", ['key', 'value'], - constraints={'key': 'PRIMARY KEY'}) + self._fields = ['key', 'value'] + self._constraints = {'key': 'PRIMARY KEY'} + super().__init__( + "CompletionMetaInfo", self._fields, constraints=self._constraints) + if sql.user_version_changed(): self._init_default_values() @@ -101,6 +104,15 @@ class CompletionMetaInfo(sql.SqlTable): if key not in self.KEYS: raise KeyError(key) + def try_recover(self): + """Try recovering the table structure. + + This should be called if getting a value via __getattr__ failed. In theory, this + should never happen, in practice, it does. + """ + self._create_table(self._fields, constraints=self._constraints, force=True) + self._init_default_values() + def _init_default_values(self): for key, default in self.KEYS.items(): if key not in self: @@ -164,7 +176,13 @@ class WebHistory(sql.SqlTable): self.completion = CompletionHistory(parent=self) self.metainfo = CompletionMetaInfo(parent=self) - rebuild_completion = self.metainfo['force_rebuild'] + try: + rebuild_completion = self.metainfo['force_rebuild'] + except sql.BugError: + log.sql.warning("Failed to access meta info, trying to recover...", + exc_info=True) + self.metainfo.try_recover() + rebuild_completion = self.metainfo['force_rebuild'] if sql.user_version_changed(): # If the DB user version changed, run a full cleanup and rebuild the diff --git a/qutebrowser/misc/sql.py b/qutebrowser/misc/sql.py index 7a3626f6e..68c0fd538 100644 --- a/qutebrowser/misc/sql.py +++ b/qutebrowser/misc/sql.py @@ -351,13 +351,13 @@ class SqlTable(QObject): self._name = name self._create_table(fields, constraints) - def _create_table(self, fields, constraints): + def _create_table(self, fields, constraints, *, force=False): """Create the table if the database is uninitialized. - If the table already exists, this does nothing, so it can e.g. be called on - every user_version change. + If the table already exists, this does nothing (except with force=True), so it + can e.g. be called on every user_version change. """ - if not user_version_changed(): + if not user_version_changed() and not force: return constraints = constraints or {} diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index 9b08de30d..1ca708ec8 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -508,6 +508,28 @@ class TestCompletionMetaInfo: metainfo['excluded_patterns'] = value assert metainfo['excluded_patterns'] == value + # FIXME: It'd be good to test those two things via WebHistory (and not just + # CompletionMetaInfo in isolation), but we can't do that right now - see the + # docstring of TestRebuild for details. + + def test_recovery_no_key(self, metainfo): + metainfo.delete('key', 'force_rebuild') + + with pytest.raises(sql.BugError, match='No result for single-result query'): + metainfo['force_rebuild'] + + metainfo.try_recover() + assert not metainfo['force_rebuild'] + + def test_recovery_no_table(self, metainfo): + sql.Query("DROP TABLE CompletionMetaInfo").run() + + with pytest.raises(sql.BugError, match='no such table: CompletionMetaInfo'): + metainfo['force_rebuild'] + + metainfo.try_recover() + assert not metainfo['force_rebuild'] + class TestHistoryProgress: -- cgit v1.2.3-54-g00ecf From b4345fb458b4015e4533f2f16452c6f8f1a40d5c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 10:34:43 +0100 Subject: Update changelog --- doc/changelog.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 9c7a69d3c..27b413690 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -48,6 +48,13 @@ Fixed - The workaround for black on (almost) black formula images in dark mode now also works with Qt 5.12 and 5.13. - When running in Flatpak, the QtWebEngine version is now detected properly. + Before, a wrong version was assumed, breaking dark mode and certain workarounds + (resulting in crashes on websites like LinkedIn or TradingView). +- When running in Flatpak, communicating with an existing instance now works + properly. Before, a new instance was always opened. +- When the metainfo in the completion database doesn't have the expected + structure, qutebrowser now tries to gracefully recover from the situation + instead of crashing. [[v2.1.0]] v2.1.0 (2021-03-12) -- cgit v1.2.3-54-g00ecf From 8adac6948bb5c8b41da4650229f8c3469f7ee033 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 08:46:25 +0100 Subject: Use correct runtime path for Flatpak See #6300 (cherry picked from commit 9f67a763ef86805f0981f037ccd2fb5cb0e84b88) --- qutebrowser/utils/standarddir.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index f94d46061..4a571696f 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -232,7 +232,14 @@ def _init_runtime(args: Optional[argparse.Namespace]) -> None: # Unfortunately this path could get too long for sockets (which have a # maximum length of 104 chars), so we don't add the username here... - _create(path) + from qutebrowser.utils import version + if version.is_sandboxed(): + *parts, app_name = os.path.split(path) + assert app_name == APPNAME, app_name + path = os.path.join(*parts, 'app', os.environ['FLATPAK_ID']) + else: + _create(path) + _locations[_Location.runtime] = path -- cgit v1.2.3-54-g00ecf From fb0154ae26b09accc08c9ab7fa7cbcbe9fe2578c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 10:54:59 +0100 Subject: Import webenginesettings lazily in version --- qutebrowser/utils/standarddir.py | 3 +-- qutebrowser/utils/version.py | 8 +------- tests/unit/utils/test_version.py | 19 ++++++++++--------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 4a571696f..4ea7e5dbf 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -30,7 +30,7 @@ from typing import Iterator, Optional from PyQt5.QtCore import QStandardPaths from PyQt5.QtWidgets import QApplication -from qutebrowser.utils import log, debug, utils +from qutebrowser.utils import log, debug, utils, version # The cached locations _locations = {} @@ -232,7 +232,6 @@ def _init_runtime(args: Optional[argparse.Namespace]) -> None: # Unfortunately this path could get too long for sockets (which have a # maximum length of 104 chars), so we don't add the username here... - from qutebrowser.utils import version if version.is_sandboxed(): *parts, app_name = os.path.split(path) assert app_name == APPNAME, app_name diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 97b286fba..63097bb8b 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -58,12 +58,6 @@ from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf from qutebrowser.browser import pdfjs from qutebrowser.config import config, websettings -try: - from qutebrowser.browser.webengine import webenginesettings -except ImportError: # pragma: no cover - webenginesettings = None # type: ignore[assignment] - - _LOGO = r''' ______ ,, ,.-"` | ,-` | @@ -684,7 +678,7 @@ def qtwebengine_versions(avoid_init: bool = False) -> WebEngineVersions: - https://www.chromium.org/developers/calendar - https://chromereleases.googleblog.com/ """ - assert webenginesettings is not None + from qutebrowser.browser.webengine import webenginesettings if webenginesettings.parsed_user_agent is None and not avoid_init: webenginesettings.init_user_agent() diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index c91017e84..4b01be537 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -40,6 +40,11 @@ from qutebrowser.utils import version, usertypes, utils, standarddir from qutebrowser.misc import pastebin, objects, elf from qutebrowser.browser import pdfjs +try: + from qutebrowser.browser.webengine import webenginesettings +except ImportError: + webenginesettings = None + @pytest.mark.parametrize('os_release, expected', [ # No file @@ -1004,7 +1009,6 @@ class TestWebEngineVersions: versions = version.WebEngineVersions.from_pyqt(pyqt_webengine_version) - from qutebrowser.browser.webengine import webenginesettings webenginesettings.init_user_agent() expected = webenginesettings.parsed_user_agent.upstream_browser_version @@ -1045,26 +1049,24 @@ class TestChromiumVersion: @pytest.fixture(autouse=True) def clear_parsed_ua(self, monkeypatch): pytest.importorskip('PyQt5.QtWebEngineWidgets') - if version.webenginesettings is not None: + if webenginesettings is not None: # Not available with QtWebKit - monkeypatch.setattr(version.webenginesettings, 'parsed_user_agent', None) + monkeypatch.setattr(webenginesettings, 'parsed_user_agent', None) def test_fake_ua(self, monkeypatch, caplog): ver = '77.0.3865.98' - version.webenginesettings._init_user_agent_str( - _QTWE_USER_AGENT.format(ver)) + webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format(ver)) assert version.qtwebengine_versions().chromium == ver def test_prefers_saved_user_agent(self, monkeypatch): - version.webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87')) + webenginesettings._init_user_agent_str(_QTWE_USER_AGENT.format('87')) class FakeProfile: def defaultProfile(self): raise AssertionError("Should not be called") - monkeypatch.setattr(version.webenginesettings, 'QWebEngineProfile', - FakeProfile()) + monkeypatch.setattr(webenginesettings, 'QWebEngineProfile', FakeProfile()) version.qtwebengine_versions() @@ -1250,7 +1252,6 @@ def test_version_info(params, stubs, monkeypatch, config_stub): if params.with_webkit: patches['qWebKitVersion'] = lambda: 'WEBKIT VERSION' patches['objects.backend'] = usertypes.Backend.QtWebKit - patches['webenginesettings'] = None substitutions['backend'] = 'new QtWebKit (WebKit WEBKIT VERSION)' else: monkeypatch.delattr(version, 'qtutils.qWebKitVersion', raising=False) -- cgit v1.2.3-54-g00ecf From 5ce8a9c9c19e2aaec591b191d3c3efebd1957fa7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 10:58:04 +0100 Subject: Rename version.is_sandboxed() to is_flatpak() --- qutebrowser/misc/elf.py | 2 +- qutebrowser/utils/standarddir.py | 5 ++++- qutebrowser/utils/utils.py | 2 +- qutebrowser/utils/version.py | 8 ++++++-- tests/unit/utils/test_version.py | 4 ++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/qutebrowser/misc/elf.py b/qutebrowser/misc/elf.py index 6d35eecb4..18cb9634c 100644 --- a/qutebrowser/misc/elf.py +++ b/qutebrowser/misc/elf.py @@ -310,7 +310,7 @@ def _parse_from_file(f: IO[bytes]) -> Versions: def parse_webenginecore() -> Optional[Versions]: """Parse the QtWebEngineCore library file.""" - if version.is_sandboxed(): + if version.is_flatpak(): # Flatpak has Qt in /usr/lib/x86_64-linux-gnu, but QtWebEngine in /app/lib. library_path = pathlib.Path("/app/lib") else: diff --git a/qutebrowser/utils/standarddir.py b/qutebrowser/utils/standarddir.py index 4ea7e5dbf..7bb632b57 100644 --- a/qutebrowser/utils/standarddir.py +++ b/qutebrowser/utils/standarddir.py @@ -232,7 +232,10 @@ def _init_runtime(args: Optional[argparse.Namespace]) -> None: # Unfortunately this path could get too long for sockets (which have a # maximum length of 104 chars), so we don't add the username here... - if version.is_sandboxed(): + if version.is_flatpak(): + # We need a path like /run/user/1000/app/org.qutebrowser.qutebrowser rather than + # /run/user/1000/qutebrowser on Flatpak, since that's bind-mounted in a way that + # it is accessible by any other qutebrowser instances. *parts, app_name = os.path.split(path) assert app_name == APPNAME, app_name path = os.path.join(*parts, 'app', os.environ['FLATPAK_ID']) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index afc4c4f8d..2a47d60aa 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -605,7 +605,7 @@ def open_file(filename: str, cmdline: str = None) -> None: # if we want to use the default override = config.val.downloads.open_dispatcher - if version.is_sandboxed(): + if version.is_flatpak(): if cmdline: message.error("Cannot spawn download dispatcher from sandbox") return diff --git a/qutebrowser/utils/version.py b/qutebrowser/utils/version.py index 63097bb8b..89da353fc 100644 --- a/qutebrowser/utils/version.py +++ b/qutebrowser/utils/version.py @@ -183,8 +183,12 @@ def distribution() -> Optional[DistributionInfo]: parsed=parsed, version=dist_version, pretty=pretty, id=dist_id) -def is_sandboxed() -> bool: - """Whether the environment has restricted access to the host system.""" +def is_flatpak() -> bool: + """Whether qutebrowser is running via Flatpak. + + If packaged via Flatpak, the environment is has restricted access to the host + system. + """ current_distro = distribution() if current_distro is None: return False diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 4b01be537..a53b4bdce 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -319,9 +319,9 @@ def test_distribution(tmpdir, monkeypatch, os_release, expected): id='arch', parsed=version.Distribution.arch, version=None, pretty='Arch Linux'), False) ]) -def test_is_sandboxed(monkeypatch, distribution, expected): +def test_is_flatpak(monkeypatch, distribution, expected): monkeypatch.setattr(version, "distribution", lambda: distribution) - assert version.is_sandboxed() == expected + assert version.is_flatpak() == expected class GitStrSubprocessFake: -- cgit v1.2.3-54-g00ecf From c07b93b7c40aef496e8e0a298e4fbafedf4ee8d0 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 10:59:51 +0100 Subject: pylint: Disable pointless-statement in tests --- scripts/dev/run_pylint_on_tests.py | 1 + tests/unit/browser/test_history.py | 3 +-- tests/unit/browser/webkit/test_webkitelem.py | 2 +- tests/unit/config/test_config.py | 6 +++--- tests/unit/config/test_configcache.py | 2 -- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/scripts/dev/run_pylint_on_tests.py b/scripts/dev/run_pylint_on_tests.py index 16a281d57..d0385bd17 100644 --- a/scripts/dev/run_pylint_on_tests.py +++ b/scripts/dev/run_pylint_on_tests.py @@ -58,6 +58,7 @@ def main(): 'protected-access', 'len-as-condition', 'compare-to-empty-string', + 'pointless-statement', # directories without __init__.py... 'import-error', ] diff --git a/tests/unit/browser/test_history.py b/tests/unit/browser/test_history.py index 1ca708ec8..1a46c5be0 100644 --- a/tests/unit/browser/test_history.py +++ b/tests/unit/browser/test_history.py @@ -488,12 +488,11 @@ class TestCompletionMetaInfo: def test_contains_keyerror(self, metainfo): with pytest.raises(KeyError): - # pylint: disable=pointless-statement 'does_not_exist' in metainfo # noqa: B015 def test_getitem_keyerror(self, metainfo): with pytest.raises(KeyError): - metainfo['does_not_exist'] # pylint: disable=pointless-statement + metainfo['does_not_exist'] def test_setitem_keyerror(self, metainfo): with pytest.raises(KeyError): diff --git a/tests/unit/browser/webkit/test_webkitelem.py b/tests/unit/browser/webkit/test_webkitelem.py index 33af45b6c..593896e96 100644 --- a/tests/unit/browser/webkit/test_webkitelem.py +++ b/tests/unit/browser/webkit/test_webkitelem.py @@ -303,7 +303,7 @@ class TestWebKitElement: def test_getitem_keyerror(self, elem): with pytest.raises(KeyError): - elem['foo'] # pylint: disable=pointless-statement + elem['foo'] def test_setitem(self, elem): elem['foo'] = 'bar' diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 8a9d8154d..c28e8ce07 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -725,7 +725,7 @@ class TestContainer: def test_getattr_invalid_private(self, container): """Make sure an invalid _attribute doesn't try getting a container.""" with pytest.raises(AttributeError): - container._foo # pylint: disable=pointless-statement + container._foo def test_getattr_prefix(self, container): new_container = container.tabs @@ -744,7 +744,7 @@ class TestContainer: def test_getattr_invalid(self, container): with pytest.raises(configexc.NoOptionError) as excinfo: - container.tabs.foobar # pylint: disable=pointless-statement + container.tabs.foobar assert excinfo.value.option == 'tabs.foobar' def test_setattr_option(self, config_stub, container): @@ -754,7 +754,7 @@ class TestContainer: def test_confapi_errors(self, container): configapi = types.SimpleNamespace(errors=[]) container._configapi = configapi - container.tabs.foobar # pylint: disable=pointless-statement + container.tabs.foobar assert len(configapi.errors) == 1 error = configapi.errors[0] diff --git a/tests/unit/config/test_configcache.py b/tests/unit/config/test_configcache.py index 6bd841a65..87514bada 100644 --- a/tests/unit/config/test_configcache.py +++ b/tests/unit/config/test_configcache.py @@ -55,12 +55,10 @@ def test_configcache_get_after_set(config_stub): def test_configcache_naive_benchmark(config_stub, benchmark): def _run_bench(): for _i in range(10000): - # pylint: disable=pointless-statement config.cache['tabs.padding'] config.cache['tabs.indicator.width'] config.cache['tabs.indicator.padding'] config.cache['tabs.min_width'] config.cache['tabs.max_width'] config.cache['tabs.pinned.shrink'] - # pylint: enable=pointless-statement benchmark(_run_bench) -- cgit v1.2.3-54-g00ecf From ca8c3205857bf81a06f8f36aae999303281412e2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 11:04:39 +0100 Subject: Add a test for flatpak runtime dir See #6300 --- qutebrowser/browser/history.py | 2 +- tests/unit/utils/test_standarddir.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/qutebrowser/browser/history.py b/qutebrowser/browser/history.py index 9eaa88760..773c6cc51 100644 --- a/qutebrowser/browser/history.py +++ b/qutebrowser/browser/history.py @@ -178,7 +178,7 @@ class WebHistory(sql.SqlTable): try: rebuild_completion = self.metainfo['force_rebuild'] - except sql.BugError: + except sql.BugError: # pragma: no cover log.sql.warning("Failed to access meta info, trying to recover...", exc_info=True) self.metainfo.try_recover() diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index 5b24ed962..c8a7f47e5 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -202,6 +202,17 @@ class TestStandardDir: standarddir._init_runtime(args=None) assert standarddir.runtime() == str(tmpdir_env / APPNAME) + def test_flatpak_runtimedir(self, monkeypatch, tmp_path): + app_id = 'org.qutebrowser.qutebrowser' + expected = tmp_path / 'app' / app_id + + monkeypatch.setattr(standarddir.version, 'is_flatpak', lambda: True) + monkeypatch.setenv('XDG_RUNTIME_DIR', str(tmp_path)) + monkeypatch.setenv('FLATPAK_ID', app_id) + + standarddir._init_runtime(args=None) + assert standarddir.runtime() == str(expected) + @pytest.mark.fake_os('windows') def test_runtimedir_empty_tempdir(self, monkeypatch, tmpdir): """With an empty tempdir on non-Linux, we should raise.""" -- cgit v1.2.3-54-g00ecf From 56bd87f0a6bfd07a1939c2e4697725be085d9c59 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 11:31:55 +0100 Subject: Fix test_no_webengine_vailable --- tests/unit/config/test_qtargs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/config/test_qtargs.py b/tests/unit/config/test_qtargs.py index d7e24ae0a..4c31c5b07 100644 --- a/tests/unit/config/test_qtargs.py +++ b/tests/unit/config/test_qtargs.py @@ -111,7 +111,6 @@ def test_no_webengine_available(monkeypatch, config_stub, parser, stubs): here. """ monkeypatch.setattr(qtargs.objects, 'backend', usertypes.Backend.QtWebEngine) - monkeypatch.setattr(qtargs.version, 'webenginesettings', None) fake = stubs.ImportFake({'qutebrowser.browser.webengine': False}, monkeypatch) fake.patch() -- cgit v1.2.3-54-g00ecf From 6a65ff1ad07911d2ee23a1c6360d2bebdc305b33 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 12:17:35 +0100 Subject: Fix test_flatpak_runtimedir --- tests/unit/utils/test_standarddir.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index c8a7f47e5..4614373db 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -203,11 +203,15 @@ class TestStandardDir: assert standarddir.runtime() == str(tmpdir_env / APPNAME) def test_flatpak_runtimedir(self, monkeypatch, tmp_path): + runtime_path = tmp_path / 'runtime' + runtime_path.mkdir() + runtime_path.chmod(0o0700) + app_id = 'org.qutebrowser.qutebrowser' - expected = tmp_path / 'app' / app_id + expected = runtime_path / 'app' / app_id monkeypatch.setattr(standarddir.version, 'is_flatpak', lambda: True) - monkeypatch.setenv('XDG_RUNTIME_DIR', str(tmp_path)) + monkeypatch.setenv('XDG_RUNTIME_DIR', str(runtime_path)) monkeypatch.setenv('FLATPAK_ID', app_id) standarddir._init_runtime(args=None) -- cgit v1.2.3-54-g00ecf From 069743e98d3dd18954298b7a10b55c5156a8d765 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 12:31:05 +0100 Subject: Only run test_flatpak_runtimedir on Linux Paths are different on macOS/Windows --- tests/unit/utils/test_standarddir.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index 4614373db..1a9107995 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -202,6 +202,7 @@ class TestStandardDir: standarddir._init_runtime(args=None) assert standarddir.runtime() == str(tmpdir_env / APPNAME) + @pytest.mark.linux def test_flatpak_runtimedir(self, monkeypatch, tmp_path): runtime_path = tmp_path / 'runtime' runtime_path.mkdir() -- cgit v1.2.3-54-g00ecf From ad5381c92d1c61e726d6937f83309e9d6dfe6a22 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 14:28:09 +0100 Subject: Make sure QtWebEngine is imported early We need to import the module before a QApplication is created, so that it can set everything up properly. This needs to happen even with the QtWebKit backend configured, so that a proper error can be printed later in backendproblem.py if QtWebKit is unavailable. For QtWebEngine, this is done implicitly in qtargs.py before getting QtWebEngine arguments. For QtWebKit, this used to be done implicitly via version.py importing webenginesettings, but that's not the case anymore since fb0154ae26b09accc08c9ab7fa7cbcbe9fe2578c. Either way, it's better to be explicit about this. --- qutebrowser/misc/earlyinit.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index b154eaca8..ecd3fac1a 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -279,6 +279,21 @@ def check_optimize_flag(): "unexpected behavior may occur.") +def webengine_early_import(): + """If QtWebEngine is available, import it early. + + We need to ensure that QtWebEngine is imported before a QApplication is created for + everything to work properly. + + This needs to be done even when using the QtWebKit backend, to ensure that e.g. + error messages in backendproblem.py are accurate. + """ + try: + from PyQt5 import QtWebEngineWidgets + except ImportError: + pass + + def early_init(args): """Do all needed early initialization. @@ -303,3 +318,4 @@ def early_init(args): configure_pyqt() check_ssl_support() check_optimize_flag() + webengine_early_import() -- cgit v1.2.3-54-g00ecf From 2279d8fc230515a768dfe525bb366cfddb2713b4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 14:49:45 +0100 Subject: Restore config access in commands.parser See https://github.com/qutebrowser/qutebrowser/pull/5967#issuecomment-791373157 but no issues with circular imports here, from what I can see... --- qutebrowser/commands/parser.py | 19 +++++++++---------- qutebrowser/commands/runners.py | 3 +-- qutebrowser/config/config.py | 3 +-- tests/unit/commands/test_parser.py | 16 ++++++++-------- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index ceebad57e..710c31717 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -24,6 +24,7 @@ from typing import Optional, List, Mapping, Iterator, List, Union from qutebrowser.commands import cmdexc, command from qutebrowser.misc import split, objects +from qutebrowser.config import config @dataclasses.dataclass @@ -47,7 +48,7 @@ class CommandParser: def __init__(self, partial_match: bool = False) -> None: self._partial_match = partial_match - def _get_alias(self, text: str, aliases: Mapping[str, str], *, default: str) -> str: + def _get_alias(self, text: str, *, default: str) -> str: """Get an alias from the config. Args: @@ -60,6 +61,7 @@ class CommandParser: otherwise. """ parts = text.strip().split(maxsplit=1) + aliases = config.cache['aliases'] if parts[0] not in aliases: return default alias = aliases[parts[0]] @@ -75,7 +77,7 @@ class CommandParser: def _parse_all_gen( self, text: str, - aliases: Mapping[str, str] = None, + aliases: bool = True, **kwargs: bool, ) -> Iterator[ParseResult]: """Split a command on ;; and parse all parts. @@ -85,7 +87,7 @@ class CommandParser: Args: text: Text to parse. - aliases: A map of aliases to commands. + aliases: Whether to handle aliases. **kwargs: Passed to parse(). Yields: @@ -96,7 +98,7 @@ class CommandParser: raise cmdexc.NoSuchCommandError("No command given") if aliases: - text = self._get_alias(text, aliases, default=text) + text = self._get_alias(text, default=text) if ';;' in text: # Get the first command and check if it doesn't want to have ;; @@ -122,7 +124,6 @@ class CommandParser: *, fallback: bool = False, keep: bool = False, - use_best_match: bool = False, ) -> ParseResult: """Split the commandline text into command and arguments. @@ -131,7 +132,6 @@ class CommandParser: fallback: Whether to do a fallback splitting when the command was unknown. keep: Whether to keep special chars and whitespace - use_best_match: Whether to use the best match even if incomplete. Return: A ParseResult tuple. @@ -142,7 +142,7 @@ class CommandParser: raise cmdexc.NoSuchCommandError("No command given") if self._partial_match: - cmdstr = self._completion_match(cmdstr, use_best_match) + cmdstr = self._completion_match(cmdstr) try: cmd = objects.commands[cmdstr] @@ -163,12 +163,11 @@ class CommandParser: return ParseResult(cmd=cmd, args=args, cmdline=cmdline) - def _completion_match(self, cmdstr: str, use_best_match: bool) -> str: + def _completion_match(self, cmdstr: str) -> str: """Replace cmdstr with a matching completion if there's only one match. Args: cmdstr: The string representing the entered command so far. - use_best_match: Whether to use the best match even if incomplete. Return: cmdstr modified to the matching completion or unmodified @@ -177,7 +176,7 @@ class CommandParser: if cmdstr in cmd] if len(matches) == 1: cmdstr = matches[0] - elif len(matches) > 1 and use_best_match: + elif len(matches) > 1 and config.val.completion.use_best_match: cmdstr = matches[0] return cmdstr diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 6d4ccdfc2..7386d7d3b 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -172,8 +172,7 @@ class CommandRunner(AbstractCommandRunner): parsed = None with self._handle_error(safely): - parsed = self._parser.parse_all( - text, use_best_match=config.val.completion.use_best_match) + parsed = self._parser.parse_all(text) if parsed is None: return diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 719721cb7..07d16ea92 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -166,8 +166,7 @@ class KeyConfig: def _implied_cmd(self, cmdline: str) -> Optional[str]: """Return cmdline, or the implied cmd if cmdline is a set-cmd-text.""" try: - results = parser.CommandParser().parse_all( - cmdline, aliases=cache['aliases']) + results = parser.CommandParser().parse_all(cmdline) except cmdexc.NoSuchCommandError: return None diff --git a/tests/unit/commands/test_parser.py b/tests/unit/commands/test_parser.py index 3a0c5f314..b851ad3b0 100644 --- a/tests/unit/commands/test_parser.py +++ b/tests/unit/commands/test_parser.py @@ -37,24 +37,24 @@ class TestCommandParser: """ p = parser.CommandParser() if cmdline_test.valid: - p.parse_all(cmdline_test.cmd) + p.parse_all(cmdline_test.cmd, aliases=False) else: with pytest.raises(cmdexc.NoSuchCommandError): - p.parse_all(cmdline_test.cmd) + p.parse_all(cmdline_test.cmd, aliases=False) def test_parse_all_with_alias(self, cmdline_test, monkeypatch, config_stub): if not cmdline_test.cmd: pytest.skip("Empty command") - aliases = {'alias_name': cmdline_test.cmd} + config_stub.val.aliases = {'alias_name': cmdline_test.cmd} p = parser.CommandParser() if cmdline_test.valid: - assert len(p.parse_all("alias_name", aliases=aliases)) > 0 + assert len(p.parse_all("alias_name")) > 0 else: with pytest.raises(cmdexc.NoSuchCommandError): - p.parse_all("alias_name", aliases=aliases) + p.parse_all("alias_name") @pytest.mark.parametrize('command', ['', ' ']) def test_parse_empty_with_alias(self, command): @@ -65,7 +65,7 @@ class TestCommandParser: """ p = parser.CommandParser() with pytest.raises(cmdexc.NoSuchCommandError): - p.parse_all(command, aliases={"foo": "bar"}) + p.parse_all(command) @pytest.mark.parametrize('command, name, args', [ ("set-cmd-text -s :open", "set-cmd-text", ["-s", ":open"]), @@ -85,7 +85,7 @@ class TestCommandParser: ("set-cmd-text ?", "set-cmd-text", ["?"]), ("set-cmd-text :", "set-cmd-text", [":"]), ]) - def test_parse_result(self, command, name, args): + def test_parse_result(self, config_stub, command, name, args): p = parser.CommandParser() result = p.parse_all(command)[0] assert result.cmd.name == name @@ -133,5 +133,5 @@ class TestCompletions: config_stub.val.completion.use_best_match = True p = parser.CommandParser(partial_match=True) - result = p.parse('tw', best_match=True) + result = p.parse('tw') assert result.cmd.name == 'two' -- cgit v1.2.3-54-g00ecf From f3e604d7fbf2ceb8f15a92c5ba7c177c83ee4db1 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 14:54:55 +0100 Subject: Fix copy-paste issue --- qutebrowser/commands/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index 710c31717..547641281 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -112,7 +112,7 @@ class CommandParser: else: sub_texts = [text] for sub in sub_texts: - yield self.parse(first, **kwargs) + yield self.parse(sub, **kwargs) def parse_all(self, text: str, **kwargs: bool) -> List[ParseResult]: """Wrapper over _parse_all_gen.""" -- cgit v1.2.3-54-g00ecf From 27ad47825279a39141efd11ec9cc54ff2a872517 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 15:00:15 +0100 Subject: Fix lint --- qutebrowser/misc/earlyinit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index ecd3fac1a..ca8f9e8fe 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -289,7 +289,7 @@ def webengine_early_import(): error messages in backendproblem.py are accurate. """ try: - from PyQt5 import QtWebEngineWidgets + from PyQt5 import QtWebEngineWidgets # pylint: disable=unused-import except ImportError: pass -- cgit v1.2.3-54-g00ecf From 3d1f975391ef61a43490e93b4ce0c54fdd5c22ab Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 15:15:22 +0100 Subject: Move completion fallback handling out of CommandParser This makes CompletionParser.parse simpler and makes ParseResult.cmd and .args non-Optional. Them being Optional would mean we would've to either resort to more complex typing with Literal, or to check whether they are really non-None everywhere. Since fallback=True is only used at one point, let's just handle this at the calling site instead. In theory, this changes the behavior when the cmdstr is empty and self._partial_match is set, because we now raise early and self._completion_match isn't called anymore. In practice, I think this shouldn't make a difference anywhere, and tests seem to agree. If cmdstr is empty and self._partial_match is False, the behavior should be the same, because objects.commands[''] will raise KeyError. --- qutebrowser/commands/parser.py | 29 +++++++---------------------- qutebrowser/completion/completer.py | 23 ++++++++++++++--------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index 547641281..5adbc003c 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -20,7 +20,7 @@ """Module for parsing commands entered into the browser.""" import dataclasses -from typing import Optional, List, Mapping, Iterator, List, Union +from typing import Optional, List, Mapping, Iterator, Union from qutebrowser.commands import cmdexc, command from qutebrowser.misc import split, objects @@ -32,8 +32,8 @@ class ParseResult: """The result of parsing a commandline.""" - cmd: Optional[command.Command] - args: Optional[List[str]] + cmd: command.Command + args: List[str] cmdline: List[str] @@ -118,27 +118,16 @@ class CommandParser: """Wrapper over _parse_all_gen.""" return list(self._parse_all_gen(text, **kwargs)) - def parse( - self, - text: str, - *, - fallback: bool = False, - keep: bool = False, - ) -> ParseResult: + def parse(self, text: str, *, keep: bool = False) -> ParseResult: """Split the commandline text into command and arguments. Args: text: Text to parse. - fallback: Whether to do a fallback splitting when the command was - unknown. - keep: Whether to keep special chars and whitespace - - Return: - A ParseResult tuple. + keep: Whether to keep special chars and whitespace. """ cmdstr, sep, argstr = text.partition(' ') - if not cmdstr and not fallback: + if not cmdstr: raise cmdexc.NoSuchCommandError("No command given") if self._partial_match: @@ -147,11 +136,7 @@ class CommandParser: try: cmd = objects.commands[cmdstr] except KeyError: - if not fallback: - raise cmdexc.NoSuchCommandError( - '{}: no such command'.format(cmdstr)) - cmdline = split.split(text, keep=keep) - return ParseResult(cmd=None, args=None, cmdline=cmdline) + raise cmdexc.NoSuchCommandError(f'{cmdstr}: no such command') args = self._split_args(cmd, argstr, keep) if keep and args: diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index 548ace2c7..778333854 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -25,8 +25,8 @@ from typing import TYPE_CHECKING from PyQt5.QtCore import pyqtSlot, QObject, QTimer from qutebrowser.config import config -from qutebrowser.commands import parser -from qutebrowser.misc import objects +from qutebrowser.commands import parser, cmdexc +from qutebrowser.misc import objects, split from qutebrowser.utils import log, utils, debug, objreg from qutebrowser.completion.models import miscmodels if TYPE_CHECKING: @@ -139,12 +139,18 @@ class Completer(QObject): if not text or not text.strip(): # Only ":", empty part under the cursor with nothing before/after return [], '', [] - result = parser.CommandParser().parse(text, fallback=True, keep=True) - parts = [x for x in result.cmdline if x] + + try: + parse_result = parser.CommandParser().parse(text, keep=True) + except cmdexc.NoSuchCommandError: + cmdline = split.split(text, keep=True) + else: + cmdline = parse_result.cmdline + + parts = [x for x in cmdline if x] pos = self._cmd.cursorPosition() - len(self._cmd.prefix()) pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars - log.completion.debug('partitioning {} around position {}'.format(parts, - pos)) + log.completion.debug(f'partitioning {parts} around position {pos}') for i, part in enumerate(parts): pos -= len(part) if pos <= 0: @@ -155,11 +161,10 @@ class Completer(QObject): center = parts[i].strip() # strip trailing whitespace included as a separate token postfix = [x.strip() for x in parts[i+1:] if not x.isspace()] - log.completion.debug( - "partitioned: {} '{}' {}".format(prefix, center, postfix)) + log.completion.debug(f"partitioned: {prefix} '{center}' {postfix}") return prefix, center, postfix - raise utils.Unreachable("Not all parts consumed: {}".format(parts)) + raise utils.Unreachable(f"Not all parts consumed: {parts}") @pyqtSlot(str) def on_selection_changed(self, text): -- cgit v1.2.3-54-g00ecf From c158cafe3395e130946cb192247e544251da8601 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 15:24:58 +0100 Subject: Fix lint As for the mypy unreachable warning, see: See https://github.com/python/mypy/issues/7214 and https://github.com/python/mypy/issues/8766 Includes cherry-pick of 27ad47825279a39141efd11ec9cc54ff2a872517 --- qutebrowser/commands/parser.py | 4 ++-- qutebrowser/commands/runners.py | 6 ++---- qutebrowser/misc/earlyinit.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/qutebrowser/commands/parser.py b/qutebrowser/commands/parser.py index 5adbc003c..06a20cdf6 100644 --- a/qutebrowser/commands/parser.py +++ b/qutebrowser/commands/parser.py @@ -15,12 +15,12 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with qutebrowser. If not, see . +# along with qutebrowser. If not, see . """Module for parsing commands entered into the browser.""" import dataclasses -from typing import Optional, List, Mapping, Iterator, Union +from typing import List, Iterator from qutebrowser.commands import cmdexc, command from qutebrowser.misc import split, objects diff --git a/qutebrowser/commands/runners.py b/qutebrowser/commands/runners.py index 7386d7d3b..5fb054455 100644 --- a/qutebrowser/commands/runners.py +++ b/qutebrowser/commands/runners.py @@ -22,13 +22,11 @@ import traceback import re import contextlib -from typing import (TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping, - List, Optional) +from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping from PyQt5.QtCore import pyqtSlot, QUrl, QObject from qutebrowser.api import cmdutils -from qutebrowser.config import config from qutebrowser.commands import cmdexc, parser from qutebrowser.utils import message, objreg, qtutils, usertypes, utils from qutebrowser.keyinput import macros, modeman @@ -175,7 +173,7 @@ class CommandRunner(AbstractCommandRunner): parsed = self._parser.parse_all(text) if parsed is None: - return + return # type: ignore[unreachable] for result in parsed: with self._handle_error(safely): diff --git a/qutebrowser/misc/earlyinit.py b/qutebrowser/misc/earlyinit.py index ecd3fac1a..ca8f9e8fe 100644 --- a/qutebrowser/misc/earlyinit.py +++ b/qutebrowser/misc/earlyinit.py @@ -289,7 +289,7 @@ def webengine_early_import(): error messages in backendproblem.py are accurate. """ try: - from PyQt5 import QtWebEngineWidgets + from PyQt5 import QtWebEngineWidgets # pylint: disable=unused-import except ImportError: pass -- cgit v1.2.3-54-g00ecf From 6f567aff4aa5720db42a4fc3d4744c79615ad6b2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 19:19:08 +0100 Subject: Update changelog --- doc/changelog.asciidoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 27b413690..9df59f0dc 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -32,6 +32,12 @@ Added - New `input.media_keys` setting which can be used to disable Chromium's handling of media keys. +Changed +~~~~~~~ + +- The completion now also shows bindings starting with `set-cmd-text` in its + third column, such as `o` for `:open`. + [[v2.1.1]] v2.1.1 (unreleased) ------------------- -- cgit v1.2.3-54-g00ecf From 7de06df5f4d034c50f96f0d5a241d5e222cb5d2a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 19:38:41 +0100 Subject: Add quirk for krunker.io --- qutebrowser/browser/webengine/webenginesettings.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py index 090cdfd4c..23d8812c3 100644 --- a/qutebrowser/browser/webengine/webenginesettings.py +++ b/qutebrowser/browser/webengine/webenginesettings.py @@ -458,6 +458,13 @@ def _init_site_specific_quirks(): pattern=urlmatch.UrlPattern(pattern), hide_userconfig=True) + config.instance.set_obj( + 'content.headers.accept_language', + '', + pattern=urlmatch.UrlPattern('https://matchmaker.krunker.io/*'), + hide_userconfig=True, + ) + def _init_devtools_settings(): """Make sure the devtools always get images/JS permissions.""" -- cgit v1.2.3-54-g00ecf From 7207c88a56db4240bb2408dcb46a9f641f08408e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 18 Mar 2021 19:39:51 +0100 Subject: Update changelog --- doc/changelog.asciidoc | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 9df59f0dc..9ada74f7e 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -42,6 +42,13 @@ Changed v2.1.1 (unreleased) ------------------- +Added +~~~~~ + +- Site-specific quirk for krunker.io, which shows a "Socket Error" with + qutebrowser's default Accept-Language header. The workaround is equivalent to + doing `:set -u matchmaker.krunker.io content.headers.accept_language ""`. + Changed ~~~~~~~ -- cgit v1.2.3-54-g00ecf