From c5d5197b01d7eceafa9030953cdc64c8905ea752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Geel=20Weirs=C3=B8e?= Date: Thu, 22 Oct 2020 13:50:03 +0200 Subject: [PATCH 001/194] Re-adding history to dmenu_qutebrowser userscript As there is not a file called history to cat from in $QUTE_DATA_DIR, but instead a sqlite file called history.sqlite. Thus I changed this for sqlite3 and an inline query on the history table, extracting distinct urls. And with fprint and cat piping them into dmenu, the rest works as always. --- misc/userscripts/dmenu_qutebrowser | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index 84be1b619..465d37507 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -38,11 +38,13 @@ # (This is unnecessarily long. I use this rarely, feel free to make this script accept parameters.) # -[ -z "$QUTE_URL" ] && QUTE_URL='http://google.com' -url=$(echo "$QUTE_URL" | cat - "$QUTE_CONFIG_DIR/quickmarks" "$QUTE_DATA_DIR/history" | dmenu -l 15 -p qutebrowser) +[ -z "$QUTE_URL" ] && QUTE_URL='https://duckduckgo.com' + +url=$(printf "%s\n%s" "$QUTE_URL" "$(sqlite3 "$QUTE_DATA_DIR/history.sqlite" 'select url from CompletionHistory')" | cat "$QUTE_CONFIG_DIR/quickmarks" - | dmenu -l 15 -p qutebrowser) url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url") [ -z "${url// }" ] && exit echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" + From b6bc90c9c35d60db86c1862799ac1a0ccb6095ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asger=20Geel=20Weirs=C3=B8e?= Date: Thu, 22 Oct 2020 14:45:21 +0200 Subject: [PATCH 002/194] Removes empty line at end of file --- misc/userscripts/dmenu_qutebrowser | 1 - 1 file changed, 1 deletion(-) diff --git a/misc/userscripts/dmenu_qutebrowser b/misc/userscripts/dmenu_qutebrowser index 465d37507..ab218c0e6 100755 --- a/misc/userscripts/dmenu_qutebrowser +++ b/misc/userscripts/dmenu_qutebrowser @@ -47,4 +47,3 @@ url=$(echo "$url" | sed -E 's/[^ ]+ +//g' | grep -E "https?:" || echo "$url") [ -z "${url// }" ] && exit echo "open $url" >> "$QUTE_FIFO" || qutebrowser "$url" - From 1d08b329daa663a8c4f86572379c00232b976e9e Mon Sep 17 00:00:00 2001 From: Tim Brown Date: Sat, 31 Oct 2020 13:50:07 +1000 Subject: [PATCH 003/194] mypy: use from-import for typing in `config` See #5396 --- qutebrowser/config/config.py | 46 ++++++++++----------- qutebrowser/config/configcache.py | 6 +-- qutebrowser/config/configcommands.py | 16 +++----- qutebrowser/config/configdata.py | 48 +++++++++++----------- qutebrowser/config/configdiff.py | 6 +-- qutebrowser/config/configexc.py | 17 ++++---- qutebrowser/config/configfiles.py | 61 ++++++++++++---------------- qutebrowser/config/configutils.py | 49 +++++++++++----------- qutebrowser/config/qtargs.py | 18 ++++---- qutebrowser/config/websettings.py | 35 ++++++++-------- 10 files changed, 141 insertions(+), 161 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 8611e46ab..d8e8d612e 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -22,8 +22,8 @@ import copy import contextlib import functools -import typing -from typing import Any, Tuple, MutableMapping +from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping, + MutableMapping, MutableSequence, Optional, Tuple, cast) from PyQt5.QtCore import pyqtSignal, QObject, QUrl @@ -32,15 +32,15 @@ from qutebrowser.utils import utils, log, urlmatch from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.config import configcache, configfiles from qutebrowser.misc import savemanager # An easy way to access the config from other code via config.val.foo -val = typing.cast('ConfigContainer', None) -instance = typing.cast('Config', None) -key_instance = typing.cast('KeyConfig', None) -cache = typing.cast('configcache.ConfigCache', None) +val = cast('ConfigContainer', None) +instance = cast('Config', None) +key_instance = cast('KeyConfig', None) +cache = cast('configcache.ConfigCache', None) # Keeping track of all change filters to validate them later. change_filters = [] @@ -83,7 +83,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name not configdata.is_valid_prefix(self._option)): raise configexc.NoOptionError(self._option) - def check_match(self, option: typing.Optional[str]) -> bool: + def check_match(self, option: Optional[str]) -> bool: """Check if the given option matches the filter.""" if option is None: # Called directly, not from a config change event. @@ -96,7 +96,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name else: return False - def __call__(self, func: typing.Callable) -> typing.Callable: + def __call__(self, func: Callable) -> Callable: """Filter calls to the decorated function. Gets called when a function should be decorated. @@ -114,7 +114,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name """ if self._function: @functools.wraps(func) - def func_wrapper(option: str = None) -> typing.Any: + def func_wrapper(option: str = None) -> Any: """Call the underlying function.""" if self.check_match(option): return func() @@ -122,8 +122,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name return func_wrapper else: @functools.wraps(func) - def meth_wrapper(wrapper_self: typing.Any, - option: str = None) -> typing.Any: + def meth_wrapper(wrapper_self: Any, option: str = None) -> Any: """Call the underlying function.""" if self.check_match(option): return func(wrapper_self) @@ -141,7 +140,7 @@ class KeyConfig: _config: The Config object to be used. """ - _ReverseBindings = typing.Dict[str, typing.MutableSequence[str]] + _ReverseBindings = Dict[str, MutableSequence[str]] def __init__(self, config: 'Config') -> None: self._config = config @@ -153,10 +152,7 @@ class KeyConfig: if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - def get_bindings_for( - self, - mode: str - ) -> typing.Dict[keyutils.KeySequence, str]: + def get_bindings_for(self, mode: str) -> Dict[keyutils.KeySequence, str]: """Get the combined bindings for the given mode.""" bindings = dict(val.bindings.default[mode]) for key, binding in val.bindings.commands[mode].items(): @@ -168,7 +164,7 @@ class KeyConfig: def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings': """Get a dict of commands to a list of bindings for the mode.""" - cmd_to_keys = {} # type: KeyConfig._ReverseBindings + 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(';;'): @@ -184,7 +180,7 @@ class KeyConfig: def get_command(self, key: keyutils.KeySequence, mode: str, - default: bool = False) -> typing.Optional[str]: + default: bool = False) -> Optional[str]: """Get the command for a given key (or None).""" self._validate(key, mode) if default: @@ -277,7 +273,7 @@ class Config(QObject): yaml_config: 'configfiles.YamlConfig', parent: QObject = None) -> None: super().__init__(parent) - self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]] + self._mutables: MutableMapping[str, Tuple[Any, Any]] = {} self._yaml = yaml_config self._init_values() self.yaml_loaded = False @@ -286,11 +282,11 @@ class Config(QObject): def _init_values(self) -> None: """Populate the self._values dict.""" - self._values = {} # type: typing.Mapping + self._values: Mapping = {} for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def __iter__(self) -> typing.Iterator[configutils.Values]: + def __iter__(self) -> Iterator[configutils.Values]: """Iterate over configutils.Values items.""" yield from self._values.values() @@ -391,7 +387,7 @@ class Config(QObject): def get_obj_for_pattern( self, name: str, *, - pattern: typing.Optional[urlmatch.UrlPattern] + pattern: Optional[urlmatch.UrlPattern] ) -> Any: """Get the given setting as object (for YAML/config.py). @@ -525,7 +521,7 @@ class Config(QObject): Return: The changed config part as string. """ - lines = [] # type: typing.List[str] + lines: List[str] = [] for values in sorted(self, key=lambda v: v.opt.name): lines += values.dump() @@ -564,7 +560,7 @@ class ConfigContainer: pattern=self._pattern) @contextlib.contextmanager - def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: + def _handle_error(self, action: str, name: str) -> Iterator[None]: try: yield except configexc.Error as e: diff --git a/qutebrowser/config/configcache.py b/qutebrowser/config/configcache.py index 2bc45f0f8..a07a22ee5 100644 --- a/qutebrowser/config/configcache.py +++ b/qutebrowser/config/configcache.py @@ -20,7 +20,7 @@ """Implementation of a basic config cache.""" -import typing +from typing import Any, Dict from qutebrowser.config import config @@ -38,14 +38,14 @@ class ConfigCache: """ def __init__(self) -> None: - self._cache = {} # type: typing.Dict[str, typing.Any] + self._cache: Dict[str, Any] = {} config.instance.changed.connect(self._on_config_changed) def _on_config_changed(self, attr: str) -> None: if attr in self._cache: del self._cache[attr] - def __getitem__(self, attr: str) -> typing.Any: + def __getitem__(self, attr: str) -> Any: try: return self._cache[attr] except KeyError: diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 20702be10..a0739af4d 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -19,9 +19,9 @@ """Commands related to the configuration.""" -import typing import os.path import contextlib +from typing import TYPE_CHECKING, Iterator, List, Optional from PyQt5.QtCore import QUrl @@ -32,7 +32,7 @@ from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.misc import editor from qutebrowser.keyinput import keyutils -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.config.config import Config, KeyConfig @@ -47,17 +47,14 @@ class ConfigCommands: self._keyconfig = keyconfig @contextlib.contextmanager - def _handle_config_error(self) -> typing.Iterator[None]: + def _handle_config_error(self) -> Iterator[None]: """Catch errors in set_command and raise CommandError.""" try: yield except configexc.Error as e: raise cmdutils.CommandError(str(e)) - def _parse_pattern( - self, - pattern: typing.Optional[str] - ) -> typing.Optional[urlmatch.UrlPattern]: + def _parse_pattern(self, pattern: Optional[str]) -> Optional[urlmatch.UrlPattern]: """Parse a pattern string argument to a pattern.""" if pattern is None: return None @@ -75,8 +72,7 @@ class ConfigCommands: except keyutils.KeyParseError as e: raise cmdutils.CommandError(str(e)) - def _print_value(self, option: str, - pattern: typing.Optional[urlmatch.UrlPattern]) -> None: + def _print_value(self, option: str, pattern: Optional[urlmatch.UrlPattern]) -> None: """Print the value of the given option.""" with self._handle_config_error(): value = self._config.get_str(option, pattern=pattern) @@ -468,7 +464,7 @@ class ConfigCommands: raise cmdutils.CommandError("{} already exists - use --force to " "overwrite!".format(filename)) - options = [] # type: typing.List + options: List = [] if defaults: options = [(None, opt, opt.default) for _name, opt in sorted(configdata.DATA.items())] diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 290d897bd..c50efc9d4 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -24,8 +24,8 @@ Module attributes: DATA: A dict of Option objects after init() has been called. """ -import typing -from typing import Optional +from typing import (Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, + Sequence, Tuple, Union, cast) import functools import attr @@ -33,10 +33,10 @@ from qutebrowser.config import configtypes from qutebrowser.utils import usertypes, qtutils, utils from qutebrowser.misc import debugcachestats -DATA = typing.cast(typing.Mapping[str, 'Option'], None) -MIGRATIONS = typing.cast('Migrations', None) +DATA = cast(Mapping[str, 'Option'], None) +MIGRATIONS = cast('Migrations', None) -_BackendDict = typing.Mapping[str, typing.Union[str, bool]] +_BackendDict = Mapping[str, Union[str, bool]] @attr.s @@ -47,15 +47,15 @@ class Option: Note that this is just an option which exists, with no value associated. """ - name = attr.ib() # type: str - typ = attr.ib() # type: configtypes.BaseType - default = attr.ib() # type: typing.Any - backends = attr.ib() # type: typing.Iterable[usertypes.Backend] - raw_backends = attr.ib() # type: Optional[typing.Mapping[str, bool]] - description = attr.ib() # type: str - supports_pattern = attr.ib(default=False) # type: bool - restart = attr.ib(default=False) # type: bool - no_autoconfig = attr.ib(default=False) # type: bool + name: str = attr.ib() + typ: configtypes.BaseType = attr.ib() + default: Any = attr.ib() + backends: Iterable[usertypes.Backend] = attr.ib() + raw_backends: Optional[Mapping[str, bool]] = attr.ib() + description: str = attr.ib() + supports_pattern: bool = attr.ib(default=False) + restart: bool = attr.ib(default=False) + no_autoconfig: bool = attr.ib(default=False) @attr.s @@ -68,13 +68,11 @@ class Migrations: deleted: A list of option names which have been removed. """ - renamed = attr.ib( - default=attr.Factory(dict)) # type: typing.Dict[str, str] - deleted = attr.ib( - default=attr.Factory(list)) # type: typing.List[str] + renamed: Dict[str, str] = attr.ib(default=attr.Factory(dict)) + deleted: List[str] = attr.ib(default=attr.Factory(list)) -def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None: +def _raise_invalid_node(name: str, what: str, node: Any) -> None: """Raise an exception for an invalid configdata YAML node. Args: @@ -88,14 +86,14 @@ def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None: def _parse_yaml_type( name: str, - node: typing.Union[str, typing.Mapping[str, typing.Any]], + node: Union[str, Mapping[str, Any]], ) -> configtypes.BaseType: if isinstance(node, str): # e.g: # > type: Bool # -> create the type object without any arguments type_name = node - kwargs = {} # type: typing.MutableMapping[str, typing.Any] + kwargs: MutableMapping[str, Any] = {} elif isinstance(node, dict): # e.g: # > type: @@ -136,7 +134,7 @@ def _parse_yaml_type( def _parse_yaml_backends_dict( name: str, node: _BackendDict, -) -> typing.Sequence[usertypes.Backend]: +) -> Sequence[usertypes.Backend]: """Parse a dict definition for backends. Example: @@ -178,8 +176,8 @@ def _parse_yaml_backends_dict( def _parse_yaml_backends( name: str, - node: typing.Union[None, str, _BackendDict], -) -> typing.Sequence[usertypes.Backend]: + node: Union[None, str, _BackendDict], +) -> Sequence[usertypes.Backend]: """Parse a backend node in the yaml. It can have one of those four forms: @@ -208,7 +206,7 @@ def _parse_yaml_backends( def _read_yaml( yaml_data: str, -) -> typing.Tuple[typing.Mapping[str, Option], Migrations]: +) -> Tuple[Mapping[str, Option], Migrations]: """Read config data from a YAML file. Args: diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py index 53177134b..42215c093 100644 --- a/qutebrowser/config/configdiff.py +++ b/qutebrowser/config/configdiff.py @@ -19,9 +19,9 @@ """Code to show a diff of the legacy config format.""" -import typing import difflib import os.path +from typing import MutableSequence import pygments import pygments.lexers @@ -730,8 +730,8 @@ scroll right def get_diff() -> str: """Get a HTML diff for the old config files.""" - old_conf_lines = [] # type: typing.MutableSequence[str] - old_key_lines = [] # type: typing.MutableSequence[str] + old_conf_lines: MutableSequence[str] = [] + old_key_lines: MutableSequence[str] = [] for filename, dest in [('qutebrowser.conf', old_conf_lines), ('keys.conf', old_key_lines)]: diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index b409bc883..872f777ff 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -19,9 +19,9 @@ """Exceptions related to config parsing.""" -import typing -import attr +from typing import Any, Mapping, Optional, Sequence, Union +import attr from qutebrowser.utils import usertypes, log @@ -46,7 +46,7 @@ class BackendError(Error): def __init__( self, name: str, backend: usertypes.Backend, - raw_backends: typing.Optional[typing.Mapping[str, bool]] + raw_backends: Optional[Mapping[str, bool]] ) -> None: if raw_backends is None or not raw_backends[backend.name]: msg = ("The {} setting is not available with the {} backend!" @@ -76,8 +76,7 @@ class ValidationError(Error): msg: Additional error message. """ - def __init__(self, value: typing.Any, - msg: typing.Union[str, Exception]) -> None: + def __init__(self, value: Any, msg: Union[str, Exception]) -> None: super().__init__("Invalid value '{}' - {}".format(value, msg)) self.option = None @@ -117,9 +116,9 @@ class ConfigErrorDesc: traceback: The formatted traceback of the exception. """ - text = attr.ib() # type: str - exception = attr.ib() # type: typing.Union[str, Exception] - traceback = attr.ib(None) # type: str + text: str = attr.ib() + exception: Union[str, Exception] = attr.ib() + traceback: str = attr.ib(None) def __str__(self) -> str: if self.traceback: @@ -141,7 +140,7 @@ class ConfigFileErrors(Error): def __init__(self, basename: str, - errors: typing.Sequence[ConfigErrorDesc], *, + errors: Sequence[ConfigErrorDesc], *, fatal: bool = False) -> None: super().__init__("Errors occurred while reading {}:\n{}".format( basename, '\n'.join(' {}'.format(e) for e in errors))) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index ae05a2861..a7fab52f7 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -27,8 +27,9 @@ import textwrap import traceback import configparser import contextlib -import typing import re +from typing import (TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Mapping, + MutableMapping, Optional, cast) import yaml from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QSettings, qVersion @@ -39,15 +40,15 @@ from qutebrowser.config import (configexc, config, configdata, configutils, from qutebrowser.keyinput import keyutils from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.misc import savemanager # The StateConfig instance -state = typing.cast('StateConfig', None) +state = cast('StateConfig', None) -_SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]] +_SettingsType = Dict[str, Dict[str, Any]] class StateConfig(configparser.ConfigParser): @@ -117,7 +118,7 @@ class YamlConfig(QObject): 'autoconfig.yml') self._dirty = False - self._values = {} # type: typing.Dict[str, configutils.Values] + self._values: Dict[str, configutils.Values] = {} for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) @@ -130,7 +131,7 @@ class YamlConfig(QObject): """ save_manager.add_saveable('yaml-config', self._save, self.changed) - def __iter__(self) -> typing.Iterator[configutils.Values]: + def __iter__(self) -> Iterator[configutils.Values]: """Iterate over configutils.Values items.""" yield from self._values.values() @@ -145,7 +146,7 @@ class YamlConfig(QObject): if not self._dirty: return - settings = {} # type: _SettingsType + settings: _SettingsType = {} for name, values in sorted(self._values.items()): if not values: continue @@ -167,10 +168,7 @@ class YamlConfig(QObject): """.lstrip('\n'))) utils.yaml_dump(data, f) - def _pop_object(self, - yaml_data: typing.Any, - key: str, - typ: type) -> typing.Any: + def _pop_object(self, yaml_data: Any, key: str, typ: type) -> Any: """Get a global object from the given data.""" if not isinstance(yaml_data, dict): desc = configexc.ConfigErrorDesc("While loading data", @@ -227,19 +225,18 @@ class YamlConfig(QObject): self._validate_names(settings) self._build_values(settings) - def _load_settings_object(self, yaml_data: typing.Any) -> '_SettingsType': + def _load_settings_object(self, yaml_data: Any) -> '_SettingsType': """Load the settings from the settings: key.""" return self._pop_object(yaml_data, 'settings', dict) - def _load_legacy_settings_object(self, - yaml_data: typing.Any) -> '_SettingsType': + def _load_legacy_settings_object(self, yaml_data: Any) -> '_SettingsType': data = self._pop_object(yaml_data, 'global', dict) settings = {} for name, value in data.items(): settings[name] = {'global': value} return settings - def _build_values(self, settings: typing.Mapping) -> None: + def _build_values(self, settings: Mapping) -> None: """Build up self._values from the values in the given dict.""" errors = [] for name, yaml_values in settings.items(): @@ -285,7 +282,7 @@ class YamlConfig(QObject): for e in sorted(unknown)] raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def set_obj(self, name: str, value: typing.Any, *, + def set_obj(self, name: str, value: Any, *, pattern: urlmatch.UrlPattern = None) -> None: """Set the given setting to the given value.""" self._values[name].add(value, pattern) @@ -477,8 +474,7 @@ class YamlMigrations(QObject): self._settings[name][scope] = value self.changed.emit() - def _migrate_to_multiple(self, old_name: str, - new_names: typing.Iterable[str]) -> None: + def _migrate_to_multiple(self, old_name: str, new_names: Iterable[str]) -> None: if old_name not in self._settings: return @@ -541,12 +537,12 @@ class ConfigAPI: def __init__(self, conf: config.Config, keyconfig: config.KeyConfig): self._config = conf self._keyconfig = keyconfig - self.errors = [] # type: typing.List[configexc.ConfigErrorDesc] + self.errors: List[configexc.ConfigErrorDesc] = [] self.configdir = pathlib.Path(standarddir.config()) self.datadir = pathlib.Path(standarddir.data()) @contextlib.contextmanager - def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: + def _handle_error(self, action: str, name: str) -> Iterator[None]: """Catch config-related exceptions and save them in self.errors.""" try: yield @@ -582,21 +578,19 @@ class ConfigAPI: with self._handle_error('reading', 'autoconfig.yml'): read_autoconfig() - def get(self, name: str, pattern: str = None) -> typing.Any: + def get(self, name: str, pattern: str = None) -> Any: """Get a setting value from the config, optionally with a pattern.""" with self._handle_error('getting', name): urlpattern = urlmatch.UrlPattern(pattern) if pattern else None return self._config.get_mutable_obj(name, pattern=urlpattern) - def set(self, name: str, value: typing.Any, pattern: str = None) -> None: + def set(self, name: str, value: Any, pattern: str = None) -> None: """Set a setting value in the config, optionally with a pattern.""" with self._handle_error('setting', name): urlpattern = urlmatch.UrlPattern(pattern) if pattern else None self._config.set_obj(name, value, pattern=urlpattern) - def bind(self, key: str, - command: typing.Optional[str], - mode: str = 'normal') -> None: + def bind(self, key: str, command: Optional[str], mode: str = 'normal') -> None: """Bind a key to a command, with an optional key mode.""" with self._handle_error('binding', key): seq = keyutils.KeySequence.parse(key) @@ -625,7 +619,7 @@ class ConfigAPI: self.errors += e.errors @contextlib.contextmanager - def pattern(self, pattern: str) -> typing.Iterator[config.ConfigContainer]: + def pattern(self, pattern: str) -> Iterator[config.ConfigContainer]: """Get a ConfigContainer for the given pattern.""" # We need to propagate the exception so we don't need to return # something. @@ -641,9 +635,8 @@ class ConfigPyWriter: def __init__( self, - options: typing.List, - bindings: typing.MutableMapping[ - str, typing.Mapping[str, typing.Optional[str]]], + options: List, + bindings: MutableMapping[str, Mapping[str, Optional[str]]], *, commented: bool) -> None: self._options = options self._bindings = bindings @@ -664,7 +657,7 @@ class ConfigPyWriter: else: return line - def _gen_lines(self) -> typing.Iterator[str]: + def _gen_lines(self) -> Iterator[str]: """Generate a config.py with the given settings/bindings. Yields individual lines. @@ -673,7 +666,7 @@ class ConfigPyWriter: yield from self._gen_options() yield from self._gen_bindings() - def _gen_header(self) -> typing.Iterator[str]: + def _gen_header(self) -> Iterator[str]: """Generate the initial header of the config.""" yield self._line("# Autogenerated config.py") yield self._line("#") @@ -706,7 +699,7 @@ class ConfigPyWriter: yield self._line("config.load_autoconfig(False)") yield '' - def _gen_options(self) -> typing.Iterator[str]: + def _gen_options(self) -> Iterator[str]: """Generate the options part of the config.""" for pattern, opt, value in self._options: if opt.name in ['bindings.commands', 'bindings.default']: @@ -734,7 +727,7 @@ class ConfigPyWriter: opt.name, value, str(pattern))) yield '' - def _gen_bindings(self) -> typing.Iterator[str]: + def _gen_bindings(self) -> Iterator[str]: """Generate the bindings part of the config.""" normal_bindings = self._bindings.pop('normal', {}) if normal_bindings: @@ -835,7 +828,7 @@ def read_autoconfig() -> None: @contextlib.contextmanager -def saved_sys_properties() -> typing.Iterator[None]: +def saved_sys_properties() -> Iterator[None]: """Save various sys properties such as sys.path and sys.modules.""" old_path = sys.path.copy() old_modules = sys.modules.copy() diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index 3f7823772..d2be3b9f7 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -21,21 +21,22 @@ """Utilities and data structures used by various config code.""" -import typing import collections import itertools import operator +from typing import ( + TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Union) from PyQt5.QtCore import QUrl from qutebrowser.utils import utils, urlmatch, usertypes, qtutils from qutebrowser.config import configexc -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from qutebrowser.config import configdata -def _widened_hostnames(hostname: str) -> typing.Iterable[str]: +def _widened_hostnames(hostname: str) -> Iterable[str]: """A generator for widening string hostnames. Ex: a.c.foo -> [a.c.foo, c.foo, foo]""" @@ -56,8 +57,8 @@ class ScopedValue: id_gen = itertools.count(0) - def __init__(self, value: typing.Any, - pattern: typing.Optional[urlmatch.UrlPattern], + def __init__(self, value: Any, + pattern: Optional[urlmatch.UrlPattern], hide_userconfig: bool = False) -> None: self.value = value self.pattern = pattern @@ -90,17 +91,18 @@ class Values: _domain_map: A mapping from hostnames to all associated ScopedValues. """ - _VmapKeyType = typing.Optional[urlmatch.UrlPattern] + _VmapKeyType = Optional[urlmatch.UrlPattern] def __init__(self, opt: 'configdata.Option', - values: typing.Sequence[ScopedValue] = ()) -> None: + values: Sequence[ScopedValue] = ()) -> None: self.opt = opt - self._vmap = collections.OrderedDict() \ - # type: collections.OrderedDict[Values._VmapKeyType, ScopedValue] + self._vmap: 'collections.OrderedDict[Values._VmapKeyType, ScopedValue]' = ( + collections.OrderedDict() + ) # A map from domain parts to rules that fall under them. - self._domain_map = collections.defaultdict(set) \ - # type: typing.Dict[typing.Optional[str], typing.Set[ScopedValue]] + self._domain_map: Dict[ + Optional[str], Set[ScopedValue]] = collections.defaultdict(set) for scoped in values: self._add_scoped(scoped) @@ -117,7 +119,7 @@ class Values: return '\n'.join(lines) return '{}: '.format(self.opt.name) - def dump(self, include_hidden: bool = False) -> typing.Sequence[str]: + def dump(self, include_hidden: bool = False) -> Sequence[str]: """Dump all customizations for this value. Arguments: @@ -138,7 +140,7 @@ class Values: return lines - def __iter__(self) -> typing.Iterator['ScopedValue']: + def __iter__(self) -> Iterator['ScopedValue']: """Yield ScopedValue elements. This yields in "normal" order, i.e. global and then first-set settings @@ -151,12 +153,12 @@ class Values: return bool(self._vmap) def _check_pattern_support( - self, arg: typing.Union[urlmatch.UrlPattern, QUrl, None]) -> None: + self, arg: Union[urlmatch.UrlPattern, QUrl, None]) -> None: """Make sure patterns are supported if one was given.""" if arg is not None and not self.opt.supports_pattern: raise configexc.NoPatternError(self.opt.name) - def add(self, value: typing.Any, + def add(self, value: Any, pattern: urlmatch.UrlPattern = None, *, hide_userconfig: bool = False) -> None: """Add a value with the given pattern to the list of values. @@ -201,7 +203,7 @@ class Values: self._vmap.clear() self._domain_map.clear() - def _get_fallback(self, fallback: bool) -> typing.Any: + def _get_fallback(self, fallback: bool) -> Any: """Get the fallback global/default value.""" if None in self._vmap: return self._vmap[None].value @@ -211,8 +213,7 @@ class Values: else: return usertypes.UNSET - def get_for_url(self, url: QUrl = None, *, - fallback: bool = True) -> typing.Any: + def get_for_url(self, url: QUrl = None, *, fallback: bool = True) -> Any: """Get a config value, falling back when needed. This first tries to find a value matching the URL (if given). @@ -225,7 +226,7 @@ class Values: return self._get_fallback(fallback) qtutils.ensure_valid(url) - candidates = [] # type: typing.List[ScopedValue] + candidates: List[ScopedValue] = [] # Urls trailing with '.' are equivalent to non-trailing types. # urlutils strips them, so in order to match we will need to as well. widened_hosts = _widened_hostnames(url.host().rstrip('.')) @@ -247,8 +248,8 @@ class Values: return self._get_fallback(fallback) def get_for_pattern(self, - pattern: typing.Optional[urlmatch.UrlPattern], *, - fallback: bool = True) -> typing.Any: + pattern: Optional[urlmatch.UrlPattern], *, + fallback: bool = True) -> Any: """Get a value only if it's been overridden for the given pattern. This is useful when showing values to the user. @@ -272,11 +273,11 @@ class FontFamilies: """A list of font family names.""" - def __init__(self, families: typing.Sequence[str]) -> None: + def __init__(self, families: Sequence[str]) -> None: self._families = families self.family = families[0] if families else None - def __iter__(self) -> typing.Iterator[str]: + def __iter__(self) -> Iterator[str]: yield from self._families def __repr__(self) -> str: @@ -285,7 +286,7 @@ class FontFamilies: def __str__(self) -> str: return self.to_str() - def _quoted_families(self) -> typing.Iterator[str]: + def _quoted_families(self) -> Iterator[str]: for f in self._families: needs_quoting = any(c in f for c in ', ') yield '"{}"'.format(f) if needs_quoting else f diff --git a/qutebrowser/config/qtargs.py b/qutebrowser/config/qtargs.py index 868f4d669..0785e3b58 100644 --- a/qutebrowser/config/qtargs.py +++ b/qutebrowser/config/qtargs.py @@ -21,15 +21,15 @@ import os import sys -import typing import argparse +from typing import Any, Dict, Iterator, List, Optional, Sequence from qutebrowser.config import config from qutebrowser.misc import objects from qutebrowser.utils import usertypes, qtutils, utils -def qt_args(namespace: argparse.Namespace) -> typing.List[str]: +def qt_args(namespace: argparse.Namespace) -> List[str]: """Get the Qt QApplication arguments based on an argparse namespace. Args: @@ -61,9 +61,7 @@ def qt_args(namespace: argparse.Namespace) -> typing.List[str]: return argv -def _qtwebengine_enabled_features( - feature_flags: typing.Sequence[str], -) -> typing.Iterator[str]: +def _qtwebengine_enabled_features(feature_flags: Sequence[str]) -> Iterator[str]: """Get --enable-features flags for QtWebEngine. Args: @@ -114,8 +112,8 @@ def _qtwebengine_enabled_features( def _qtwebengine_args( namespace: argparse.Namespace, - feature_flags: typing.Sequence[str], -) -> typing.Iterator[str]: + feature_flags: Sequence[str], +) -> Iterator[str]: """Get the QtWebEngine arguments to use based on the config.""" is_qt_514 = (qtutils.version_check('5.14', compiled=False) and not qtutils.version_check('5.15', compiled=False)) @@ -162,8 +160,8 @@ def _qtwebengine_args( yield from _qtwebengine_settings_args() -def _qtwebengine_settings_args() -> typing.Iterator[str]: - settings = { +def _qtwebengine_settings_args() -> Iterator[str]: + settings: Dict[str, Dict[Any, Optional[str]]] = { 'qt.force_software_rendering': { 'software-opengl': None, 'qt-quick': None, @@ -201,7 +199,7 @@ def _qtwebengine_settings_args() -> typing.Iterator[str]: 'never': '--no-referrers', 'same-domain': '--reduced-referrer-granularity', } - } # type: typing.Dict[str, typing.Dict[typing.Any, typing.Optional[str]]] + } if not qtutils.version_check('5.11'): # On Qt 5.11, we can control this via QWebEngineSettings diff --git a/qutebrowser/config/websettings.py b/qutebrowser/config/websettings.py index cc307dd75..4b50b1cdc 100644 --- a/qutebrowser/config/websettings.py +++ b/qutebrowser/config/websettings.py @@ -20,9 +20,9 @@ """Bridge from QWeb(Engine)Settings to our own settings.""" import re -import typing import argparse import functools +from typing import Any, Callable, Dict, Optional, Set import attr from PyQt5.QtCore import QUrl, pyqtSlot, qVersion @@ -41,11 +41,11 @@ class UserAgent: """A parsed user agent.""" - os_info = attr.ib() # type: str - webkit_version = attr.ib() # type: str - upstream_browser_key = attr.ib() # type: str - upstream_browser_version = attr.ib() # type: str - qt_key = attr.ib() # type: str + os_info: str = attr.ib() + webkit_version: str = attr.ib() + upstream_browser_key: str = attr.ib() + upstream_browser_version: str = attr.ib() + qt_key: str = attr.ib() @classmethod def parse(cls, ua: str) -> 'UserAgent': @@ -82,8 +82,7 @@ class AttributeInfo: """Info about a settings attribute.""" - def __init__(self, *attributes: typing.Any, - converter: typing.Callable = None) -> None: + def __init__(self, *attributes: Any, converter: Callable = None) -> None: self.attributes = attributes if converter is None: self.converter = lambda val: val @@ -95,18 +94,18 @@ class AbstractSettings: """Abstract base class for settings set via QWeb(Engine)Settings.""" - _ATTRIBUTES = {} # type: typing.Dict[str, AttributeInfo] - _FONT_SIZES = {} # type: typing.Dict[str, typing.Any] - _FONT_FAMILIES = {} # type: typing.Dict[str, typing.Any] - _FONT_TO_QFONT = {} # type: typing.Dict[typing.Any, QFont.StyleHint] + _ATTRIBUTES: Dict[str, AttributeInfo] = {} + _FONT_SIZES: Dict[str, Any] = {} + _FONT_FAMILIES: Dict[str, Any] = {} + _FONT_TO_QFONT: Dict[Any, QFont.StyleHint] = {} - def __init__(self, settings: typing.Any) -> None: + def __init__(self, settings: Any) -> None: self._settings = settings - def _assert_not_unset(self, value: typing.Any) -> None: + def _assert_not_unset(self, value: Any) -> None: assert value is not usertypes.UNSET - def set_attribute(self, name: str, value: typing.Any) -> bool: + def set_attribute(self, name: str, value: Any) -> bool: """Set the given QWebSettings/QWebEngineSettings attribute. If the value is usertypes.UNSET, the value is reset instead. @@ -148,7 +147,7 @@ class AbstractSettings: self._settings.setFontSize(family, value) return old_value != value - def set_font_family(self, name: str, value: typing.Optional[str]) -> bool: + def set_font_family(self, name: str, value: Optional[str]) -> bool: """Set the given QWebSettings/QWebEngineSettings font family. With None (the default), QFont is used to get the default font for the @@ -180,7 +179,7 @@ class AbstractSettings: self._settings.setDefaultTextEncoding(encoding) return old_value != encoding - def _update_setting(self, setting: str, value: typing.Any) -> bool: + def _update_setting(self, setting: str, value: Any) -> bool: """Update the given setting/value. Unknown settings are ignored. @@ -203,7 +202,7 @@ class AbstractSettings: value = config.instance.get(setting) self._update_setting(setting, value) - def update_for_url(self, url: QUrl) -> typing.Set[str]: + def update_for_url(self, url: QUrl) -> Set[str]: """Update settings customized for the given tab. Return: From cf9c1c6ca0195f07c86c48583fe24ffc098a5645 Mon Sep 17 00:00:00 2001 From: Tim Brown Date: Sat, 31 Oct 2020 14:02:35 +1000 Subject: [PATCH 004/194] mypy: use from-import for typing in `configtypes.py` See #5396 --- qutebrowser/config/configtypes.py | 215 ++++++++++++++---------------- 1 file changed, 101 insertions(+), 114 deletions(-) diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index 81c47590d..9185ee6ef 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -50,7 +50,8 @@ import itertools import functools import operator import json -import typing +from typing import (Any, Callable, Dict as DictType, Iterable, Iterator, + List as ListType, Optional, Pattern, Sequence, Tuple, Union) import attr import yaml @@ -79,10 +80,10 @@ BOOLEAN_STATES = {'1': True, 'yes': True, 'true': True, 'on': True, '0': False, 'no': False, 'false': False, 'off': False} -_Completions = typing.Optional[typing.Iterable[typing.Tuple[str, str]]] -_StrUnset = typing.Union[str, usertypes.Unset] -_UnsetNone = typing.Union[None, usertypes.Unset] -_StrUnsetNone = typing.Union[str, _UnsetNone] +_Completions = Optional[Iterable[Tuple[str, str]]] +_StrUnset = Union[str, usertypes.Unset] +_UnsetNone = Union[None, usertypes.Unset] +_StrUnsetNone = Union[str, _UnsetNone] class ValidValues: @@ -96,14 +97,12 @@ class ValidValues: """ def __init__(self, - *values: typing.Union[str, - typing.Dict[str, str], - typing.Tuple[str, str]], + *values: Union[str, DictType[str, str], Tuple[str, str]], generate_docs: bool = True) -> None: if not values: raise ValueError("ValidValues with no values makes no sense!") - self.descriptions = {} # type: typing.Dict[str, str] - self.values = [] # type: typing.List[str] + self.descriptions: DictType[str, str] = {} + self.values: ListType[str] = [] self.generate_docs = generate_docs for value in values: if isinstance(value, str): @@ -123,7 +122,7 @@ class ValidValues: def __contains__(self, val: str) -> bool: return val in self.values - def __iter__(self) -> typing.Iterator[str]: + def __iter__(self) -> Iterator[str]: return self.values.__iter__() def __repr__(self) -> str: @@ -150,19 +149,19 @@ class BaseType: def __init__(self, none_ok: bool = False) -> None: self.none_ok = none_ok - self.valid_values = None # type: typing.Optional[ValidValues] + self.valid_values: Optional[ValidValues] = None def get_name(self) -> str: """Get a name for the type for documentation.""" return self.__class__.__name__ - def get_valid_values(self) -> typing.Optional[ValidValues]: + def get_valid_values(self) -> Optional[ValidValues]: """Get the type's valid values for documentation.""" return self.valid_values def _basic_py_validation( - self, value: typing.Any, - pytype: typing.Union[type, typing.Tuple[type, ...]]) -> None: + self, value: Any, + pytype: Union[type, Tuple[type, ...]]) -> None: """Do some basic validation for Python values (emptyness, type). Arguments: @@ -214,8 +213,7 @@ class BaseType: raise configexc.ValidationError( value, "may not contain unprintable chars!") - def _validate_surrogate_escapes(self, full_value: typing.Any, - value: typing.Any) -> None: + def _validate_surrogate_escapes(self, full_value: Any, value: Any) -> None: """Make sure the given value doesn't contain surrogate escapes. This is used for values passed to json.dump, as it can't handle those. @@ -241,7 +239,7 @@ class BaseType: value, "valid values: {}".format(', '.join(self.valid_values))) - def from_str(self, value: str) -> typing.Any: + def from_str(self, value: str) -> Any: """Get the setting value from a string. By default this invokes to_py() for validation and returns the @@ -260,11 +258,11 @@ class BaseType: return None return value - def from_obj(self, value: typing.Any) -> typing.Any: + def from_obj(self, value: Any) -> Any: """Get the setting value from a config.py/YAML object.""" return value - def to_py(self, value: typing.Any) -> typing.Any: + def to_py(self, value: Any) -> Any: """Get the setting value from a Python value. Args: @@ -278,7 +276,7 @@ class BaseType: """ raise NotImplementedError - def to_str(self, value: typing.Any) -> str: + def to_str(self, value: Any) -> str: """Get a string from the setting value. The resulting string should be parseable again by from_str. @@ -288,7 +286,7 @@ class BaseType: assert isinstance(value, str), value return value - def to_doc(self, value: typing.Any, indent: int = 0) -> str: + def to_doc(self, value: Any, indent: int = 0) -> str: """Get a string with the given value for the documentation. This currently uses asciidoc syntax. @@ -334,14 +332,13 @@ class MappingType(BaseType): MAPPING: The mapping to use. """ - MAPPING = {} # type: typing.Dict[str, typing.Any] + MAPPING: DictType[str, Any] = {} - def __init__(self, none_ok: bool = False, - valid_values: ValidValues = None) -> None: + def __init__(self, none_ok: bool = False, valid_values: ValidValues = None) -> None: super().__init__(none_ok) self.valid_values = valid_values - def to_py(self, value: typing.Any) -> typing.Any: + def to_py(self, value: Any) -> Any: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -491,10 +488,10 @@ class List(BaseType): name += " of " + self.valtype.get_name() return name - def get_valid_values(self) -> typing.Optional[ValidValues]: + def get_valid_values(self) -> Optional[ValidValues]: return self.valtype.get_valid_values() - def from_str(self, value: str) -> typing.Optional[typing.List]: + def from_str(self, value: str) -> Optional[ListType]: self._basic_str_validation(value) if not value: return None @@ -509,15 +506,15 @@ class List(BaseType): self.to_py(yaml_val) return yaml_val - def from_obj(self, value: typing.Optional[typing.List]) -> typing.List: + def from_obj(self, value: Optional[ListType]) -> ListType: if value is None: return [] return [self.valtype.from_obj(v) for v in value] def to_py( self, - value: typing.Union[typing.List, usertypes.Unset] - ) -> typing.Union[typing.List, usertypes.Unset]: + value: Union[ListType, usertypes.Unset] + ) -> Union[ListType, usertypes.Unset]: self._basic_py_validation(value, list) if isinstance(value, usertypes.Unset): return value @@ -532,13 +529,13 @@ class List(BaseType): "be set!".format(self.length)) return [self.valtype.to_py(v) for v in value] - def to_str(self, value: typing.List) -> str: + def to_str(self, value: ListType) -> str: if not value: # An empty list is treated just like None -> empty string return '' return json.dumps(value) - def to_doc(self, value: typing.List, indent: int = 0) -> str: + def to_doc(self, value: ListType, indent: int = 0) -> str: if not value: return 'empty' @@ -572,14 +569,13 @@ class ListOrValue(BaseType): def __init__(self, valtype: BaseType, *, none_ok: bool = False, - **kwargs: typing.Any) -> None: + **kwargs: Any) -> None: super().__init__(none_ok) assert not isinstance(valtype, (List, ListOrValue)), valtype self.listtype = List(valtype, none_ok=none_ok, **kwargs) self.valtype = valtype - def _val_and_type(self, - value: typing.Any) -> typing.Tuple[typing.Any, BaseType]: + def _val_and_type(self, value: Any) -> Tuple[Any, BaseType]: """Get the value and type to use for to_str/to_doc/from_str.""" if isinstance(value, list): if len(value) == 1: @@ -592,21 +588,21 @@ class ListOrValue(BaseType): def get_name(self) -> str: return self.listtype.get_name() + ', or ' + self.valtype.get_name() - def get_valid_values(self) -> typing.Optional[ValidValues]: + def get_valid_values(self) -> Optional[ValidValues]: return self.valtype.get_valid_values() - def from_str(self, value: str) -> typing.Any: + def from_str(self, value: str) -> Any: try: return self.listtype.from_str(value) except configexc.ValidationError: return self.valtype.from_str(value) - def from_obj(self, value: typing.Any) -> typing.Any: + def from_obj(self, value: Any) -> Any: if value is None: return [] return value - def to_py(self, value: typing.Any) -> typing.Any: + def to_py(self, value: Any) -> Any: if isinstance(value, usertypes.Unset): return value @@ -615,14 +611,14 @@ class ListOrValue(BaseType): except configexc.ValidationError: return self.listtype.to_py(value) - def to_str(self, value: typing.Any) -> str: + def to_str(self, value: Any) -> str: if value is None: return '' val, typ = self._val_and_type(value) return typ.to_str(val) - def to_doc(self, value: typing.Any, indent: int = 0) -> str: + def to_doc(self, value: Any, indent: int = 0) -> str: if value is None: return 'empty' @@ -641,7 +637,7 @@ class FlagList(List): the valid values of the setting. """ - combinable_values = None # type: typing.Optional[typing.Sequence] + combinable_values: Optional[Sequence] = None _show_valtype = False @@ -651,15 +647,15 @@ class FlagList(List): super().__init__(valtype=String(), none_ok=none_ok, length=length) self.valtype.valid_values = valid_values - def _check_duplicates(self, values: typing.List) -> None: + def _check_duplicates(self, values: ListType) -> None: if len(set(values)) != len(values): raise configexc.ValidationError( values, "List contains duplicate values!") def to_py( self, - value: typing.Union[usertypes.Unset, typing.List], - ) -> typing.Union[usertypes.Unset, typing.List]: + value: Union[usertypes.Unset, ListType], + ) -> Union[usertypes.Unset, ListType]: vals = super().to_py(value) if not isinstance(vals, usertypes.Unset): self._check_duplicates(vals) @@ -703,13 +699,12 @@ class Bool(BaseType): super().__init__(none_ok) self.valid_values = ValidValues('true', 'false', generate_docs=False) - def to_py(self, - value: typing.Union[bool, str, None]) -> typing.Optional[bool]: + def to_py(self, value: Union[bool, str, None]) -> Optional[bool]: self._basic_py_validation(value, bool) assert not isinstance(value, str) return value - def from_str(self, value: str) -> typing.Optional[bool]: + def from_str(self, value: str) -> Optional[bool]: self._basic_str_validation(value) if not value: return None @@ -719,7 +714,7 @@ class Bool(BaseType): except KeyError: raise configexc.ValidationError(value, "must be a boolean!") - def to_str(self, value: typing.Optional[bool]) -> str: + def to_str(self, value: Optional[bool]) -> str: mapping = { None: '', True: 'true', @@ -737,7 +732,7 @@ class BoolAsk(Bool): self.valid_values = ValidValues('true', 'false', 'ask') def to_py(self, # type: ignore[override] - value: typing.Union[bool, str]) -> typing.Union[bool, str, None]: + value: Union[bool, str]) -> Union[bool, str, None]: # basic validation unneeded if it's == 'ask' and done by Bool if we # call super().to_py if isinstance(value, str) and value.lower() == 'ask': @@ -745,14 +740,14 @@ class BoolAsk(Bool): return super().to_py(value) def from_str(self, # type: ignore[override] - value: str) -> typing.Union[bool, str, None]: + value: str) -> Union[bool, str, None]: # basic validation unneeded if it's == 'ask' and done by Bool if we # call super().from_str if value.lower() == 'ask': return 'ask' return super().from_str(value) - def to_str(self, value: typing.Union[bool, str, None]) -> str: + def to_str(self, value: Union[bool, str, None]) -> str: mapping = { None: '', True: 'true', @@ -785,8 +780,8 @@ class _Numeric(BaseType): # pylint: disable=abstract-method .format(self.minval, self.maxval)) def _parse_bound( - self, bound: typing.Union[None, str, int, float] - ) -> typing.Union[None, int, float]: + self, bound: Union[None, str, int, float] + ) -> Union[None, int, float]: """Get a numeric bound from a string like 'maxint'.""" if bound == 'maxint': return qtutils.MAXVALS['int'] @@ -798,7 +793,7 @@ class _Numeric(BaseType): # pylint: disable=abstract-method return bound def _validate_bounds(self, - value: typing.Union[int, float, _UnsetNone], + value: Union[int, float, _UnsetNone], suffix: str = '') -> None: """Validate self.minval and self.maxval.""" if value is None: @@ -814,7 +809,7 @@ class _Numeric(BaseType): # pylint: disable=abstract-method elif not self.zero_ok and value == 0: raise configexc.ValidationError(value, "must not be 0!") - def to_str(self, value: typing.Union[None, int, float]) -> str: + def to_str(self, value: Union[None, int, float]) -> str: if value is None: return '' return str(value) @@ -828,7 +823,7 @@ class Int(_Numeric): """Base class for an integer setting.""" - def from_str(self, value: str) -> typing.Optional[int]: + def from_str(self, value: str) -> Optional[int]: self._basic_str_validation(value) if not value: return None @@ -840,10 +835,7 @@ class Int(_Numeric): self.to_py(intval) return intval - def to_py( - self, - value: typing.Union[int, _UnsetNone] - ) -> typing.Union[int, _UnsetNone]: + def to_py(self, value: Union[int, _UnsetNone]) -> Union[int, _UnsetNone]: self._basic_py_validation(value, int) self._validate_bounds(value) return value @@ -853,7 +845,7 @@ class Float(_Numeric): """Base class for a float setting.""" - def from_str(self, value: str) -> typing.Optional[float]: + def from_str(self, value: str) -> Optional[float]: self._basic_str_validation(value) if not value: return None @@ -867,8 +859,8 @@ class Float(_Numeric): def to_py( self, - value: typing.Union[int, float, _UnsetNone], - ) -> typing.Union[int, float, _UnsetNone]: + value: Union[int, float, _UnsetNone], + ) -> Union[int, float, _UnsetNone]: self._basic_py_validation(value, (int, float)) self._validate_bounds(value) return value @@ -880,8 +872,8 @@ class Perc(_Numeric): def to_py( self, - value: typing.Union[float, int, str, _UnsetNone] - ) -> typing.Union[float, int, _UnsetNone]: + value: Union[float, int, str, _UnsetNone] + ) -> Union[float, int, _UnsetNone]: self._basic_py_validation(value, (float, int, str)) if isinstance(value, usertypes.Unset): return value @@ -898,7 +890,7 @@ class Perc(_Numeric): self._validate_bounds(value, suffix='%') return value - def to_str(self, value: typing.Union[None, float, int, str]) -> str: + def to_str(self, value: Union[None, float, int, str]) -> str: if value is None: return '' elif isinstance(value, str): @@ -929,7 +921,7 @@ class PercOrInt(_Numeric): raise ValueError("minperc ({}) needs to be <= maxperc " "({})!".format(self.minperc, self.maxperc)) - def from_str(self, value: str) -> typing.Union[None, str, int]: + def from_str(self, value: str) -> Union[None, str, int]: self._basic_str_validation(value) if not value: return None @@ -946,10 +938,7 @@ class PercOrInt(_Numeric): self.to_py(intval) return intval - def to_py( - self, - value: typing.Union[None, str, int] - ) -> typing.Union[None, str, int]: + def to_py(self, value: Union[None, str, int]) -> Union[None, str, int]: """Expect a value like '42%' as string, or 23 as int.""" self._basic_py_validation(value, (int, str)) if value is None: @@ -1076,7 +1065,7 @@ class QtColor(BaseType): except ValueError: raise configexc.ValidationError(val, "must be a valid color value") - def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QColor]: + def to_py(self, value: _StrUnset) -> Union[_UnsetNone, QColor]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1088,12 +1077,12 @@ class QtColor(BaseType): kind = value[:openparen] vals = value[openparen+1:-1].split(',') - converters = { + converters: DictType[str, Callable[..., QColor]] = { 'rgba': QColor.fromRgb, 'rgb': QColor.fromRgb, 'hsva': QColor.fromHsv, 'hsv': QColor.fromHsv, - } # type: typing.Dict[str, typing.Callable[..., QColor]] + } conv = converters.get(kind) if not conv: @@ -1159,8 +1148,8 @@ class FontBase(BaseType): """Base class for Font/FontFamily.""" # Gets set when the config is initialized. - default_family = None # type: str - default_size = None # type: str + default_family: Optional[str] = None + default_size: Optional[str] = None font_regex = re.compile(r""" ( ( @@ -1178,8 +1167,7 @@ class FontBase(BaseType): (?P.+) # mandatory font family""", re.VERBOSE) @classmethod - def set_defaults(cls, default_family: typing.List[str], - default_size: str) -> None: + def set_defaults(cls, default_family: ListType[str], default_size: str) -> None: """Make sure default_family/default_size are available. If the given family value (fonts.default_family in the config) is @@ -1232,7 +1220,7 @@ class FontBase(BaseType): cls.default_family = families.to_str(quote=True) cls.default_size = default_size - def to_py(self, value: typing.Any) -> typing.Any: + def to_py(self, value: Any) -> Any: raise NotImplementedError @@ -1315,7 +1303,7 @@ class Regex(BaseType): operator.or_, (getattr(re, flag.strip()) for flag in flags.split(' | '))) - def _compile_regex(self, pattern: str) -> typing.Pattern[str]: + def _compile_regex(self, pattern: str) -> Pattern[str]: """Check if the given regex is valid. Some semi-invalid regexes can also raise warnings - we also treat them as @@ -1336,8 +1324,8 @@ class Regex(BaseType): def to_py( self, - value: typing.Union[str, typing.Pattern[str], usertypes.Unset] - ) -> typing.Union[_UnsetNone, typing.Pattern[str]]: + value: Union[str, Pattern[str], usertypes.Unset] + ) -> Union[_UnsetNone, Pattern[str]]: """Get a compiled regex from either a string or a regex object.""" self._basic_py_validation(value, (str, self._regex_type)) if isinstance(value, usertypes.Unset): @@ -1349,8 +1337,7 @@ class Regex(BaseType): else: return value - def to_str(self, - value: typing.Union[None, str, typing.Pattern[str]]) -> str: + def to_str(self, value: Union[None, str, Pattern[str]]) -> str: if value is None: return '' elif isinstance(value, self._regex_type): @@ -1370,10 +1357,10 @@ class Dict(BaseType): When setting from a string, pass a json-like dict, e.g. `{"key", "value"}`. """ - def __init__(self, keytype: typing.Union[String, 'Key'], + def __init__(self, keytype: Union[String, 'Key'], valtype: BaseType, *, - fixed_keys: typing.Iterable = None, - required_keys: typing.Iterable = None, + fixed_keys: Iterable = None, + required_keys: Iterable = None, none_ok: bool = False) -> None: super().__init__(none_ok) # If the keytype is not a string, we'll get problems with showing it as @@ -1384,7 +1371,7 @@ class Dict(BaseType): self.fixed_keys = fixed_keys self.required_keys = required_keys - def _validate_keys(self, value: typing.Dict) -> None: + def _validate_keys(self, value: DictType) -> None: if (self.fixed_keys is not None and not set(value.keys()).issubset(self.fixed_keys)): raise configexc.ValidationError( @@ -1395,7 +1382,7 @@ class Dict(BaseType): raise configexc.ValidationError( value, "Required keys {}".format(self.required_keys)) - def from_str(self, value: str) -> typing.Optional[typing.Dict]: + def from_str(self, value: str) -> Optional[DictType]: self._basic_str_validation(value) if not value: return None @@ -1410,14 +1397,14 @@ class Dict(BaseType): self.to_py(yaml_val) return yaml_val - def from_obj(self, value: typing.Optional[typing.Dict]) -> typing.Dict: + def from_obj(self, value: Optional[DictType]) -> DictType: if value is None: return {} return {self.keytype.from_obj(key): self.valtype.from_obj(val) for key, val in value.items()} - def _fill_fixed_keys(self, value: typing.Dict) -> typing.Dict: + def _fill_fixed_keys(self, value: DictType) -> DictType: """Fill missing fixed keys with a None-value.""" if self.fixed_keys is None: return value @@ -1428,8 +1415,8 @@ class Dict(BaseType): def to_py( self, - value: typing.Union[typing.Dict, _UnsetNone] - ) -> typing.Union[typing.Dict, usertypes.Unset]: + value: Union[DictType, _UnsetNone] + ) -> Union[DictType, usertypes.Unset]: self._basic_py_validation(value, dict) if isinstance(value, usertypes.Unset): return value @@ -1445,13 +1432,13 @@ class Dict(BaseType): for key, val in value.items()} return self._fill_fixed_keys(d) - def to_str(self, value: typing.Dict) -> str: + def to_str(self, value: DictType) -> str: if not value: # An empty Dict is treated just like None -> empty string return '' return json.dumps(value, sort_keys=True) - def to_doc(self, value: typing.Dict, indent: int = 0) -> str: + def to_doc(self, value: DictType, indent: int = 0) -> str: if not value: return 'empty' lines = ['\n'] @@ -1474,7 +1461,7 @@ class File(BaseType): """A file on the local filesystem.""" - def __init__(self, required: bool = True, **kwargs: typing.Any) -> None: + def __init__(self, required: bool = True, **kwargs: Any) -> None: super().__init__(**kwargs) self.required = required @@ -1540,7 +1527,7 @@ class FormatString(BaseType): completions: completions to be used, or None """ - def __init__(self, fields: typing.Iterable[str], + def __init__(self, fields: Iterable[str], none_ok: bool = False, completions: _Completions = None) -> None: super().__init__(none_ok) @@ -1593,8 +1580,8 @@ class ShellCommand(List): def to_py( self, - value: typing.Union[typing.List, usertypes.Unset], - ) -> typing.Union[typing.List, usertypes.Unset]: + value: Union[ListType, usertypes.Unset], + ) -> Union[ListType, usertypes.Unset]: py_value = super().to_py(value) if isinstance(py_value, usertypes.Unset): return py_value @@ -1627,7 +1614,7 @@ class Proxy(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[_UnsetNone, QNetworkProxy, _SystemProxy, pac.PACFetcher]: + ) -> Union[_UnsetNone, QNetworkProxy, _SystemProxy, pac.PACFetcher]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1697,7 +1684,7 @@ class FuzzyUrl(BaseType): """A URL which gets interpreted as search if needed.""" - def to_py(self, value: _StrUnset) -> typing.Union[QUrl, _UnsetNone]: + def to_py(self, value: _StrUnset) -> Union[QUrl, _UnsetNone]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1715,10 +1702,10 @@ class PaddingValues: """Four padding values.""" - top = attr.ib() # type: int - bottom = attr.ib() # type: int - left = attr.ib() # type: int - right = attr.ib() # type: int + top: int = attr.ib() + bottom: int = attr.ib() + left: int = attr.ib() + right: int = attr.ib() class Padding(Dict): @@ -1735,8 +1722,8 @@ class Padding(Dict): def to_py( # type: ignore[override] self, - value: typing.Union[typing.Dict, _UnsetNone], - ) -> typing.Union[usertypes.Unset, PaddingValues]: + value: Union[DictType, _UnsetNone], + ) -> Union[usertypes.Unset, PaddingValues]: d = super().to_py(value) if isinstance(d, usertypes.Unset): return d @@ -1807,7 +1794,7 @@ class Url(BaseType): """A URL as a string.""" - def to_py(self, value: _StrUnset) -> typing.Union[_UnsetNone, QUrl]: + def to_py(self, value: _StrUnset) -> Union[_UnsetNone, QUrl]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1877,8 +1864,8 @@ class ConfirmQuit(FlagList): def to_py( self, - value: typing.Union[usertypes.Unset, typing.List], - ) -> typing.Union[typing.List, usertypes.Unset]: + value: Union[usertypes.Unset, ListType], + ) -> Union[ListType, usertypes.Unset]: values = super().to_py(value) if isinstance(values, usertypes.Unset): return values @@ -1931,7 +1918,7 @@ class Key(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[_UnsetNone, keyutils.KeySequence]: + ) -> Union[_UnsetNone, keyutils.KeySequence]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value @@ -1955,7 +1942,7 @@ class UrlPattern(BaseType): def to_py( self, value: _StrUnset - ) -> typing.Union[_UnsetNone, urlmatch.UrlPattern]: + ) -> Union[_UnsetNone, urlmatch.UrlPattern]: self._basic_py_validation(value, str) if isinstance(value, usertypes.Unset): return value From 174c265a0c44acee4b58809247ba8009a72467e7 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 31 Oct 2020 12:59:25 +0100 Subject: [PATCH 005/194] Improve OrderedDict annotation --- qutebrowser/config/configutils.py | 8 ++++---- qutebrowser/utils/docutils.py | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index d2be3b9f7..7c2d4ee8c 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -25,7 +25,8 @@ import collections import itertools import operator from typing import ( - TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Union) + TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Sequence, Set, Union, + MutableMapping) from PyQt5.QtCore import QUrl @@ -97,9 +98,8 @@ class Values: opt: 'configdata.Option', values: Sequence[ScopedValue] = ()) -> None: self.opt = opt - self._vmap: 'collections.OrderedDict[Values._VmapKeyType, ScopedValue]' = ( - collections.OrderedDict() - ) + self._vmap: MutableMapping[ + Values._VmapKeyType, ScopedValue] = collections.OrderedDict() # A map from domain parts to rules that fall under them. self._domain_map: Dict[ Optional[str], Set[ScopedValue]] = collections.defaultdict(set) diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py index 5a3a0d263..f80931ea8 100644 --- a/qutebrowser/utils/docutils.py +++ b/qutebrowser/utils/docutils.py @@ -25,7 +25,7 @@ import inspect import os.path import collections import enum -from typing import Callable, Dict, Optional, List, Union +from typing import Callable, MutableMapping, Optional, List, Union import qutebrowser from qutebrowser.utils import log, utils @@ -98,7 +98,8 @@ class DocstringParser: self._cur_arg_name: Optional[str] = None self._short_desc_parts: List[str] = [] self._long_desc_parts: List[str] = [] - self.arg_descs: Dict[str, Union[str, List[str]]] = collections.OrderedDict() + self.arg_descs: MutableMapping[ + str, Union[str, List[str]]] = collections.OrderedDict() doc = inspect.getdoc(func) handlers = { self.State.short: self._parse_short, From 2b130a1781ce908154d8aa881945df3d5d585151 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 31 Oct 2020 13:39:15 +0100 Subject: [PATCH 006/194] Fix lint --- qutebrowser/utils/docutils.py | 2 +- qutebrowser/utils/qtutils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qutebrowser/utils/docutils.py b/qutebrowser/utils/docutils.py index f80931ea8..f1ee11f84 100644 --- a/qutebrowser/utils/docutils.py +++ b/qutebrowser/utils/docutils.py @@ -99,7 +99,7 @@ class DocstringParser: self._short_desc_parts: List[str] = [] self._long_desc_parts: List[str] = [] self.arg_descs: MutableMapping[ - str, Union[str, List[str]]] = collections.OrderedDict() + str, Union[str, List[str]]] = collections.OrderedDict() doc = inspect.getdoc(func) handlers = { self.State.short: self._parse_short, diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index c92827f99..1c702eb05 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -48,7 +48,7 @@ if TYPE_CHECKING: from PyQt5.QtWebEngineWidgets import QWebEngineHistory from qutebrowser.misc import objects -from qutebrowser.utils import usertypes +from qutebrowser.utils import usertypes, version MAXVALS = { From 22d2447acb7085e73c591a7648ddf1cb22d54f2f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Sat, 31 Oct 2020 13:39:56 +0100 Subject: [PATCH 007/194] Remove accidental import --- qutebrowser/utils/qtutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py index 1c702eb05..c92827f99 100644 --- a/qutebrowser/utils/qtutils.py +++ b/qutebrowser/utils/qtutils.py @@ -48,7 +48,7 @@ if TYPE_CHECKING: from PyQt5.QtWebEngineWidgets import QWebEngineHistory from qutebrowser.misc import objects -from qutebrowser.utils import usertypes, version +from qutebrowser.utils import usertypes MAXVALS = { From 0a9b85c310e0e74384b6e1c698b47a634db52aa2 Mon Sep 17 00:00:00 2001 From: Tim Brown Date: Sat, 31 Oct 2020 22:52:00 +1000 Subject: [PATCH 008/194] mypy: use from-import for typing in remaining files I believe this should close #5396 --- qutebrowser/app.py | 8 ++++---- qutebrowser/misc/consolewidget.py | 4 ++-- qutebrowser/misc/crashdialog.py | 4 ++-- scripts/mkvenv.py | 6 +++--- tests/unit/api/test_cmdutils.py | 12 ++++++------ 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/qutebrowser/app.py b/qutebrowser/app.py index 27f03fd54..a7a8d45f8 100644 --- a/qutebrowser/app.py +++ b/qutebrowser/app.py @@ -43,7 +43,7 @@ import functools import tempfile import datetime import argparse -import typing +from typing import Iterable, Optional, cast from PyQt5.QtWidgets import QApplication, QWidget from PyQt5.QtGui import QDesktopServices, QPixmap, QIcon @@ -74,7 +74,7 @@ from qutebrowser.misc import utilcmds # pylint: enable=unused-import -q_app = typing.cast(QApplication, None) +q_app = cast(QApplication, None) def run(args): @@ -256,7 +256,7 @@ def process_pos_args(args, via_ipc=False, cwd=None, target_arg=None): if command_target in {'window', 'private-window'}: command_target = 'tab-silent' - win_id = None # type: typing.Optional[int] + win_id: Optional[int] = None if via_ipc and not args: win_id = mainwindow.get_window(via_ipc=via_ipc, @@ -325,7 +325,7 @@ def _open_startpage(win_id=None): If set, open the startpage in the given window. """ if win_id is not None: - window_ids = [win_id] # type: typing.Iterable[int] + window_ids: Iterable[int] = [win_id] else: window_ids = objreg.window_registry for cur_win_id in list(window_ids): # Copying as the dict could change diff --git a/qutebrowser/misc/consolewidget.py b/qutebrowser/misc/consolewidget.py index aed42237a..14e4a7dc3 100644 --- a/qutebrowser/misc/consolewidget.py +++ b/qutebrowser/misc/consolewidget.py @@ -21,7 +21,7 @@ import sys import code -import typing +from typing import MutableSequence from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt from PyQt5.QtWidgets import QTextEdit, QWidget, QVBoxLayout, QApplication @@ -165,7 +165,7 @@ class ConsoleWidget(QWidget): 'objreg': objreg, } self._more = False - self._buffer = [] # type: typing.MutableSequence[str] + self._buffer: MutableSequence[str] = [] self._lineedit = ConsoleLineEdit(namespace, self) self._lineedit.execute.connect(self.push) self._output = ConsoleTextEdit() diff --git a/qutebrowser/misc/crashdialog.py b/qutebrowser/misc/crashdialog.py index 850883bf4..2bdb790e7 100644 --- a/qutebrowser/misc/crashdialog.py +++ b/qutebrowser/misc/crashdialog.py @@ -28,7 +28,7 @@ import fnmatch import traceback import datetime import enum -import typing +from typing import List, Tuple import pkg_resources from PyQt5.QtCore import pyqtSlot, Qt, QSize @@ -119,7 +119,7 @@ class _CrashDialog(QDialog): super().__init__(parent) # We don't set WA_DeleteOnClose here as on an exception, we'll get # closed anyways, and it only could have unintended side-effects. - self._crash_info = [] # type: typing.List[typing.Tuple[str, str]] + self._crash_info: List[Tuple[str, str]] = [] self._btn_box = None self._paste_text = None self.setWindowTitle("Whoops!") diff --git a/scripts/mkvenv.py b/scripts/mkvenv.py index 5eeb90640..f582e23b0 100644 --- a/scripts/mkvenv.py +++ b/scripts/mkvenv.py @@ -26,10 +26,10 @@ import pathlib import sys import os import os.path -import typing import shutil import venv import subprocess +from typing import List, Optional, Tuple sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) from scripts import utils, link_pyqt @@ -77,7 +77,7 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def pyqt_versions() -> typing.List[str]: +def pyqt_versions() -> List[str]: """Get a list of all available PyQt versions. The list is based on the filenames of misc/requirements/ files. @@ -224,7 +224,7 @@ def install_qutebrowser(venv_dir: pathlib.Path) -> None: def regenerate_docs(venv_dir: pathlib.Path, - asciidoc: typing.Optional[typing.Tuple[str, str]]): + asciidoc: Optional[Tuple[str, str]]): """Regenerate docs using asciidoc.""" utils.print_title("Generating documentation") if asciidoc is not None: diff --git a/tests/unit/api/test_cmdutils.py b/tests/unit/api/test_cmdutils.py index 3ee486303..811ba2659 100644 --- a/tests/unit/api/test_cmdutils.py +++ b/tests/unit/api/test_cmdutils.py @@ -24,8 +24,8 @@ import sys import logging import types -import typing import enum +from typing import Union import pytest @@ -301,17 +301,17 @@ class TestRegister: (int, 'x', None, cmdexc.ArgumentTypeError), (str, 'foo', None, 'foo'), - (typing.Union[str, int], 'foo', None, 'foo'), - (typing.Union[str, int], '42', None, 42), + (Union[str, int], 'foo', None, 'foo'), + (Union[str, int], '42', None, 42), # Choices (str, 'foo', ['foo'], 'foo'), (str, 'bar', ['foo'], cmdexc.ArgumentTypeError), # Choices with Union: only checked when it's a str - (typing.Union[str, int], 'foo', ['foo'], 'foo'), - (typing.Union[str, int], 'bar', ['foo'], cmdexc.ArgumentTypeError), - (typing.Union[str, int], '42', ['foo'], 42), + (Union[str, int], 'foo', ['foo'], 'foo'), + (Union[str, int], 'bar', ['foo'], cmdexc.ArgumentTypeError), + (Union[str, int], '42', ['foo'], 42), (Enum, 'x', None, Enum.x), (Enum, 'z', None, cmdexc.ArgumentTypeError), From 2db5c556041565425c73a5c8bc5daaa75aaecbb9 Mon Sep 17 00:00:00 2001 From: Jan Palus Date: Sun, 1 Nov 2020 20:30:14 +0100 Subject: [PATCH 009/194] add {0} as another placeholder available in searchengine fixes #5844 --- qutebrowser/config/configdata.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 981bfb07f..ba2a2a277 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2045,6 +2045,7 @@ url.searchengines: expands to `slash%2Fand%26amp`). * `{unquoted}` quotes nothing (for `slash/and&` this placeholder expands to `slash/and&`). + * `{0}` same as `{}` but can be used multiple times. The search engine named `DEFAULT` is used when `url.auto_search` is turned on and something else than a URL was entered to be opened. Other search From fb69e6627cfef6247f131a1ce32799415a526b5a Mon Sep 17 00:00:00 2001 From: qutebrowser bot Date: Mon, 2 Nov 2020 04:30:06 +0000 Subject: [PATCH 010/194] Update dependencies --- misc/requirements/requirements-check-manifest.txt | 8 ++++++-- misc/requirements/requirements-dev.txt | 4 ++-- misc/requirements/requirements-pyinstaller.txt | 2 +- misc/requirements/requirements-pylint.txt | 2 +- misc/requirements/requirements-tests.txt | 10 +++++----- misc/requirements/requirements-tox.txt | 2 +- misc/requirements/requirements-vulture.txt | 2 +- 7 files changed, 17 insertions(+), 13 deletions(-) diff --git a/misc/requirements/requirements-check-manifest.txt b/misc/requirements/requirements-check-manifest.txt index 9993cf4dd..8b8f6ba1a 100644 --- a/misc/requirements/requirements-check-manifest.txt +++ b/misc/requirements/requirements-check-manifest.txt @@ -1,5 +1,9 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -check-manifest==0.44 +build==0.1.0 +check-manifest==0.45 +packaging==20.4 pep517==0.9.1 -toml==0.10.1 +pyparsing==2.4.7 +six==1.15.0 +toml==0.10.2 diff --git a/misc/requirements/requirements-dev.txt b/misc/requirements/requirements-dev.txt index e885b3335..b795d951e 100644 --- a/misc/requirements/requirements-dev.txt +++ b/misc/requirements/requirements-dev.txt @@ -5,7 +5,7 @@ certifi==2020.6.20 cffi==1.14.3 chardet==3.0.4 colorama==0.4.4 -cryptography==3.2 +cryptography==3.2.1 cssutils==1.0.2 github3.py==1.3.0 hunter==3.3.1 @@ -21,6 +21,6 @@ python-dateutil==2.8.1 requests==2.24.0 sip==5.4.0 six==1.15.0 -toml==0.10.1 +toml==0.10.2 uritemplate==3.0.1 # urllib3==1.25.11 diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt index 7941d2772..a48e80fde 100644 --- a/misc/requirements/requirements-pyinstaller.txt +++ b/misc/requirements/requirements-pyinstaller.txt @@ -2,4 +2,4 @@ altgraph==0.17 pyinstaller==4.0 -pyinstaller-hooks-contrib==2020.9 +pyinstaller-hooks-contrib==2020.10 diff --git a/misc/requirements/requirements-pylint.txt b/misc/requirements/requirements-pylint.txt index 75a0e8e0f..8a37c3b01 100644 --- a/misc/requirements/requirements-pylint.txt +++ b/misc/requirements/requirements-pylint.txt @@ -4,7 +4,7 @@ astroid==2.3.3 # rq.filter: < 2.4 certifi==2020.6.20 cffi==1.14.3 chardet==3.0.4 -cryptography==3.2 +cryptography==3.2.1 github3.py==1.3.0 idna==2.10 isort==4.3.21 diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt index 066f4c4db..480658aba 100644 --- a/misc/requirements/requirements-tests.txt +++ b/misc/requirements/requirements-tests.txt @@ -15,7 +15,7 @@ filelock==3.0.12 Flask==1.1.2 glob2==0.7 hunter==3.3.1 -hypothesis==5.38.0 +hypothesis==5.41.0 icdiff==1.9.1 idna==2.10 iniconfig==1.1.1 @@ -25,7 +25,7 @@ jaraco.functools==3.0.1 Mako==1.1.3 manhole==1.6.0 # MarkupSafe==1.1.1 -more-itertools==8.5.0 +more-itertools==8.6.0 packaging==20.4 parse==1.18.0 parse-type==0.5.2 @@ -35,7 +35,7 @@ py==1.9.0 py-cpuinfo==7.0.0 Pygments==2.7.2 pyparsing==2.4.7 -pytest==6.1.1 +pytest==6.1.2 pytest-bdd==4.0.1 pytest-benchmark==3.2.3 pytest-clarity==0.3.0a0 @@ -45,7 +45,7 @@ pytest-icdiff==0.5 pytest-instafail==0.4.2 pytest-mock==3.3.1 pytest-qt==3.3.0 -pytest-repeat==0.8.0 +pytest-repeat==0.9.1 pytest-rerunfailures==9.1.1 pytest-xdist==2.1.0 pytest-xvfb==2.0.0 @@ -57,7 +57,7 @@ sortedcontainers==2.2.2 soupsieve==2.0.1 termcolor==1.1.0 tldextract==3.0.2 -toml==0.10.1 +toml==0.10.2 urllib3==1.25.11 vulture==2.1 Werkzeug==1.0.1 diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt index a77333637..9241f07a6 100644 --- a/misc/requirements/requirements-tox.txt +++ b/misc/requirements/requirements-tox.txt @@ -8,7 +8,7 @@ pluggy==0.13.1 py==1.9.0 pyparsing==2.4.7 six==1.15.0 -toml==0.10.1 +toml==0.10.2 tox==3.20.1 tox-pip-version==0.0.7 tox-venv==0.4.0 diff --git a/misc/requirements/requirements-vulture.txt b/misc/requirements/requirements-vulture.txt index 70848d8ef..e0ea82ea2 100644 --- a/misc/requirements/requirements-vulture.txt +++ b/misc/requirements/requirements-vulture.txt @@ -1,4 +1,4 @@ # This file is automatically generated by scripts/dev/recompile_requirements.py -toml==0.10.1 +toml==0.10.2 vulture==2.1 From 09be405bb8004c8ec6e568b394b8af485645698f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 09:55:38 +0100 Subject: [PATCH 011/194] Pre-select default text for prompts --- doc/changelog.asciidoc | 2 ++ qutebrowser/mainwindow/prompt.py | 1 + 2 files changed, 3 insertions(+) diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc index 129378b07..027fd0938 100644 --- a/doc/changelog.asciidoc +++ b/doc/changelog.asciidoc @@ -39,6 +39,8 @@ Changed the case for character sets starting with a literal `[` or containing literal character sequences `--`, `&&`, `~~`, or `||`. To avoid a warning, remove the duplicate characters or escape them with a backslash. +- If `prompt(..., "default")` is used via JS, the default text is now + pre-selected in the prompt shown by qutebrowser. v1.14.0 (2020-10-15) -------------------- diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py index 32fd9709e..42b6c3d97 100644 --- a/qutebrowser/mainwindow/prompt.py +++ b/qutebrowser/mainwindow/prompt.py @@ -590,6 +590,7 @@ class LineEditPrompt(_BasePrompt): self._vbox.addWidget(self._lineedit) if question.default: self._lineedit.setText(question.default) + self._lineedit.selectAll() self.setFocusProxy(self._lineedit) self._init_key_label() From 5eabf460b9709c62a6535bc8eda52c2141177cb2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 11:11:47 +0100 Subject: [PATCH 012/194] Make sure we don't cancel downloads which are done With b997220c3c2976a71a45c42509ea55db2762749d, we introduced code which calls .cancel() on all downloads on shutdown. However, this includes downloads which were already finished, causing their finished signal to be emitted a second time. Combined with 4cd255c7372625b4c3a630844ffd2b42d6ddde58, this caused #5849 (though there might be other ways to reproduce?): - Window is shutting down, thus not available via objreg anymore - Downloads get cancelled - Completed PDF downloads emit finished again - PDFjs tries to open again, but no window is available anymore --- qutebrowser/browser/downloads.py | 5 ++++- qutebrowser/browser/webengine/webenginedownloads.py | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index 5430cde20..fb8ede0f7 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -574,6 +574,8 @@ class AbstractDownloadItem(QObject): Args: remove_data: Whether to remove the downloaded data. """ + # FIXME: retry() calls cancel() with self.done = True - is that a good idea? + assert not self.done or not self.successful, self.done self._do_cancel() log.downloads.debug("cancelled") if remove_data: @@ -982,7 +984,8 @@ class AbstractDownloadManager(QObject): def shutdown(self): """Cancel all downloads when shutting down.""" for download in self.downloads: - download.cancel(remove_data=False) + if not download.done: + download.cancel(remove_data=False) class DownloadModel(QAbstractListModel): diff --git a/qutebrowser/browser/webengine/webenginedownloads.py b/qutebrowser/browser/webengine/webenginedownloads.py index 16c7f020a..a1ee7c206 100644 --- a/qutebrowser/browser/webengine/webenginedownloads.py +++ b/qutebrowser/browser/webengine/webenginedownloads.py @@ -102,13 +102,17 @@ class DownloadItem(downloads.AbstractDownloadItem): self._qt_item.cancel() def _do_cancel(self): + state = self._qt_item.state() + state_name = debug.qenum_key(QWebEngineDownloadItem, state) + assert state not in [QWebEngineDownloadItem.DownloadCompleted, + QWebEngineDownloadItem.DownloadCancelled], state_name self._qt_item.cancel() def retry(self): state = self._qt_item.state() if state != QWebEngineDownloadItem.DownloadInterrupted: log.downloads.warning( - "Trying to retry download in state {}".format( + "Refusing to retry download in state {}".format( debug.qenum_key(QWebEngineDownloadItem, state))) return From 35b232372dd6d3c105a333df338691fcbb27fe30 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 11:17:37 +0100 Subject: [PATCH 013/194] Improve retrying QtNetwork downloads - Don't call cancel() in retry(), instead only do the actually needed steps. - Call remove/delete before starting the new download (to avoid race conditions and flickering). --- qutebrowser/browser/downloads.py | 3 +-- qutebrowser/browser/qtnetworkdownloads.py | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/qutebrowser/browser/downloads.py b/qutebrowser/browser/downloads.py index fb8ede0f7..31a9d7f29 100644 --- a/qutebrowser/browser/downloads.py +++ b/qutebrowser/browser/downloads.py @@ -574,8 +574,7 @@ class AbstractDownloadItem(QObject): Args: remove_data: Whether to remove the downloaded data. """ - # FIXME: retry() calls cancel() with self.done = True - is that a good idea? - assert not self.done or not self.successful, self.done + assert not self.done self._do_cancel() log.downloads.debug("cancelled") if remove_data: diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 44e6c45d5..c5bfc07e6 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -181,11 +181,16 @@ class DownloadItem(downloads.AbstractDownloadItem): assert self.done assert not self.successful assert self._retry_info is not None + + # Not calling self.cancel() here because the download is done (albeit + # unsuccessfully) + self.remove() + self.delete() + new_reply = self._retry_info.manager.get(self._retry_info.request) new_download = self._manager.fetch(new_reply, suggested_filename=self.basename) self.adopt_download.emit(new_download) - self.cancel() def _get_open_filename(self): filename = self._filename From 8028e73d343dec78be509442c2f12127a04f735f Mon Sep 17 00:00:00 2001 From: Tim Brown Date: Sun, 1 Nov 2020 14:39:48 +1000 Subject: [PATCH 014/194] mypy: use annotations for typing instead of comments --- qutebrowser/browser/webengine/interceptor.py | 4 ++-- qutebrowser/config/stylesheet.py | 6 +++--- qutebrowser/keyinput/modeman.py | 16 ++++++++-------- qutebrowser/utils/utils.py | 3 ++- scripts/asciidoc2html.py | 8 ++++---- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/qutebrowser/browser/webengine/interceptor.py b/qutebrowser/browser/webengine/interceptor.py index d4dcb522f..e5a414dfd 100644 --- a/qutebrowser/browser/webengine/interceptor.py +++ b/qutebrowser/browser/webengine/interceptor.py @@ -39,9 +39,9 @@ class WebEngineRequest(interceptors.Request): _WHITELISTED_REQUEST_METHODS = {QByteArray(b'GET'), QByteArray(b'HEAD')} - _webengine_info = attr.ib(default=None) # type: QWebEngineUrlRequestInfo + _webengine_info: QWebEngineUrlRequestInfo = attr.ib(default=None) #: If this request has been redirected already - _redirected = attr.ib(init=False, default=False) # type: bool + _redirected: bool = attr.ib(init=False, default=False) def redirect(self, url: QUrl) -> None: if self._redirected: diff --git a/qutebrowser/config/stylesheet.py b/qutebrowser/config/stylesheet.py index 10e6e4e52..3c68fc0b9 100644 --- a/qutebrowser/config/stylesheet.py +++ b/qutebrowser/config/stylesheet.py @@ -81,13 +81,13 @@ class _StyleSheetObserver(QObject): if update: self.setParent(self._obj) if stylesheet is None: - self._stylesheet = obj.STYLESHEET # type: str + self._stylesheet: str = obj.STYLESHEET else: self._stylesheet = stylesheet if update: - self._options = jinja.template_config_variables( - self._stylesheet) # type: Optional[FrozenSet[str]] + self._options: Optional[FrozenSet[str]] = jinja.template_config_variables( + self._stylesheet) else: self._options = None diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py index 27e4be34e..dcc6fa949 100644 --- a/qutebrowser/keyinput/modeman.py +++ b/qutebrowser/keyinput/modeman.py @@ -54,8 +54,8 @@ class KeyEvent: text: A string (QKeyEvent::text). """ - key = attr.ib() # type: Qt.Key - text = attr.ib() # type: str + key: Qt.Key = attr.ib() + text: str = attr.ib() @classmethod def from_event(cls, event: QKeyEvent) -> 'KeyEvent': @@ -89,7 +89,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager': modeman.hintmanager = hintmanager - keyparsers = { + keyparsers: ParserDictType = { usertypes.KeyMode.normal: modeparsers.NormalKeyParser( win_id=win_id, @@ -186,7 +186,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager': win_id=win_id, commandrunner=commandrunner, parent=modeman), - } # type: ParserDictType + } for mode, parser in keyparsers.items(): modeman.register(mode, parser) @@ -259,10 +259,10 @@ class ModeManager(QObject): def __init__(self, win_id: int, parent: QObject = None) -> None: super().__init__(parent) self._win_id = win_id - self.parsers = {} # type: ParserDictType + self.parsers: ParserDictType = {} self._prev_mode = usertypes.KeyMode.normal self.mode = usertypes.KeyMode.normal - self._releaseevents_to_pass = set() # type: Set[KeyEvent] + self._releaseevents_to_pass: Set[KeyEvent] = set() # Set after __init__ self.hintmanager = cast(hints.HintManager, None) @@ -457,12 +457,12 @@ class ModeManager(QObject): Return: True if event should be filtered, False otherwise. """ - handlers = { + handlers: Mapping[QEvent.Type, Callable[[QKeyEvent], bool]] = { QEvent.KeyPress: self._handle_keypress, QEvent.KeyRelease: self._handle_keyrelease, QEvent.ShortcutOverride: functools.partial(self._handle_keypress, dry_run=True), - } # type: Mapping[QEvent.Type, Callable[[QKeyEvent], bool]] + } handler = handlers[event.type()] return handler(cast(QKeyEvent, event)) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 6952acf97..21961101f 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -181,7 +181,8 @@ def read_file(filename: str, binary: bool = False) -> Any: # https://github.com/pyinstaller/pyinstaller/wiki/FAQ#misc fn = os.path.join(os.path.dirname(sys.executable), filename) if binary: - with open(fn, 'rb') as f: # type: IO + f: IO + with open(fn, 'rb') as f: return f.read() else: with open(fn, 'r', encoding='utf-8') as f: diff --git a/scripts/asciidoc2html.py b/scripts/asciidoc2html.py index 17c3fb367..109e61625 100755 --- a/scripts/asciidoc2html.py +++ b/scripts/asciidoc2html.py @@ -49,13 +49,13 @@ class AsciiDoc: asciidoc: Optional[str], asciidoc_python: Optional[str], website: Optional[str]) -> None: - self._cmd = None # type: Optional[List[str]] + self._cmd: Optional[List[str]] = None self._asciidoc = asciidoc self._asciidoc_python = asciidoc_python self._website = website - self._homedir = None # type: Optional[pathlib.Path] - self._themedir = None # type: Optional[pathlib.Path] - self._tempdir = None # type: Optional[pathlib.Path] + self._homedir: Optional[pathlib.Path] = None + self._themedir: Optional[pathlib.Path] = None + self._tempdir: Optional[pathlib.Path] = None self._failed = False def prepare(self) -> None: From 0a5c2114c1f74c7bcad6ff67f64c0d683497657b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 16:29:02 +0100 Subject: [PATCH 015/194] scripts: Add 'all' to misc_checks --- scripts/dev/misc_checks.py | 27 ++++++++++++++++++--------- tox.ini | 5 +---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index df2845405..7058e168f 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -203,20 +203,29 @@ def check_userscripts_descriptions(_args: argparse.Namespace = None) -> bool: def main() -> int: + checkers = { + 'git': check_git, + 'vcs': check_vcs_conflict, + 'spelling': check_spelling, + 'userscripts': check_userscripts_descriptions, + } + parser = argparse.ArgumentParser() parser.add_argument('--verbose', action='store_true', help='Show checked filenames') parser.add_argument('checker', - choices=('git', 'vcs', 'spelling', 'userscripts'), + choices=list(checkers) + ['all'], help="Which checker to run.") args = parser.parse_args() - if args.checker == 'git': - ok = check_git(args) - elif args.checker == 'vcs': - ok = check_vcs_conflict(args) - elif args.checker == 'spelling': - ok = check_spelling(args) - elif args.checker == 'userscripts': - ok = check_userscripts_descriptions(args) + + if args.checker == 'all': + retvals = [] + for name, checker in checkers.items(): + utils.print_title(name) + retvals.append(checker(args)) + return 0 if all(retvals) else 1 + + checker = checkers[args.checker] + ok = checker(args) return 0 if ok else 1 diff --git a/tox.ini b/tox.ini index bfc1f2b49..a00122333 100644 --- a/tox.ini +++ b/tox.ini @@ -50,10 +50,7 @@ pip_version = pip passenv = HOME deps = commands = - {envpython} scripts/dev/misc_checks.py git - {envpython} scripts/dev/misc_checks.py vcs - {envpython} scripts/dev/misc_checks.py spelling - {envpython} scripts/dev/misc_checks.py userscripts + {envpython} scripts/dev/misc_checks.py all [testenv:vulture] basepython = {env:PYTHON:python3} From 6d04af727d613ea7cfdd25867a2b76003af07e66 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 16:48:59 +0100 Subject: [PATCH 016/194] scripts: Improve spell checks in misc_checks.py - Precompile patterns which leads to a nice speedup (8s -> 2.75s on my machine) - Add an explanation - Output messages in a way we can use GitHub Actions problem matchers - Add those problem matchers --- scripts/dev/ci/problemmatchers.py | 14 ++++++++++++++ scripts/dev/misc_checks.py | 29 +++++++++++++++++++---------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/scripts/dev/ci/problemmatchers.py b/scripts/dev/ci/problemmatchers.py index 320d0deeb..3e804af05 100644 --- a/scripts/dev/ci/problemmatchers.py +++ b/scripts/dev/ci/problemmatchers.py @@ -182,6 +182,20 @@ MATCHERS = { ], }, ], + + "misc": [ + { + "severity": "error", + "pattern": [ + { + "regexp": r'^([^:]+):(\d+): (Found .*)', + "file": 1, + "line": 2, + "message": 3, + } + ] + } + ] } diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 7058e168f..f870e9666 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -101,6 +101,16 @@ def check_git(_args: argparse.Namespace = None) -> bool: return status +def _check_spelling_file(path, fobj, patterns): + ok = True + for num, line in enumerate(fobj, start=1): + for pattern, explanation in patterns: + if pattern.search(line): + ok = False + print(f'{path}:{num}: Found "{pattern.pattern}" ({explanation})') + return ok + + def check_spelling(args: argparse.Namespace) -> Optional[bool]: """Check commonly misspelled words.""" # Words which I often misspell @@ -119,6 +129,13 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: 'eventloops', 'sizehint', 'statemachine', 'metaobject', 'logrecord'} + patterns = [ + ( + re.compile(r'[{}{}]{}'.format(w[0], w[0].upper(), w[1:])), + "Common misspelling or non-US spelling" + ) for w in words + ] + # Files which should be ignored, e.g. because they come from another # package hint_data = pathlib.Path('tests', 'end2end', 'data', 'hints') @@ -129,20 +146,12 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: hint_data / 'bootstrap' / 'bootstrap.css', ] - seen = collections.defaultdict(list) try: ok = True for path in _get_files(verbose=args.verbose, ignored=ignored): with tokenize.open(str(path)) as f: - for line in f: - for w in words: - pattern = '[{}{}]{}'.format(w[0], w[0].upper(), w[1:]) - if (re.search(pattern, line) and - path not in seen[w] and - '# pragma: no spellcheck' not in line): - print('Found "{}" in {}!'.format(w, path)) - seen[w].append(path) - ok = False + if not _check_spelling_file(path, f, patterns): + ok = False print() return ok except Exception: From 0487383836feb8d78e11bc4f863697069ec9ced2 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 17:02:06 +0100 Subject: [PATCH 017/194] scripts: Disallow blanket noqa/type ignores --- qutebrowser/browser/webkit/webpage.py | 2 +- qutebrowser/misc/checkpyver.py | 2 +- qutebrowser/misc/sessions.py | 2 +- scripts/dev/misc_checks.py | 14 +++++++++++++- scripts/dev/run_vulture.py | 2 +- tests/helpers/stubs.py | 2 +- tests/unit/config/test_configinit.py | 2 +- tests/unit/utils/test_version.py | 2 +- 8 files changed, 20 insertions(+), 8 deletions(-) diff --git a/qutebrowser/browser/webkit/webpage.py b/qutebrowser/browser/webkit/webpage.py index 956b9be9d..c20acb369 100644 --- a/qutebrowser/browser/webkit/webpage.py +++ b/qutebrowser/browser/webkit/webpage.py @@ -365,7 +365,7 @@ class BrowserPage(QWebPage): abort_on=[self.shutting_down, self.loadStarted]) if question is not None: - self.featurePermissionRequestCanceled.connect( # type: ignore + self.featurePermissionRequestCanceled.connect( # type: ignore[attr-defined] functools.partial(self._on_feature_permission_cancelled, question, frame, feature)) diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 116514b8d..98360895a 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -31,7 +31,7 @@ except ImportError: # pragma: no cover try: # Python2 from Tkinter import Tk # type: ignore[import, no-redef] - import tkMessageBox as messagebox # type: ignore # noqa: N813 + import tkMessageBox as messagebox # type: ignore[import, no-redef] # noqa: N813 except ImportError: # Some Python without Tk Tk = None # type: ignore[misc, assignment] diff --git a/qutebrowser/misc/sessions.py b/qutebrowser/misc/sessions.py index eecc1964e..b4aa72f32 100644 --- a/qutebrowser/misc/sessions.py +++ b/qutebrowser/misc/sessions.py @@ -83,7 +83,7 @@ def init(parent=None): def shutdown(session: Optional[ArgType], last_window: bool) -> None: """Handle a shutdown by saving sessions and removing the autosave file.""" if session_manager is None: - return # type: ignore + return # type: ignore[unreachable] try: if session is not None: diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index f870e9666..c8c1936e5 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -107,7 +107,8 @@ def _check_spelling_file(path, fobj, patterns): for pattern, explanation in patterns: if pattern.search(line): ok = False - print(f'{path}:{num}: Found "{pattern.pattern}" ({explanation})') + print(f'{path}:{num}: Found "{pattern.pattern}" - ', end='') + utils.print_col(explanation, 'blue') return ok @@ -135,6 +136,17 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: "Common misspelling or non-US spelling" ) for w in words ] + patterns += [ + ( + re.compile(r'(?i)# noqa(?!: )'), + "Don't use a blanket 'noqa', use something like 'noqa: X123' instead.", + ), + ( + re.compile(r'# type: ignore[^\[]'), + ("Don't use a blanket 'type: ignore', use something like " + "'type: ignore[error-code]' instead."), + ) + ] # Files which should be ignored, e.g. because they come from another # package diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 4e4fd5d5d..5e42febd4 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -42,7 +42,7 @@ from qutebrowser.browser import qutescheme from qutebrowser.config import configtypes -def whitelist_generator(): # noqa +def whitelist_generator(): # noqa: C901 """Generator which yields lines to add to a vulture whitelist.""" loader.load_components(skip_hooks=True) diff --git a/tests/helpers/stubs.py b/tests/helpers/stubs.py index f9223c3ca..43c90acb7 100644 --- a/tests/helpers/stubs.py +++ b/tests/helpers/stubs.py @@ -674,4 +674,4 @@ class FakeCookieStore: self.cookie_filter = None if has_cookie_filter: self.setCookieFilter = ( - lambda func: setattr(self, 'cookie_filter', func)) # noqa + lambda func: setattr(self, 'cookie_filter', func)) # noqa: B010 diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 6b44196b6..16842798a 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -110,7 +110,7 @@ class TestEarlyInit: expected = '' assert config.instance.dump_userconfig() == expected - @pytest.mark.parametrize('load_autoconfig', [True, False]) # noqa + @pytest.mark.parametrize('load_autoconfig', [True, False]) @pytest.mark.parametrize('config_py', [True, 'error', False]) @pytest.mark.parametrize('invalid_yaml', ['42', 'list', 'unknown', 'wrong-type', False]) diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index 4afe8a78b..abc9c9e99 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -25,7 +25,7 @@ import collections import os.path import subprocess import contextlib -import builtins # noqa https://github.com/JBKahn/flake8-debugger/issues/20 +import builtins import types import importlib import logging From 33c80e386cee4dfa5db5d8eb2a25442fa8fb864c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 17:08:33 +0100 Subject: [PATCH 018/194] scripts: Disallow type comments in misc_checks --- doc/help/configuring.asciidoc | 4 ++-- scripts/dev/misc_checks.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index aa1dfc12e..cd72ae6ce 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -422,8 +422,8 @@ stable across qutebrowser versions): # pylint: disable=C0111 from qutebrowser.config.configfiles import ConfigAPI # noqa: F401 from qutebrowser.config.config import ConfigContainer # noqa: F401 -config = config # type: ConfigAPI # noqa: F821 pylint: disable=E0602,C0103 -c = c # type: ConfigContainer # noqa: F821 pylint: disable=E0602,C0103 +config: ConfigAPI = config # noqa: F821 pylint: disable=E0602,C0103 +c: ConfigContainer = c # noqa: F821 pylint: disable=E0602,C0103 ---- emacs-like config diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index c8c1936e5..5c09a9c9d 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -145,7 +145,11 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: re.compile(r'# type: ignore[^\[]'), ("Don't use a blanket 'type: ignore', use something like " "'type: ignore[error-code]' instead."), - ) + ), + ( + re.compile(r'# type: (?!ignore\[)'), + "Don't use type comments, use type annotations instead.", + ), ] # Files which should be ignored, e.g. because they come from another From 85544945b3a028b8fdb521b6deceb8d80131d87d Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 17:10:51 +0100 Subject: [PATCH 019/194] Rephrase sentence and regenerate docs --- doc/help/settings.asciidoc | 1 + qutebrowser/config/configdata.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 4d24e5dda..452d21157 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -4178,6 +4178,7 @@ characters in the search terms are replaced by safe characters (called expands to `slash%2Fand%26amp`). * `{unquoted}` quotes nothing (for `slash/and&` this placeholder expands to `slash/and&`). +* `{0}` means the same as `{}`, but can be used multiple times. The search engine named `DEFAULT` is used when `url.auto_search` is turned on and something else than a URL was entered to be opened. Other search diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index ba2a2a277..820c34c09 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -2045,7 +2045,7 @@ url.searchengines: expands to `slash%2Fand%26amp`). * `{unquoted}` quotes nothing (for `slash/and&` this placeholder expands to `slash/and&`). - * `{0}` same as `{}` but can be used multiple times. + * `{0}` means the same as `{}`, but can be used multiple times. The search engine named `DEFAULT` is used when `url.auto_search` is turned on and something else than a URL was entered to be opened. Other search From d5b08d1516048266c1f592288d4a0bf77863cd1f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 17:14:24 +0100 Subject: [PATCH 020/194] scripts: Add typing. check to misc_checks.py --- scripts/dev/misc_checks.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 5c09a9c9d..1ce9fdfe4 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -150,6 +150,10 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: re.compile(r'# type: (?!ignore\[)'), "Don't use type comments, use type annotations instead.", ), + ( + re.compile(r': typing\.'), + "Don't use typing.SomeType, do 'from typing import SomeType' instead.", + ), ] # Files which should be ignored, e.g. because they come from another From 46be88f6b46f056abf3b14bcb5e39a4952f0f335 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 17:24:46 +0100 Subject: [PATCH 021/194] scripts: Add more changelog URLs --- scripts/dev/recompile_requirements.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/dev/recompile_requirements.py b/scripts/dev/recompile_requirements.py index 5b79b801d..2d60fa6fd 100644 --- a/scripts/dev/recompile_requirements.py +++ b/scripts/dev/recompile_requirements.py @@ -48,6 +48,7 @@ CHANGELOG_URLS = { 'execnet': 'https://execnet.readthedocs.io/en/latest/changelog.html', 'apipkg': 'https://github.com/pytest-dev/apipkg/blob/master/CHANGELOG', 'pytest-rerunfailures': 'https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst', + 'pytest-repeat': 'https://github.com/pytest-dev/pytest-repeat/blob/master/CHANGES.rst', 'requests': 'https://github.com/psf/requests/blob/master/HISTORY.md', 'requests-file': 'https://github.com/dashea/requests-file/blob/master/CHANGES.rst', 'werkzeug': 'https://github.com/pallets/werkzeug/blob/master/CHANGES.rst', @@ -62,6 +63,7 @@ CHANGELOG_URLS = { 'virtualenv': 'https://virtualenv.pypa.io/en/latest/changelog.html', 'pip': 'https://pip.pypa.io/en/stable/news/', 'packaging': 'https://pypi.org/project/packaging/', + 'build': 'https://github.com/pypa/build/commits/master', 'flake8-docstrings': 'https://pypi.org/project/flake8-docstrings/', 'attrs': 'http://www.attrs.org/en/stable/changelog.html', 'jinja2': 'https://github.com/pallets/jinja/blob/master/CHANGES.rst', From 18750a332a5474fe42bcdefcf14cea10eec41243 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 17:52:26 +0100 Subject: [PATCH 022/194] Fix objreg.first/last_opened_window w/o windows Before 4cd255c7372625b4c3a630844ffd2b42d6ddde58, we correctly raised `NoWindow()` via `_window_by_index(-1)`. After this change, `first_opened_window` and `last_opened_window()` didn't do anything at all, because their for-loop iterated over an empty range, thus causing the `Unreachable` to be hit. This caused #1246 to manifest in a different way than usual. Now, we're back to the old (still buggy) behavior in that case. Closes #5849 --- qutebrowser/utils/objreg.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qutebrowser/utils/objreg.py b/qutebrowser/utils/objreg.py index 90b70be17..c74cb9873 100644 --- a/qutebrowser/utils/objreg.py +++ b/qutebrowser/utils/objreg.py @@ -350,6 +350,8 @@ def _window_by_index(idx: int) -> 'mainwindow.MainWindow': def last_opened_window() -> 'mainwindow.MainWindow': """Get the last opened window object.""" + if not window_registry: + raise NoWindow() for idx in range(-1, -(len(window_registry)+1), -1): window = _window_by_index(idx) if not window.tabbed_browser.is_shutting_down: @@ -359,6 +361,8 @@ def last_opened_window() -> 'mainwindow.MainWindow': def first_opened_window() -> 'mainwindow.MainWindow': """Get the first opened window object.""" + if not window_registry: + raise NoWindow() for idx in range(0, len(window_registry)+1): window = _window_by_index(idx) if not window.tabbed_browser.is_shutting_down: From 3776809c8ec17a8da1ba658a29b2f6de2ca9b906 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 17:59:50 +0100 Subject: [PATCH 023/194] Fix lint --- qutebrowser/misc/checkpyver.py | 2 +- scripts/dev/misc_checks.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/qutebrowser/misc/checkpyver.py b/qutebrowser/misc/checkpyver.py index 98360895a..6f6659a24 100644 --- a/qutebrowser/misc/checkpyver.py +++ b/qutebrowser/misc/checkpyver.py @@ -31,7 +31,7 @@ except ImportError: # pragma: no cover try: # Python2 from Tkinter import Tk # type: ignore[import, no-redef] - import tkMessageBox as messagebox # type: ignore[import, no-redef] # noqa: N813 + import tkMessageBox as messagebox # type: ignore[import, no-redef] # noqa: N813 except ImportError: # Some Python without Tk Tk = None # type: ignore[misc, assignment] diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 1ce9fdfe4..2dd9e210b 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -28,7 +28,6 @@ import argparse import subprocess import tokenize import traceback -import collections import pathlib from typing import List, Iterator, Optional From b294a5444a7b40157cfc2b3292da97d2b29aaf96 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 2 Nov 2020 18:45:58 +0100 Subject: [PATCH 024/194] Improve error handling for #5848 --- qutebrowser/completion/completionwidget.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qutebrowser/completion/completionwidget.py b/qutebrowser/completion/completionwidget.py index 4f51ecd4b..1f5304b61 100644 --- a/qutebrowser/completion/completionwidget.py +++ b/qutebrowser/completion/completionwidget.py @@ -30,7 +30,7 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QItemSelectionModel, QSize from qutebrowser.config import config, stylesheet from qutebrowser.completion import completiondelegate -from qutebrowser.utils import utils, usertypes, debug, log +from qutebrowser.utils import utils, usertypes, debug, log, qtutils from qutebrowser.api import cmdutils if TYPE_CHECKING: from qutebrowser.mainwindow.statusbar import command @@ -223,8 +223,9 @@ class CompletionView(QTreeView): return model.last_item() if upwards else model.first_item() # Find height of each CompletionView element - element_height = self.visualRect(idx).height() - page_length = self.height() // element_height + rect = self.visualRect(idx) + qtutils.ensure_valid(rect) + page_length = self.height() // rect.height() # Skip one pageful, except leave one old line visible offset = -(page_length - 1) if upwards else page_length - 1 From 963df3b271113a5515e10f878496355a6dcaf9af Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 3 Nov 2020 11:37:53 +0100 Subject: [PATCH 025/194] tests: Set about:blank as active item It looks like QtWebKit sometimes tries to actually load the active page when the history is deserialized. This is problematic because of two reasons: - Sometimes the load is quick enough during the tests (?) and we end up with the real "Example Domain" title from example.org rather than "percent" from the deserialized item, thus causing flaky tests. - We don't want to do network requests during tests, yet this does such a request (sometimes). --- tests/unit/browser/webkit/test_tabhistory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/browser/webkit/test_tabhistory.py b/tests/unit/browser/webkit/test_tabhistory.py index 9325cb7d0..48e0c98fc 100644 --- a/tests/unit/browser/webkit/test_tabhistory.py +++ b/tests/unit/browser/webkit/test_tabhistory.py @@ -33,7 +33,8 @@ pytestmark = pytest.mark.qt_log_ignore('QIODevice::read.*: device not open') ITEMS = [ Item(QUrl('https://www.heise.de/'), 'heise'), - Item(QUrl('http://example.com/%E2%80%A6'), 'percent', active=True), + Item(QUrl('about:blank'), 'blank', active=True), + Item(QUrl('http://example.com/%E2%80%A6'), 'percent'), Item(QUrl('http://example.com/?foo=bar'), 'arg', original_url=QUrl('http://original.url.example.com/'), user_data={'foo': 23, 'bar': 42}), From b9a006d2bba954c9b9c8478535677c4324b69755 Mon Sep 17 00:00:00 2001 From: duthades Date: Tue, 3 Nov 2020 16:43:28 +0530 Subject: [PATCH 026/194] pass alpha to _get_color_percentage to interpolate --- qutebrowser/utils/utils.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index 21961101f..21e412154 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -211,27 +211,30 @@ def resource_filename(filename: str) -> str: return pkg_resources.resource_filename(qutebrowser.__name__, filename) -def _get_color_percentage(a_c1: int, a_c2: int, a_c3: - int, b_c1: int, b_c2: int, b_c3: int, - percent: int) -> Tuple[int, int, int]: +def _get_color_percentage(a_c1: int, a_c2: int, a_c3: int, a_alpha: int, + b_c1: int, b_c2: int, b_c3: int, b_alpha: int, + percent: int) -> Tuple[int, int, int, int]: """Get a color which is percent% interpolated between start and end. Args: - a_c1, a_c2, a_c3: Start color components (R, G, B / H, S, V / H, S, L) - b_c1, b_c2, b_c3: End color components (R, G, B / H, S, V / H, S, L) + a_c1, a_c2, a_c3, a_alpha : Start color components (R, G, B, A / H, S, V, A / + H, S, L, A) + b_c1, b_c2, b_c3, b_alpha : End color components (R, G, B, A / H, S, V, A / + H, S, L, A) percent: Percentage to interpolate, 0-100. 0: Start color will be returned. 100: End color will be returned. Return: - A (c1, c2, c3) tuple with the interpolated color components. + A (c1, c2, c3, alpha) tuple with the interpolated color components. """ if not 0 <= percent <= 100: raise ValueError("percent needs to be between 0 and 100!") out_c1 = round(a_c1 + (b_c1 - a_c1) * percent / 100) out_c2 = round(a_c2 + (b_c2 - a_c2) * percent / 100) out_c3 = round(a_c3 + (b_c3 - a_c3) * percent / 100) - return (out_c1, out_c2, out_c3) + out_alpha = round(a_alpha + (b_alpha - a_alpha) * percent / 100) + return (out_c1, out_c2, out_c3, out_alpha) def interpolate_color( @@ -264,21 +267,21 @@ def interpolate_color( out = QColor() if colorspace == QColor.Rgb: - a_c1, a_c2, a_c3, _alpha = start.getRgb() - b_c1, b_c2, b_c3, _alpha = end.getRgb() - components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, + r1, g1, b1, alpha1 = start.getRgb() + r2, g2, b2, alpha2 = end.getRgb() + components = _get_color_percentage(r1, g1, b1, alpha1, r2, g2, b2, alpha2, percent) out.setRgb(*components) elif colorspace == QColor.Hsv: - a_c1, a_c2, a_c3, _alpha = start.getHsv() - b_c1, b_c2, b_c3, _alpha = end.getHsv() - components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, + h1, s1, v1, alpha1 = start.getHsv() + h2, s2, v2, alpha2 = end.getHsv() + components = _get_color_percentage(h1, s1, v1, alpha1, h2, s2, v2, alpha2, percent) out.setHsv(*components) elif colorspace == QColor.Hsl: - a_c1, a_c2, a_c3, _alpha = start.getHsl() - b_c1, b_c2, b_c3, _alpha = end.getHsl() - components = _get_color_percentage(a_c1, a_c2, a_c3, b_c1, b_c2, b_c3, + h1, s1, l1, alpha1 = start.getHsl() + h2, s2, l2, alpha2 = end.getHsl() + components = _get_color_percentage(h1, s1, l1, alpha1, h2, s2, l2, alpha2, percent) out.setHsl(*components) else: From 767f34a353f459a72700b65f185fded6821ac190 Mon Sep 17 00:00:00 2001 From: duthades Date: Tue, 3 Nov 2020 16:44:16 +0530 Subject: [PATCH 027/194] write test cases for interpolate_color with alpha --- tests/unit/utils/test_utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/utils/test_utils.py b/tests/unit/utils/test_utils.py index 8a07e3411..8c38ea587 100644 --- a/tests/unit/utils/test_utils.py +++ b/tests/unit/utils/test_utils.py @@ -242,6 +242,16 @@ class TestInterpolateColor: expected.setHsl(0, 30, 150) assert Color(color) == expected + @pytest.mark.parametrize('colorspace', [QColor.Rgb, QColor.Hsv, + QColor.Hsl]) + def test_interpolation_alpha(self, colorspace): + """Test interpolation of colorspace's alpha.""" + start = Color(0, 0, 0, 30) + stop = Color(0, 0, 0, 100) + color = utils.interpolate_color(start, stop, 50, colorspace) + expected = Color(0, 0, 0, 65) + assert Color(color) == expected + @pytest.mark.parametrize('percentage, expected', [ (0, (0, 0, 0)), (99, (0, 0, 0)), From d6e1d7847707c4605ac3373ee8e70eeea1333079 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 3 Nov 2020 18:58:02 +0100 Subject: [PATCH 028/194] tests: Use three-argument form of monkeypatch --- scripts/dev/misc_checks.py | 5 +++++ tests/conftest.py | 8 ++++---- tests/unit/browser/test_qutescheme.py | 3 ++- tests/unit/browser/webkit/test_mhtml.py | 3 ++- tests/unit/config/test_configinit.py | 3 ++- tests/unit/misc/test_ipc.py | 2 +- tests/unit/utils/test_standarddir.py | 2 +- tests/unit/utils/test_version.py | 17 +++++++---------- 8 files changed, 24 insertions(+), 19 deletions(-) diff --git a/scripts/dev/misc_checks.py b/scripts/dev/misc_checks.py index 2dd9e210b..e168bfff2 100644 --- a/scripts/dev/misc_checks.py +++ b/scripts/dev/misc_checks.py @@ -153,6 +153,11 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]: re.compile(r': typing\.'), "Don't use typing.SomeType, do 'from typing import SomeType' instead.", ), + ( + re.compile(r"""monkeypatch\.setattr\(['"]"""), + "Don't use monkeypatch.setattr('obj.attr', value), use " + "setattr(obj, 'attr', value) instead.", + ), ] # Files which should be ignored, e.g. because they come from another diff --git a/tests/conftest.py b/tests/conftest.py index ef169be4f..6c0c716c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -281,10 +281,10 @@ def apply_fake_os(monkeypatch, request): else: raise ValueError("Invalid fake_os {}".format(name)) - monkeypatch.setattr('qutebrowser.utils.utils.is_mac', mac) - monkeypatch.setattr('qutebrowser.utils.utils.is_linux', linux) - monkeypatch.setattr('qutebrowser.utils.utils.is_windows', windows) - monkeypatch.setattr('qutebrowser.utils.utils.is_posix', posix) + monkeypatch.setattr(utils, 'is_mac', mac) + monkeypatch.setattr(utils, 'is_linux', linux) + monkeypatch.setattr(utils, 'is_windows', windows) + monkeypatch.setattr(utils, 'is_posix', posix) @pytest.fixture(scope='session', autouse=True) diff --git a/tests/unit/browser/test_qutescheme.py b/tests/unit/browser/test_qutescheme.py index d4a87c4f9..0d813df02 100644 --- a/tests/unit/browser/test_qutescheme.py +++ b/tests/unit/browser/test_qutescheme.py @@ -28,6 +28,7 @@ from PyQt5.QtCore import QUrl, QUrlQuery import pytest from qutebrowser.browser import qutescheme, pdfjs, downloads +from qutebrowser.utils import utils class TestJavascriptHandler: @@ -51,7 +52,7 @@ class TestJavascriptHandler: return content raise OSError("File not found {}!".format(path)) - monkeypatch.setattr('qutebrowser.utils.utils.read_file', _read_file) + monkeypatch.setattr(utils, 'read_file', _read_file) @pytest.mark.parametrize("filename, content", js_files) def test_qutejavascript(self, filename, content): diff --git a/tests/unit/browser/webkit/test_mhtml.py b/tests/unit/browser/webkit/test_mhtml.py index 58e5602b3..1a2b6bf31 100644 --- a/tests/unit/browser/webkit/test_mhtml.py +++ b/tests/unit/browser/webkit/test_mhtml.py @@ -21,6 +21,7 @@ import io import textwrap import re +import uuid import pytest @@ -35,7 +36,7 @@ except ImportError: @pytest.fixture(autouse=True) def patch_uuid(monkeypatch): - monkeypatch.setattr("uuid.uuid4", lambda: "UUID") + monkeypatch.setattr(uuid, "uuid4", lambda: "UUID") class Checker: diff --git a/tests/unit/config/test_configinit.py b/tests/unit/config/test_configinit.py index 16842798a..23cc890e4 100644 --- a/tests/unit/config/test_configinit.py +++ b/tests/unit/config/test_configinit.py @@ -18,6 +18,7 @@ """Tests for qutebrowser.config.configinit.""" +import builtins import logging import unittest.mock @@ -390,6 +391,6 @@ def test_get_backend(monkeypatch, args, config_stub, args.backend = arg config_stub.val.backend = confval - monkeypatch.setattr('builtins.__import__', fake_import) + monkeypatch.setattr(builtins, '__import__', fake_import) assert configinit.get_backend(args) == used diff --git a/tests/unit/misc/test_ipc.py b/tests/unit/misc/test_ipc.py index 28c570586..2c9f7ea7f 100644 --- a/tests/unit/misc/test_ipc.py +++ b/tests/unit/misc/test_ipc.py @@ -767,7 +767,7 @@ def test_long_username(monkeypatch): """See https://github.com/qutebrowser/qutebrowser/issues/888.""" username = 'alexandercogneau' basedir = '/this_is_a_long_basedir' - monkeypatch.setattr('getpass.getuser', lambda: username) + monkeypatch.setattr(getpass, 'getuser', lambda: username) name = ipc._get_socketname(basedir=basedir) server = ipc.IPCServer(name) expected_md5 = md5('{}-{}'.format(username, basedir)) diff --git a/tests/unit/utils/test_standarddir.py b/tests/unit/utils/test_standarddir.py index ea65b7cc4..572d38f02 100644 --- a/tests/unit/utils/test_standarddir.py +++ b/tests/unit/utils/test_standarddir.py @@ -392,7 +392,7 @@ class TestSystemData: fake_args): """Test that system-wide path is not used on non-Linux OS.""" fake_args.basedir = str(tmpdir) - monkeypatch.setattr('sys.platform', "potato") + monkeypatch.setattr(sys, 'platform', 'potato') standarddir._init_data(args=fake_args) assert standarddir.data(system=True) == standarddir.data() diff --git a/tests/unit/utils/test_version.py b/tests/unit/utils/test_version.py index abc9c9e99..3393ae376 100644 --- a/tests/unit/utils/test_version.py +++ b/tests/unit/utils/test_version.py @@ -615,7 +615,7 @@ class ImportFake: def import_fake(monkeypatch): """Fixture to patch imports using ImportFake.""" fake = ImportFake() - monkeypatch.setattr('builtins.__import__', fake.fake_import) + monkeypatch.setattr(builtins, '__import__', fake.fake_import) monkeypatch.setattr(version.importlib, 'import_module', fake.fake_importlib_import) return fake @@ -1023,7 +1023,7 @@ def test_version_info(params, stubs, monkeypatch, config_stub): substitutions['ssl'] = 'SSL VERSION' if params.ssl_support else 'no' for name, val in patches.items(): - monkeypatch.setattr('qutebrowser.utils.version.' + name, val) + monkeypatch.setattr(f'qutebrowser.utils.version.{name}', val) if params.frozen: monkeypatch.setattr(sys, 'frozen', True, raising=False) @@ -1138,9 +1138,8 @@ def pbclient(stubs): def test_pastebin_version(pbclient, message_mock, monkeypatch, qtbot): """Test version.pastebin_version() sets the url.""" - monkeypatch.setattr('qutebrowser.utils.version.version_info', - lambda: "dummy") - monkeypatch.setattr('qutebrowser.utils.utils.log_clipboard', True) + monkeypatch.setattr(version, 'version_info', lambda: 'dummy') + monkeypatch.setattr(utils, 'log_clipboard', True) version.pastebin_version(pbclient) pbclient.success.emit("https://www.example.com/\n") @@ -1153,8 +1152,7 @@ def test_pastebin_version(pbclient, message_mock, monkeypatch, qtbot): def test_pastebin_version_twice(pbclient, monkeypatch): """Test whether calling pastebin_version twice sends no data.""" - monkeypatch.setattr('qutebrowser.utils.version.version_info', - lambda: "dummy") + monkeypatch.setattr(version, 'version_info', lambda: 'dummy') version.pastebin_version(pbclient) pbclient.success.emit("https://www.example.com/\n") @@ -1171,8 +1169,7 @@ def test_pastebin_version_twice(pbclient, monkeypatch): def test_pastebin_version_error(pbclient, caplog, message_mock, monkeypatch): """Test version.pastebin_version() with errors.""" - monkeypatch.setattr('qutebrowser.utils.version.version_info', - lambda: "dummy") + monkeypatch.setattr(version, 'version_info', lambda: 'dummy') version.pastebin_url = None with caplog.at_level(logging.ERROR): @@ -1192,7 +1189,7 @@ def test_uptime(monkeypatch, qapp): class FakeDateTime(datetime.datetime): now = lambda x=datetime.datetime(1, 1, 1, 1, 1, 1, 2): x - monkeypatch.setattr('datetime.datetime', FakeDateTime) + monkeypatch.setattr(datetime, 'datetime', FakeDateTime) uptime_delta = version._uptime() assert uptime_delta == datetime.timedelta(0) From 128c87e44c70fde18d56863c587ff118d5dce71e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 3 Nov 2020 22:38:41 +0100 Subject: [PATCH 029/194] tests: Handle dict instability in test_from_str_hypothesis --- tests/unit/config/test_configtypes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index b637ec13c..dfbbb8d53 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -249,10 +249,11 @@ class TestAll: configtypes.PercOrInt, # ditto ]: return - elif (isinstance(klass, functools.partial) and - klass.func in [configtypes.ListOrValue, configtypes.List]): + elif (isinstance(klass, functools.partial) and klass.func in [ + configtypes.ListOrValue, configtypes.List, configtypes.Dict]): # ListOrValue: "- /" -> "/" # List: "- /" -> ["/"] + # Dict: '{":": "A"}' -> ':: A' return assert converted == s From 1c73e3b985cbd4a603ff2c89220d5918636891e8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 3 Nov 2020 22:41:56 +0100 Subject: [PATCH 030/194] tests: Cleanup import from other test file --- tests/end2end/features/test_search_bdd.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/end2end/features/test_search_bdd.py b/tests/end2end/features/test_search_bdd.py index ba3254830..d703bc47e 100644 --- a/tests/end2end/features/test_search_bdd.py +++ b/tests/end2end/features/test_search_bdd.py @@ -19,11 +19,14 @@ import json +import pytest import pytest_bdd as bdd -# pylint: disable=unused-import -from end2end.features.test_yankpaste_bdd import init_fake_clipboard -# pylint: enable=unused-import + +@pytest.fixture(autouse=True) +def init_fake_clipboard(quteproc): + """Make sure the fake clipboard will be used.""" + quteproc.send_cmd(':debug-set-fake-clipboard') @bdd.then(bdd.parsers.parse('"{text}" should be found')) From ebed435a0c0202e24c31cf0de7c76f4c05a5b607 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 4 Nov 2020 10:44:35 +0100 Subject: [PATCH 031/194] Revert "Work around pytest-bdd issues" This reverts commit d299e48960a8ae608bded9fde504ed3a5994240a. # Conflicts: # pytest.ini --- tests/unit/browser/test_hints.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/browser/test_hints.py b/tests/unit/browser/test_hints.py index 382c322fb..3b09ed0fd 100644 --- a/tests/unit/browser/test_hints.py +++ b/tests/unit/browser/test_hints.py @@ -30,10 +30,8 @@ import qutebrowser.browser.hints @pytest.fixture(autouse=True) -def setup(benchmark, win_registry, mode_manager): - yield - # WORKAROUND for https://github.com/ionelmc/pytest-benchmark/issues/125 - benchmark._mode = 'WORKAROUND' # pylint: disable=protected-access +def setup(win_registry, mode_manager): + pass @pytest.fixture From 3bfebc278f010564bb528c3fa078a0fea5fd04ef Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 4 Nov 2020 11:30:47 +0100 Subject: [PATCH 032/194] Remove old pytest readline workaround --- qutebrowser/misc/crashsignal.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qutebrowser/misc/crashsignal.py b/qutebrowser/misc/crashsignal.py index 65e584bc9..d07d8e49c 100644 --- a/qutebrowser/misc/crashsignal.py +++ b/qutebrowser/misc/crashsignal.py @@ -30,11 +30,6 @@ import functools import threading import faulthandler from typing import TYPE_CHECKING, Optional, MutableMapping, cast -try: - # WORKAROUND for segfaults when using pdb in pytest for some reason... - import readline # pylint: disable=unused-import -except ImportError: - pass import attr from PyQt5.QtCore import (pyqtSlot, qInstallMessageHandler, QObject, From cc2d73b0616c1b3d1fe5b33b104eccfb397c3dca Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 3 Nov 2020 11:52:52 +0100 Subject: [PATCH 033/194] old qt: Drop CI --- .github/workflows/ci.yml | 20 ++++--------------- .travis.yml | 16 --------------- misc/requirements/requirements-pyqt-5.10.txt | 4 ---- .../requirements-pyqt-5.10.txt-raw | 4 ---- misc/requirements/requirements-pyqt-5.11.txt | 4 ---- .../requirements-pyqt-5.11.txt-raw | 4 ---- misc/requirements/requirements-pyqt-5.7.txt | 4 ---- .../requirements-pyqt-5.7.txt-raw | 4 ---- misc/requirements/requirements-pyqt-5.9.txt | 4 ---- .../requirements-pyqt-5.9.txt-raw | 4 ---- tox.ini | 8 ++------ 11 files changed, 6 insertions(+), 70 deletions(-) delete mode 100644 .travis.yml delete mode 100644 misc/requirements/requirements-pyqt-5.10.txt delete mode 100644 misc/requirements/requirements-pyqt-5.10.txt-raw delete mode 100644 misc/requirements/requirements-pyqt-5.11.txt delete mode 100644 misc/requirements/requirements-pyqt-5.11.txt-raw delete mode 100644 misc/requirements/requirements-pyqt-5.7.txt delete mode 100644 misc/requirements/requirements-pyqt-5.7.txt-raw delete mode 100644 misc/requirements/requirements-pyqt-5.9.txt delete mode 100644 misc/requirements/requirements-pyqt-5.9.txt-raw diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 182f935be..58ebc1f3f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,26 +100,14 @@ jobs: fail-fast: false matrix: include: - ### PyQt 5.9 (Python 3.6) - - testenv: py36-pyqt59 - os: ubuntu-18.04 - python: 3.6 - ### PyQt 5.10 (Python 3.6) - - testenv: py36-pyqt510 + ### PyQt 5.12 (Python 3.6) + - testenv: py36-pyqt512 os: ubuntu-20.04 python: 3.6 - ### PyQt 5.11 (Python 3.7) - - testenv: py37-pyqt511 + ### PyQt 5.13 (Python 3.7) + - testenv: py37-pyqt513 os: ubuntu-20.04 python: 3.7 - ### PyQt 5.12 (Python 3.8) - - testenv: py38-pyqt512 - os: ubuntu-20.04 - python: 3.8 - ### PyQt 5.13 (Python 3.8) - - testenv: py38-pyqt513 - os: ubuntu-20.04 - python: 3.8 ### PyQt 5.14 (Python 3.8) - testenv: py38-pyqt514 os: ubuntu-20.04 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b75081477..000000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -dist: xenial -language: python -python: 3.6 -os: linux -env: TESTENV=py36-pyqt57 - -install: - - python -m pip install -U pip - - python -m pip install -U -r misc/requirements/requirements-tox.txt - - ulimit -c unlimited - -script: - - tox -e "$TESTENV" - -after_failure: - - bash scripts/dev/ci/backtrace.sh diff --git a/misc/requirements/requirements-pyqt-5.10.txt b/misc/requirements/requirements-pyqt-5.10.txt deleted file mode 100644 index 69c3ccbd0..000000000 --- a/misc/requirements/requirements-pyqt-5.10.txt +++ /dev/null @@ -1,4 +0,0 @@ -# This file is automatically generated by scripts/dev/recompile_requirements.py - -PyQt5==5.10.1 # rq.filter: < 5.11 -sip==4.19.8 # rq.filter: < 5 diff --git a/misc/requirements/requirements-pyqt-5.10.txt-raw b/misc/requirements/requirements-pyqt-5.10.txt-raw deleted file mode 100644 index 4fbea8575..000000000 --- a/misc/requirements/requirements-pyqt-5.10.txt-raw +++ /dev/null @@ -1,4 +0,0 @@ -#@ filter: PyQt5 < 5.11 -PyQt5 >= 5.10, < 5.11 -#@ filter: sip < 5 -sip < 5 diff --git a/misc/requirements/requirements-pyqt-5.11.txt b/misc/requirements/requirements-pyqt-5.11.txt deleted file mode 100644 index bfee87c0f..000000000 --- a/misc/requirements/requirements-pyqt-5.11.txt +++ /dev/null @@ -1,4 +0,0 @@ -# This file is automatically generated by scripts/dev/recompile_requirements.py - -PyQt5==5.11.3 # rq.filter: < 5.12 -PyQt5-sip==4.19.19 # rq.filter: < 4.20 diff --git a/misc/requirements/requirements-pyqt-5.11.txt-raw b/misc/requirements/requirements-pyqt-5.11.txt-raw deleted file mode 100644 index bdbe43f19..000000000 --- a/misc/requirements/requirements-pyqt-5.11.txt-raw +++ /dev/null @@ -1,4 +0,0 @@ -#@ filter: PyQt5 < 5.12 -PyQt5 >= 5.11, < 5.12 - -#@ filter: PyQt5-sip < 4.20 diff --git a/misc/requirements/requirements-pyqt-5.7.txt b/misc/requirements/requirements-pyqt-5.7.txt deleted file mode 100644 index 703c95a92..000000000 --- a/misc/requirements/requirements-pyqt-5.7.txt +++ /dev/null @@ -1,4 +0,0 @@ -# This file is automatically generated by scripts/dev/recompile_requirements.py - -PyQt5==5.7.1 # rq.filter: < 5.8 -sip==4.19.8 # rq.filter: < 5 diff --git a/misc/requirements/requirements-pyqt-5.7.txt-raw b/misc/requirements/requirements-pyqt-5.7.txt-raw deleted file mode 100644 index 745deb4b9..000000000 --- a/misc/requirements/requirements-pyqt-5.7.txt-raw +++ /dev/null @@ -1,4 +0,0 @@ -#@ filter: PyQt5 < 5.8 -#@ filter: sip < 5 -PyQt5 >= 5.7, < 5.8 -sip < 5 diff --git a/misc/requirements/requirements-pyqt-5.9.txt b/misc/requirements/requirements-pyqt-5.9.txt deleted file mode 100644 index 8f3258721..000000000 --- a/misc/requirements/requirements-pyqt-5.9.txt +++ /dev/null @@ -1,4 +0,0 @@ -# This file is automatically generated by scripts/dev/recompile_requirements.py - -PyQt5==5.9.2 # rq.filter: < 5.10 -sip==4.19.8 # rq.filter: < 5 diff --git a/misc/requirements/requirements-pyqt-5.9.txt-raw b/misc/requirements/requirements-pyqt-5.9.txt-raw deleted file mode 100644 index 45d4e0c10..000000000 --- a/misc/requirements/requirements-pyqt-5.9.txt-raw +++ /dev/null @@ -1,4 +0,0 @@ -#@ filter: PyQt5 < 5.10 -PyQt5 >= 5.9, < 5.10 -#@ filter: sip < 5 -sip < 5 diff --git a/tox.ini b/tox.ini index a00122333..8d9eedeac 100644 --- a/tox.ini +++ b/tox.ini @@ -12,8 +12,8 @@ minversion = 3.15 [testenv] setenv = PYTEST_QT_API=pyqt5 - pyqt{,57,59,510,511,512,513,514,515}: LINK_PYQT_SKIP=true - pyqt{,57,59,510,511,512,513,514,515}: QUTE_BDD_WEBENGINE=true + pyqt{,512,513,514,515}: LINK_PYQT_SKIP=true + pyqt{,512,513,514,515}: QUTE_BDD_WEBENGINE=true cov: PYTEST_ADDOPTS=--cov --cov-report xml --cov-report=html --cov-report= passenv = PYTHON DISPLAY XAUTHORITY HOME USERNAME USER CI XDG_* QUTE_* DOCKER QT_QUICK_BACKEND PY_COLORS basepython = @@ -27,10 +27,6 @@ deps = -r{toxinidir}/requirements.txt -r{toxinidir}/misc/requirements/requirements-tests.txt pyqt: -r{toxinidir}/misc/requirements/requirements-pyqt.txt - pyqt57: -r{toxinidir}/misc/requirements/requirements-pyqt-5.7.txt - pyqt59: -r{toxinidir}/misc/requirements/requirements-pyqt-5.9.txt - pyqt510: -r{toxinidir}/misc/requirements/requirements-pyqt-5.10.txt - pyqt511: -r{toxinidir}/misc/requirements/requirements-pyqt-5.11.txt pyqt512: -r{toxinidir}/misc/requirements/requirements-pyqt-5.12.txt pyqt513: -r{toxinidir}/misc/requirements/requirements-pyqt-5.13.txt pyqt514: -r{toxinidir}/misc/requirements/requirements-pyqt-5.14.txt From c6d6ea532f5d81d5c0c687fda0307f4ffb621470 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 3 Nov 2020 11:58:52 +0100 Subject: [PATCH 034/194] old qt: Adjust configdata.yml and settings docs --- doc/help/settings.asciidoc | 64 ++++----------- qutebrowser/config/configdata.py | 11 +-- qutebrowser/config/configdata.yml | 82 +++++-------------- tests/unit/browser/webengine/test_darkmode.py | 8 +- tests/unit/config/test_configdata.py | 2 +- 5 files changed, 46 insertions(+), 121 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index 452d21157..ddd5df44c 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -1584,9 +1584,7 @@ Valid values: Default: +pass:[lightness-cielab]+ -On QtWebEngine, this setting requires Qt 5.10 or newer. - -On QtWebKit, this setting is unavailable. +This setting is only available with the QtWebEngine backend. [[colors.webpage.darkmode.contrast]] === colors.webpage.darkmode.contrast @@ -1599,9 +1597,7 @@ Type: <> Default: +pass:[0.0]+ -On QtWebEngine, this setting requires Qt 5.10 or newer. - -On QtWebKit, this setting is unavailable. +This setting is only available with the QtWebEngine backend. [[colors.webpage.darkmode.enabled]] === colors.webpage.darkmode.enabled @@ -1627,9 +1623,7 @@ Type: <> Default: +pass:[false]+ -On QtWebEngine, this setting requires Qt 5.10 or newer. - -On QtWebKit, this setting is unavailable. +This setting is only available with the QtWebEngine backend. [[colors.webpage.darkmode.grayscale.all]] === colors.webpage.darkmode.grayscale.all @@ -1642,9 +1636,7 @@ Type: <> Default: +pass:[false]+ -On QtWebEngine, this setting requires Qt 5.10 or newer. - -On QtWebKit, this setting is unavailable. +This setting is only available with the QtWebEngine backend. [[colors.webpage.darkmode.grayscale.images]] === colors.webpage.darkmode.grayscale.images @@ -1664,7 +1656,7 @@ On QtWebKit, this setting is unavailable. [[colors.webpage.darkmode.policy.images]] === colors.webpage.darkmode.policy.images Which images to apply dark mode to. -With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt]. With QtWebEngine 5.10, this is not available at all. In those cases, the 'smart' setting is ignored and treated like 'never'. +With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashes due to a https://codereview.qt-project.org/c/qt/qtwebengine-chromium/+/304211[bug in Qt]. This setting requires a restart. @@ -1674,13 +1666,11 @@ Valid values: * +always+: Apply dark mode filter to all images. * +never+: Never apply dark mode filter to any images. - * +smart+: Apply dark mode based on image content. Not available with Qt 5.10 / 5.15.0. + * +smart+: Apply dark mode based on image content. Not available with Qt 5.15.0. Default: +pass:[smart]+ -On QtWebEngine, this setting requires Qt 5.10 or newer. - -On QtWebKit, this setting is unavailable. +This setting is only available with the QtWebEngine backend. [[colors.webpage.darkmode.policy.page]] === colors.webpage.darkmode.policy.page @@ -1903,7 +1893,6 @@ Default: [[content.autoplay]] === content.autoplay Automatically start playing `