From 208d3db475cf5e5fd50840dd80bfef0fc85e98f5 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Dec 2018 12:14:33 +0100 Subject: [PATCH] Add types for most of qutebrowser.config --- mypy.ini | 36 ++++++ qutebrowser/config/config.py | 184 +++++++++++++++++---------- qutebrowser/config/configcommands.py | 97 ++++++++------ qutebrowser/config/configdata.py | 59 ++++++--- qutebrowser/config/configdiff.py | 7 +- qutebrowser/config/configexc.py | 35 +++-- qutebrowser/config/configfiles.py | 114 ++++++++++------- qutebrowser/config/configinit.py | 20 +-- qutebrowser/config/configutils.py | 49 ++++--- 9 files changed, 385 insertions(+), 216 deletions(-) diff --git a/mypy.ini b/mypy.ini index 288cc6515..be5424327 100644 --- a/mypy.ini +++ b/mypy.ini @@ -61,3 +61,39 @@ disallow_incomplete_defs = True [mypy-qutebrowser.commands.cmdutils] disallow_untyped_defs = True disallow_incomplete_defs = True + +[mypy-qutebrowser.config.config] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configcache] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configcommands] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configdata] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configdiff] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configexc] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configfiles] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configinit] +disallow_untyped_defs = True +disallow_incomplete_defs = True + +[mypy-qutebrowser.config.configutils] +disallow_untyped_defs = True +disallow_incomplete_defs = True diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 1d7e34345..44bf3ca77 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -23,18 +23,21 @@ import copy import contextlib import functools import typing +from typing import Any -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl from qutebrowser.config import configdata, configexc, configutils -from qutebrowser.utils import utils, log, jinja +from qutebrowser.utils import utils, log, jinja, urlmatch from qutebrowser.misc import objects from qutebrowser.keyinput import keyutils MYPY = False -if MYPY: - # pylint: disable=unused-import - from qutebrowser.config import configcache # pragma: no cover +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from typing import Tuple, MutableMapping + 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) @@ -61,7 +64,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name _function: Whether a function rather than a method is decorated. """ - def __init__(self, option, function=False): + def __init__(self, option: str, function: bool = False) -> None: """Save decorator arguments. Gets called on parse-time with the decorator arguments. @@ -74,7 +77,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name self._function = function change_filters.append(self) - def validate(self): + def validate(self) -> None: """Make sure the configured option or prefix exists. We can't do this in __init__ as configdata isn't ready yet. @@ -83,7 +86,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): + def _check_match(self, option: typing.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 +99,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name else: return False - def __call__(self, func): + def __call__(self, func: typing.Callable) -> typing.Callable: """Filter calls to the decorated function. Gets called when a function should be decorated. @@ -114,20 +117,21 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name """ if self._function: @functools.wraps(func) - def wrapper(option=None): + def func_wrapper(option: str = None) -> typing.Any: """Call the underlying function.""" if self._check_match(option): return func() return None + return func_wrapper else: @functools.wraps(func) - def wrapper(wrapper_self, option=None): + def meth_wrapper(wrapper_self: typing.Any, + option: str = None) -> typing.Any: """Call the underlying function.""" if self._check_match(option): return func(wrapper_self) return None - - return wrapper + return meth_wrapper class KeyConfig: @@ -140,17 +144,22 @@ class KeyConfig: _config: The Config object to be used. """ - def __init__(self, config): + _ReverseBindings = typing.Dict[str, typing.MutableSequence[str]] + + def __init__(self, config: 'Config') -> None: self._config = config - def _validate(self, key, mode): + def _validate(self, key: keyutils.KeySequence, mode: str) -> None: """Validate the given key and mode.""" # Catch old usage of this code assert isinstance(key, keyutils.KeySequence), key if mode not in configdata.DATA['bindings.default'].default: raise configexc.KeybindingError("Invalid mode {}!".format(mode)) - def get_bindings_for(self, mode): + def get_bindings_for( + self, + mode: str + ) -> typing.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(): @@ -160,9 +169,9 @@ class KeyConfig: bindings[key] = binding return bindings - def get_reverse_bindings_for(self, mode): + 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 = {} + cmd_to_keys = {} # type: KeyConfig._ReverseBindings bindings = self.get_bindings_for(mode) for seq, full_cmd in sorted(bindings.items()): for cmd in full_cmd.split(';;'): @@ -175,7 +184,10 @@ class KeyConfig: cmd_to_keys[cmd].insert(0, str(seq)) return cmd_to_keys - def get_command(self, key, mode, default=False): + def get_command(self, + key: keyutils.KeySequence, + mode: str, + default: bool = False) -> str: """Get the command for a given key (or None).""" self._validate(key, mode) if default: @@ -184,7 +196,11 @@ class KeyConfig: bindings = self.get_bindings_for(mode) return bindings.get(key, None) - def bind(self, key, command, *, mode, save_yaml=False): + def bind(self, + key: keyutils.KeySequence, + command: str, *, + mode: str, + save_yaml: bool = False) -> None: """Add a new binding from key to command.""" if command is not None and not command.strip(): raise configexc.KeybindingError( @@ -192,8 +208,8 @@ class KeyConfig: 'mode'.format(key, mode)) self._validate(key, mode) - log.keyboard.vdebug("Adding binding {} -> {} in mode {}.".format( - key, command, mode)) + log.keyboard.vdebug( # type: ignore + "Adding binding {} -> {} in mode {}.".format(key, command, mode)) bindings = self._config.get_mutable_obj('bindings.commands') if mode not in bindings: @@ -201,7 +217,10 @@ class KeyConfig: bindings[mode][str(key)] = command self._config.update_mutables(save_yaml=save_yaml) - def bind_default(self, key, *, mode='normal', save_yaml=False): + def bind_default(self, + key: keyutils.KeySequence, *, + mode: str = 'normal', + save_yaml: bool = False) -> None: """Restore a default keybinding.""" self._validate(key, mode) @@ -213,7 +232,10 @@ class KeyConfig: "Can't find binding '{}' in {} mode".format(key, mode)) self._config.update_mutables(save_yaml=save_yaml) - def unbind(self, key, *, mode='normal', save_yaml=False): + def unbind(self, + key: keyutils.KeySequence, *, + mode: str = 'normal', + save_yaml: bool = False) -> None: """Unbind the given key in the given mode.""" self._validate(key, mode) @@ -254,24 +276,27 @@ class Config(QObject): MUTABLE_TYPES = (dict, list) changed = pyqtSignal(str) - def __init__(self, yaml_config, parent=None): + def __init__(self, + yaml_config: 'configfiles.YamlConfig', + parent: QObject = None) -> None: super().__init__(parent) self.changed.connect(_render_stylesheet.cache_clear) - self._mutables = {} + self._mutables = {} # type: MutableMapping[str, Tuple[Any, Any]] self._yaml = yaml_config self._init_values() - def _init_values(self): + def _init_values(self) -> None: """Populate the self._values dict.""" - self._values = {} + self._values = {} # type: typing.Mapping for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def __iter__(self): + def __iter__(self) -> typing.Iterator[configutils.Values]: """Iterate over configutils.Values items.""" yield from self._values.values() - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -279,7 +304,10 @@ class Config(QObject): """ self._yaml.init_save_manager(save_manager) - def _set_value(self, opt, value, pattern=None): + def _set_value(self, + opt: 'configdata.Option', + value: Any, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given option to the given value.""" if not isinstance(objects.backend, objects.NoBackend): if objects.backend not in opt.backends: @@ -294,12 +322,12 @@ class Config(QObject): log.config.debug("Config option changed: {} = {}".format( opt.name, value)) - def _check_yaml(self, opt, save_yaml): + def _check_yaml(self, opt: 'configdata.Option', save_yaml: bool) -> None: """Make sure the given option may be set in autoconfig.yml.""" if save_yaml and opt.no_autoconfig: raise configexc.NoAutoconfigError(opt.name) - def read_yaml(self): + def read_yaml(self) -> None: """Read the YAML settings from self._yaml.""" self._yaml.load() for values in self._yaml: @@ -307,7 +335,7 @@ class Config(QObject): self._set_value(values.opt, scoped.value, pattern=scoped.pattern) - def get_opt(self, name): + def get_opt(self, name: str) -> 'configdata.Option': """Get a configdata.Option object for the given setting.""" try: return configdata.DATA[name] @@ -318,7 +346,10 @@ class Config(QObject): name, deleted=deleted, renamed=renamed) raise exception from None - def get(self, name, url=None, *, fallback=True): + def get(self, + name: str, + url: QUrl = None, *, + fallback: bool = True) -> Any: """Get the given setting converted for Python code. Args: @@ -328,7 +359,7 @@ class Config(QObject): obj = self.get_obj(name, url=url, fallback=fallback) return opt.typ.to_py(obj) - def _maybe_copy(self, value): + def _maybe_copy(self, value: Any) -> Any: """Copy the value if it could potentially be mutated.""" if isinstance(value, self.MUTABLE_TYPES): # For mutable objects, create a copy so we don't accidentally @@ -339,7 +370,10 @@ class Config(QObject): assert value.__hash__ is not None, value return value - def get_obj(self, name, *, url=None, fallback=True): + def get_obj(self, + name: str, *, + url: QUrl = None, + fallback: bool = True) -> Any: """Get the given setting as object (for YAML/config.py). Note that the returned values are not watched for mutation. @@ -349,7 +383,10 @@ class Config(QObject): value = self._values[name].get_for_url(url, fallback=fallback) return self._maybe_copy(value) - def get_obj_for_pattern(self, name, *, pattern): + def get_obj_for_pattern( + self, name: str, *, + pattern: typing.Optional[urlmatch.UrlPattern] + ) -> Any: """Get the given setting as object (for YAML/config.py). This gets the overridden value for a given pattern, or @@ -359,7 +396,8 @@ class Config(QObject): value = self._values[name].get_for_pattern(pattern, fallback=False) return self._maybe_copy(value) - def get_mutable_obj(self, name, *, pattern=None): + def get_mutable_obj(self, name: str, *, + pattern: urlmatch.UrlPattern = None) -> Any: """Get an object which can be mutated, e.g. in a config.py. If a pattern is given, return the value for that pattern. @@ -384,7 +422,8 @@ class Config(QObject): return copy_value - def get_str(self, name, *, pattern=None): + def get_str(self, name: str, *, + pattern: urlmatch.UrlPattern = None) -> str: """Get the given setting as string. If a pattern is given, get the setting for the given pattern or @@ -395,7 +434,10 @@ class Config(QObject): value = values.get_for_pattern(pattern) return opt.typ.to_str(value) - def set_obj(self, name, value, *, pattern=None, save_yaml=False): + def set_obj(self, name: str, + value: Any, *, + pattern: urlmatch.UrlPattern = None, + save_yaml: bool = False) -> None: """Set the given setting from a YAML/config.py object. If save_yaml=True is given, store the new value to YAML. @@ -406,7 +448,10 @@ class Config(QObject): if save_yaml: self._yaml.set_obj(name, value, pattern=pattern) - def set_str(self, name, value, *, pattern=None, save_yaml=False): + def set_str(self, name: str, + value: str, *, + pattern: urlmatch.UrlPattern = None, + save_yaml: bool = False) -> None: """Set the given setting from a string. If save_yaml=True is given, store the new value to YAML. @@ -421,7 +466,9 @@ class Config(QObject): if save_yaml: self._yaml.set_obj(name, converted, pattern=pattern) - def unset(self, name, *, save_yaml=False, pattern=None): + def unset(self, name: str, *, + save_yaml: bool = False, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given setting back to its default.""" opt = self.get_opt(name) self._check_yaml(opt, save_yaml) @@ -432,7 +479,7 @@ class Config(QObject): if save_yaml: self._yaml.unset(name, pattern=pattern) - def clear(self, *, save_yaml=False): + def clear(self, *, save_yaml: bool = False) -> None: """Clear all settings in the config. If save_yaml=True is given, also remove all customization from the YAML @@ -446,7 +493,7 @@ class Config(QObject): if save_yaml: self._yaml.clear() - def update_mutables(self, *, save_yaml=False): + def update_mutables(self, *, save_yaml: bool = False) -> None: """Update mutable settings if they changed. Every time someone calls get_obj() on a mutable object, we save a @@ -461,7 +508,7 @@ class Config(QObject): self.set_obj(name, new_value, save_yaml=save_yaml) self._mutables = {} - def dump_userconfig(self): + def dump_userconfig(self) -> str: """Get the part of the config which was changed by the user. Return: @@ -490,7 +537,10 @@ class ConfigContainer: _pattern: The URL pattern to be used. """ - def __init__(self, config, configapi=None, prefix='', pattern=None): + def __init__(self, config: Config, + configapi: 'configfiles.ConfigAPI' = None, + prefix: str = '', + pattern: urlmatch.UrlPattern = None) -> None: self._config = config self._prefix = prefix self._configapi = configapi @@ -498,13 +548,13 @@ class ConfigContainer: if configapi is None and pattern is not None: raise TypeError("Can't use pattern without configapi!") - def __repr__(self): + def __repr__(self) -> str: return utils.get_repr(self, constructor=True, config=self._config, configapi=self._configapi, prefix=self._prefix, pattern=self._pattern) @contextlib.contextmanager - def _handle_error(self, action, name): + def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: try: yield except configexc.Error as e: @@ -513,7 +563,7 @@ class ConfigContainer: text = "While {} '{}'".format(action, name) self._configapi.errors.append(configexc.ConfigErrorDesc(text, e)) - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: """Get an option or a new ConfigContainer with the added prefix. If we get an option which exists, we return the value for it. @@ -540,7 +590,7 @@ class ConfigContainer: return self._config.get_mutable_obj( name, pattern=self._pattern) - def __setattr__(self, attr, value): + def __setattr__(self, attr: str, value: Any) -> None: """Set the given option in the config.""" if attr.startswith('_'): super().__setattr__(attr, value) @@ -550,7 +600,7 @@ class ConfigContainer: with self._handle_error('setting', name): self._config.set_obj(name, value, pattern=self._pattern) - def _join(self, attr): + def _join(self, attr: str) -> str: """Get the prefix joined with the given attribute.""" if self._prefix: return '{}.{}'.format(self._prefix, attr) @@ -558,8 +608,10 @@ class ConfigContainer: return attr -def set_register_stylesheet(obj, *, stylesheet=None, update=True): - """Set the stylesheet for an object based on it's STYLESHEET attribute. +def set_register_stylesheet(obj: QObject, *, + stylesheet: str = None, + update: bool = True) -> None: + """Set the stylesheet for an object. Also, register an update when the config is changed. @@ -574,7 +626,7 @@ def set_register_stylesheet(obj, *, stylesheet=None, update=True): @functools.lru_cache() -def _render_stylesheet(stylesheet): +def _render_stylesheet(stylesheet: str) -> str: """Render the given stylesheet jinja template.""" with jinja.environment.no_autoescape(): template = jinja.environment.from_string(stylesheet) @@ -590,7 +642,9 @@ class StyleSheetObserver(QObject): _stylesheet: The stylesheet template to use. """ - def __init__(self, obj, stylesheet, update): + def __init__(self, obj: QObject, + stylesheet: typing.Optional[str], + update: bool) -> None: super().__init__() self._obj = obj self._update = update @@ -599,11 +653,11 @@ class StyleSheetObserver(QObject): if self._update: self.setParent(self._obj) if stylesheet is None: - self._stylesheet = obj.STYLESHEET + self._stylesheet = obj.STYLESHEET # type: str else: self._stylesheet = stylesheet - def _get_stylesheet(self): + def _get_stylesheet(self) -> str: """Format a stylesheet based on a template. Return: @@ -612,19 +666,15 @@ class StyleSheetObserver(QObject): return _render_stylesheet(self._stylesheet) @pyqtSlot() - def _update_stylesheet(self): + def _update_stylesheet(self) -> None: """Update the stylesheet for obj.""" self._obj.setStyleSheet(self._get_stylesheet()) - def register(self): - """Do a first update and listen for more. - - Args: - update: if False, don't listen for future updates. - """ + def register(self) -> None: + """Do a first update and listen for more.""" qss = self._get_stylesheet() - log.config.vdebug("stylesheet for {}: {}".format( - self._obj.__class__.__name__, qss)) + log.config.vdebug( # type: ignore + "stylesheet for {}: {}".format(self._obj.__class__.__name__, qss)) self._obj.setStyleSheet(qss) if self._update: instance.changed.connect(self._update_stylesheet) diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 574bc06af..0ee77fcc9 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -19,6 +19,7 @@ """Commands related to the configuration.""" +import typing import os.path import contextlib @@ -31,24 +32,34 @@ from qutebrowser.config import configtypes, configexc, configfiles, configdata from qutebrowser.misc import editor from qutebrowser.keyinput import keyutils +MYPY = False +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from qutebrowser.config.config import Config, KeyConfig + class ConfigCommands: """qutebrowser commands related to the configuration.""" - def __init__(self, config, keyconfig): + def __init__(self, + config: 'Config', + keyconfig: 'KeyConfig') -> None: self._config = config self._keyconfig = keyconfig @contextlib.contextmanager - def _handle_config_error(self): + def _handle_config_error(self) -> typing.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): + def _parse_pattern( + self, + pattern: typing.Optional[str] + ) -> typing.Optional[urlmatch.UrlPattern]: """Parse a pattern string argument to a pattern.""" if pattern is None: return None @@ -59,14 +70,15 @@ class ConfigCommands: raise cmdutils.CommandError("Error while parsing {}: {}" .format(pattern, str(e))) - def _parse_key(self, key): + def _parse_key(self, key: str) -> keyutils.KeySequence: """Parse a key argument.""" try: return keyutils.KeySequence.parse(key) except keyutils.KeyParseError as e: raise cmdutils.CommandError(str(e)) - def _print_value(self, option, pattern): + def _print_value(self, option: str, + pattern: typing.Optional[urlmatch.UrlPattern]) -> None: """Print the value of the given option.""" with self._handle_config_error(): value = self._config.get_str(option, pattern=pattern) @@ -81,8 +93,9 @@ class ConfigCommands: @cmdutils.argument('value', completion=configmodel.value) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) @cmdutils.argument('pattern', flag='u') - def set(self, win_id, option=None, value=None, temp=False, print_=False, - *, pattern=None): + def set(self, win_id: int, option: str = None, value: str = None, + temp: bool = False, print_: bool = False, + *, pattern: str = None) -> None: """Set an option. If the option name ends with '?' or no value is provided, the @@ -108,28 +121,28 @@ class ConfigCommands: raise cmdutils.CommandError("Toggling values was moved to the " ":config-cycle command") - pattern = self._parse_pattern(pattern) + parsed_pattern = self._parse_pattern(pattern) if option.endswith('?') and option != '?': - self._print_value(option[:-1], pattern=pattern) + self._print_value(option[:-1], pattern=parsed_pattern) return with self._handle_config_error(): if value is None: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) else: - self._config.set_str(option, value, pattern=pattern, + self._config.set_str(option, value, pattern=parsed_pattern, save_yaml=not temp) if print_: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) @cmdutils.register(instance='config-commands', maxsplit=1, no_cmd_split=True, no_replace_variables=True) @cmdutils.argument('command', completion=configmodel.bind) @cmdutils.argument('win_id', value=cmdutils.Value.win_id) - def bind(self, win_id, key=None, command=None, *, mode='normal', - default=False): + def bind(self, win_id: str, key: str = None, command: str = None, *, + mode: str = 'normal', default: bool = False) -> None: """Bind a key to a command. If no command is given, show the current binding for the given key. @@ -174,7 +187,7 @@ class ConfigCommands: self._keyconfig.bind(seq, command, mode=mode, save_yaml=True) @cmdutils.register(instance='config-commands') - def unbind(self, key, *, mode='normal'): + def unbind(self, key: str, *, mode: str = 'normal') -> None: """Unbind a keychain. Args: @@ -191,8 +204,9 @@ class ConfigCommands: @cmdutils.argument('option', completion=configmodel.option) @cmdutils.argument('values', completion=configmodel.value) @cmdutils.argument('pattern', flag='u') - def config_cycle(self, option, *values, pattern=None, temp=False, - print_=False): + def config_cycle(self, option: str, *values: str, + pattern: str = None, + temp: bool = False, print_: bool = False) -> None: """Cycle an option between multiple values. Args: @@ -202,15 +216,15 @@ class ConfigCommands: temp: Set value temporarily until qutebrowser is closed. print_: Print the value after setting. """ - pattern = self._parse_pattern(pattern) + parsed_pattern = self._parse_pattern(pattern) with self._handle_config_error(): opt = self._config.get_opt(option) - old_value = self._config.get_obj_for_pattern(option, - pattern=pattern) + old_value = self._config.get_obj_for_pattern( + option, pattern=parsed_pattern) if not values and isinstance(opt.typ, configtypes.Bool): - values = ['true', 'false'] + values = ('true', 'false') if len(values) < 2: raise cmdutils.CommandError("Need at least two values for " @@ -219,25 +233,25 @@ class ConfigCommands: # Use the next valid value from values, or the first if the current # value does not appear in the list with self._handle_config_error(): - values = [opt.typ.from_str(val) for val in values] + cycle_values = [opt.typ.from_str(val) for val in values] try: - idx = values.index(old_value) - idx = (idx + 1) % len(values) - value = values[idx] + idx = cycle_values.index(old_value) + idx = (idx + 1) % len(cycle_values) + value = cycle_values[idx] except ValueError: - value = values[0] + value = cycle_values[0] with self._handle_config_error(): - self._config.set_obj(option, value, pattern=pattern, + self._config.set_obj(option, value, pattern=parsed_pattern, save_yaml=not temp) if print_: - self._print_value(option, pattern=pattern) + self._print_value(option, pattern=parsed_pattern) @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.customized_option) - def config_unset(self, option, temp=False): + def config_unset(self, option: str, temp: bool = False) -> None: """Unset an option. This sets an option back to its default and removes it from @@ -252,7 +266,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.list_option) - def config_list_add(self, option, value, temp=False): + def config_list_add(self, option: str, value: str, + temp: bool = False) -> None: """Append a value to a config option that is a list. Args: @@ -273,7 +288,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.dict_option) - def config_dict_add(self, option, key, value, temp=False, replace=False): + def config_dict_add(self, option: str, key: str, value: str, + temp: bool = False, replace: bool = False) -> None: """Add a key/value pair to a dictionary option. Args: @@ -302,7 +318,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.list_option) - def config_list_remove(self, option, value, temp=False): + def config_list_remove(self, option: str, value: str, + temp: bool = False) -> None: """Remove a value from a list. Args: @@ -329,7 +346,8 @@ class ConfigCommands: @cmdutils.register(instance='config-commands') @cmdutils.argument('option', completion=configmodel.dict_option) - def config_dict_remove(self, option, key, temp=False): + def config_dict_remove(self, option: str, key: str, + temp: bool = False) -> None: """Remove a key from a dict. Args: @@ -354,7 +372,7 @@ class ConfigCommands: self._config.update_mutables(save_yaml=not temp) @cmdutils.register(instance='config-commands') - def config_clear(self, save=False): + def config_clear(self, save: bool = False) -> None: """Set all settings back to their default. Args: @@ -364,7 +382,7 @@ class ConfigCommands: self._config.clear(save_yaml=save) @cmdutils.register(instance='config-commands') - def config_source(self, filename=None, clear=False): + def config_source(self, filename: str = None, clear: bool = False) -> None: """Read a config.py file. Args: @@ -386,13 +404,13 @@ class ConfigCommands: raise cmdutils.CommandError(e) @cmdutils.register(instance='config-commands') - def config_edit(self, no_source=False): + def config_edit(self, no_source: bool = False) -> None: """Open the config.py file in the editor. Args: no_source: Don't re-source the config file after editing. """ - def on_file_updated(): + def on_file_updated() -> None: """Source the new config when editing finished. This can't use cmdutils.CommandError as it's run async. @@ -410,7 +428,8 @@ class ConfigCommands: ed.edit_file(filename) @cmdutils.register(instance='config-commands') - def config_write_py(self, filename=None, force=False, defaults=False): + def config_write_py(self, filename: str = None, + force: bool = False, defaults: bool = False) -> None: """Write the current configuration to a config.py file. Args: @@ -429,13 +448,13 @@ class ConfigCommands: raise cmdutils.CommandError("{} already exists - use --force to " "overwrite!".format(filename)) + options = [] # type: typing.List if defaults: options = [(None, opt, opt.default) for _name, opt in sorted(configdata.DATA.items())] bindings = dict(configdata.DATA['bindings.default'].default) commented = True else: - options = [] for values in self._config: for scoped in values: options.append((scoped.pattern, values.opt, scoped.value)) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index dace0772a..c93032387 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -24,14 +24,18 @@ Module attributes: DATA: A dict of Option objects after init() has been called. """ +import typing +from typing import Optional # pylint: disable=unused-import import functools import attr from qutebrowser.config import configtypes from qutebrowser.utils import usertypes, qtutils, utils -DATA = None -MIGRATIONS = None +DATA = typing.cast(typing.Mapping[str, 'Option'], None) +MIGRATIONS = typing.cast('Migrations', None) + +_BackendDict = typing.Mapping[str, typing.Union[str, bool]] @attr.s @@ -42,15 +46,15 @@ class Option: Note that this is just an option which exists, with no value associated. """ - name = attr.ib() - typ = attr.ib() - default = attr.ib() - backends = attr.ib() - raw_backends = attr.ib() - description = attr.ib() - supports_pattern = attr.ib(default=False) - restart = attr.ib(default=False) - no_autoconfig = attr.ib(default=False) + 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 @attr.s @@ -63,11 +67,13 @@ class Migrations: deleted: A list of option names which have been removed. """ - renamed = attr.ib(default=attr.Factory(dict)) - deleted = attr.ib(default=attr.Factory(list)) + renamed = attr.ib( + default=attr.Factory(dict)) # type: typing.Dict[str, str] + deleted = attr.ib( + default=attr.Factory(list)) # type: typing.List[str] -def _raise_invalid_node(name, what, node): +def _raise_invalid_node(name: str, what: str, node: typing.Any) -> None: """Raise an exception for an invalid configdata YAML node. Args: @@ -79,13 +85,16 @@ def _raise_invalid_node(name, what, node): name, what, node)) -def _parse_yaml_type(name, node): +def _parse_yaml_type( + name: str, + node: typing.Union[str, typing.Mapping[str, typing.Any]], +) -> configtypes.BaseType: if isinstance(node, str): # e.g: # type: Bool # -> create the type object without any arguments type_name = node - kwargs = {} + kwargs = {} # type: typing.MutableMapping[str, typing.Any] elif isinstance(node, dict): # e.g: # type: @@ -123,7 +132,10 @@ def _parse_yaml_type(name, node): type_name, node, e)) -def _parse_yaml_backends_dict(name, node): +def _parse_yaml_backends_dict( + name: str, + node: _BackendDict, +) -> typing.Sequence[usertypes.Backend]: """Parse a dict definition for backends. Example: @@ -160,7 +172,10 @@ def _parse_yaml_backends_dict(name, node): return backends -def _parse_yaml_backends(name, node): +def _parse_yaml_backends( + name: str, + node: typing.Union[None, str, _BackendDict], +) -> typing.Sequence[usertypes.Backend]: """Parse a backend node in the yaml. It can have one of those four forms: @@ -187,7 +202,9 @@ def _parse_yaml_backends(name, node): raise utils.Unreachable -def _read_yaml(yaml_data): +def _read_yaml( + yaml_data: str, +) -> typing.Tuple[typing.Mapping[str, Option], Migrations]: """Read config data from a YAML file. Args: @@ -249,12 +266,12 @@ def _read_yaml(yaml_data): @functools.lru_cache(maxsize=256) -def is_valid_prefix(prefix): +def is_valid_prefix(prefix: str) -> bool: """Check whether the given prefix is a valid prefix for some option.""" return any(key.startswith(prefix + '.') for key in DATA) -def init(): +def init() -> None: """Initialize configdata from the YAML file.""" global DATA, MIGRATIONS DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml')) diff --git a/qutebrowser/config/configdiff.py b/qutebrowser/config/configdiff.py index 9f8b70a26..ba78f64b4 100644 --- a/qutebrowser/config/configdiff.py +++ b/qutebrowser/config/configdiff.py @@ -19,6 +19,7 @@ """Code to show a diff of the legacy config format.""" +import typing # pylint: disable=unused-import,useless-suppression import difflib import os.path @@ -727,10 +728,10 @@ scroll right """ -def get_diff(): +def get_diff() -> str: """Get a HTML diff for the old config files.""" - old_conf_lines = [] - old_key_lines = [] + old_conf_lines = [] # type: typing.MutableSequence[str] + old_key_lines = [] # type: typing.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 051ed971a..b1dc04e09 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -19,9 +19,10 @@ """Exceptions related to config parsing.""" +import typing import attr -from qutebrowser.utils import jinja +from qutebrowser.utils import jinja, usertypes class Error(Exception): @@ -33,7 +34,7 @@ class NoAutoconfigError(Error): """Raised when this option can't be set in autoconfig.yml.""" - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__("The {} setting can only be set in config.py!" .format(name)) @@ -42,7 +43,11 @@ class BackendError(Error): """Raised when this setting is unavailable with the current backend.""" - def __init__(self, name, backend, raw_backends): + def __init__( + self, name: str, + backend: usertypes.Backend, + raw_backends: typing.Optional[typing.Mapping[str, bool]] + ) -> None: if raw_backends is None or not raw_backends[backend.name]: msg = ("The {} setting is not available with the {} backend!" .format(name, backend.name)) @@ -57,7 +62,7 @@ class NoPatternError(Error): """Raised when the given setting does not support URL patterns.""" - def __init__(self, name): + def __init__(self, name: str) -> None: super().__init__("The {} setting does not support URL patterns!" .format(name)) @@ -71,7 +76,7 @@ class ValidationError(Error): msg: Additional error message. """ - def __init__(self, value, msg): + def __init__(self, value: typing.Any, msg: str) -> None: super().__init__("Invalid value '{}' - {}".format(value, msg)) self.option = None @@ -85,7 +90,9 @@ class NoOptionError(Error): """Raised when an option was not found.""" - def __init__(self, option, *, deleted=False, renamed=None): + def __init__(self, option: str, *, + deleted: bool = False, + renamed: str = None) -> None: if deleted: assert renamed is None suffix = ' (this option was removed from qutebrowser)' @@ -109,18 +116,18 @@ class ConfigErrorDesc: traceback: The formatted traceback of the exception. """ - text = attr.ib() - exception = attr.ib() - traceback = attr.ib(None) + text = attr.ib() # type: str + exception = attr.ib() # type: typing.Union[str, Exception] + traceback = attr.ib(None) # type: str - def __str__(self): + def __str__(self) -> str: if self.traceback: return '{} - {}: {}'.format(self.text, self.exception.__class__.__name__, self.exception) return '{}: {}'.format(self.text, self.exception) - def with_text(self, text): + def with_text(self, text: str) -> 'ConfigErrorDesc': """Get a new ConfigErrorDesc with the given text appended.""" return self.__class__(text='{} ({})'.format(self.text, text), exception=self.exception, @@ -131,13 +138,15 @@ class ConfigFileErrors(Error): """Raised when multiple errors occurred inside the config.""" - def __init__(self, basename, errors): + def __init__(self, + basename: str, + errors: typing.Sequence[ConfigErrorDesc]) -> None: super().__init__("Errors occurred while reading {}:\n{}".format( basename, '\n'.join(' {}'.format(e) for e in errors))) self.basename = basename self.errors = errors - def to_html(self): + def to_html(self) -> str: """Get the error texts as a HTML snippet.""" template = jinja.environment.from_string(""" Errors occurred while reading {{ basename }}: diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index b4c8ea4ec..54ca91488 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -27,6 +27,7 @@ import textwrap import traceback import configparser import contextlib +import typing import yaml from PyQt5.QtCore import pyqtSignal, QObject, QSettings @@ -36,16 +37,21 @@ from qutebrowser.config import configexc, config, configdata, configutils from qutebrowser.keyinput import keyutils from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch +MYPY = False +if MYPY: + # pylint: disable=unused-import, useless-suppression + from qutebrowser.misc import savemanager + # The StateConfig instance -state = None +state = typing.cast('StateConfig', None) class StateConfig(configparser.ConfigParser): """The "state" file saving various application state.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._filename = os.path.join(standarddir.data(), 'state') self.read(self._filename, encoding='utf-8') @@ -59,7 +65,8 @@ class StateConfig(configparser.ConfigParser): for key in deleted_keys: self['general'].pop(key, None) - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -67,7 +74,7 @@ class StateConfig(configparser.ConfigParser): """ save_manager.add_saveable('state-config', self._save) - def _save(self): + def _save(self) -> None: """Save the state file to the configured location.""" with open(self._filename, 'w', encoding='utf-8') as f: self.write(f) @@ -84,17 +91,20 @@ class YamlConfig(QObject): VERSION = 2 changed = pyqtSignal() - def __init__(self, parent=None): + _SettingsType = typing.Dict[str, typing.Dict[str, typing.Any]] + + def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._filename = os.path.join(standarddir.config(auto=True), 'autoconfig.yml') - self._dirty = None + self._dirty = False - self._values = {} + self._values = {} # type: typing.Dict[str, configutils.Values] for name, opt in configdata.DATA.items(): self._values[name] = configutils.Values(opt) - def init_save_manager(self, save_manager): + def init_save_manager(self, + save_manager: 'savemanager.SaveManager') -> None: """Make sure the config gets saved properly. We do this outside of __init__ because the config gets created before @@ -102,21 +112,21 @@ class YamlConfig(QObject): """ save_manager.add_saveable('yaml-config', self._save, self.changed) - def __iter__(self): + def __iter__(self) -> typing.Iterator[configutils.Values]: """Iterate over configutils.Values items.""" yield from self._values.values() - def _mark_changed(self): + def _mark_changed(self) -> None: """Mark the YAML config as changed.""" self._dirty = True self.changed.emit() - def _save(self): + def _save(self) -> None: """Save the settings to the YAML file if they've changed.""" if not self._dirty: return - settings = {} + settings = {} # type: YamlConfig._SettingsType for name, values in sorted(self._values.items()): if not values: continue @@ -135,7 +145,10 @@ class YamlConfig(QObject): """.lstrip('\n'))) utils.yaml_dump(data, f) - def _pop_object(self, yaml_data, key, typ): + def _pop_object(self, + yaml_data: typing.Any, + key: str, + typ: type) -> typing.Any: """Get a global object from the given data.""" if not isinstance(yaml_data, dict): desc = configexc.ConfigErrorDesc("While loading data", @@ -158,7 +171,7 @@ class YamlConfig(QObject): return data - def load(self): + def load(self) -> None: """Load configuration from the configured YAML file.""" try: with open(self._filename, 'r', encoding='utf-8') as f: @@ -189,18 +202,19 @@ class YamlConfig(QObject): self._validate(settings) self._build_values(settings) - def _load_settings_object(self, yaml_data): + def _load_settings_object(self, yaml_data: typing.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): + def _load_legacy_settings_object(self, + yaml_data: typing.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): + def _build_values(self, settings: typing.Mapping) -> None: """Build up self._values from the values in the given dict.""" errors = [] for name, yaml_values in settings.items(): @@ -233,7 +247,8 @@ class YamlConfig(QObject): if errors: raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def _migrate_bool(self, settings, name, true_value, false_value): + def _migrate_bool(self, settings: _SettingsType, name: str, + true_value: str, false_value: str) -> None: """Migrate a boolean in the settings.""" if name in settings: for scope, val in settings[name].items(): @@ -241,7 +256,7 @@ class YamlConfig(QObject): settings[name][scope] = true_value if val else false_value self._mark_changed() - def _handle_migrations(self, settings): + def _handle_migrations(self, settings: _SettingsType) -> '_SettingsType': """Migrate older configs to the newest format.""" # Simple renamed/deleted options for name in list(settings): @@ -299,7 +314,7 @@ class YamlConfig(QObject): return settings - def _validate(self, settings): + def _validate(self, settings: _SettingsType) -> None: """Make sure all settings exist.""" unknown = [] for name in settings: @@ -312,18 +327,19 @@ class YamlConfig(QObject): for e in sorted(unknown)] raise configexc.ConfigFileErrors('autoconfig.yml', errors) - def set_obj(self, name, value, *, pattern=None): + def set_obj(self, name: str, value: typing.Any, *, + pattern: urlmatch.UrlPattern = None) -> None: """Set the given setting to the given value.""" self._values[name].add(value, pattern) self._mark_changed() - def unset(self, name, *, pattern=None): + def unset(self, name: str, *, pattern: urlmatch.UrlPattern = None) -> None: """Remove the given option name if it's configured.""" changed = self._values[name].remove(pattern) if changed: self._mark_changed() - def clear(self): + def clear(self) -> None: """Clear all values from the YAML file.""" for values in self._values.values(): values.clear() @@ -346,15 +362,15 @@ class ConfigAPI: datadir: The qutebrowser data directory, as pathlib.Path. """ - def __init__(self, conf, keyconfig): + def __init__(self, conf: config.Config, keyconfig: config.KeyConfig): self._config = conf self._keyconfig = keyconfig - self.errors = [] + self.errors = [] # type: typing.List[configexc.ConfigErrorDesc] self.configdir = pathlib.Path(standarddir.config()) self.datadir = pathlib.Path(standarddir.data()) @contextlib.contextmanager - def _handle_error(self, action, name): + def _handle_error(self, action: str, name: str) -> typing.Iterator[None]: """Catch config-related exceptions and save them in self.errors.""" try: yield @@ -372,40 +388,40 @@ class ConfigAPI: text = "While {} '{}' and parsing key".format(action, name) self.errors.append(configexc.ConfigErrorDesc(text, e)) - def finalize(self): + def finalize(self) -> None: """Do work which needs to be done after reading config.py.""" self._config.update_mutables() - def load_autoconfig(self): + def load_autoconfig(self) -> None: """Load the autoconfig.yml file which is used for :set/:bind/etc.""" with self._handle_error('reading', 'autoconfig.yml'): read_autoconfig() - def get(self, name, pattern=None): + def get(self, name: str, pattern: str = None) -> typing.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, value, pattern=None): + def set(self, name: str, value: typing.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, command, mode='normal'): + def bind(self, key: str, command: 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) self._keyconfig.bind(seq, command, mode=mode) - def unbind(self, key, mode='normal'): + def unbind(self, key: str, mode: str = 'normal') -> None: """Unbind a key from a command, with an optional key mode.""" with self._handle_error('unbinding', key): seq = keyutils.KeySequence.parse(key) self._keyconfig.unbind(seq, mode=mode) - def source(self, filename): + def source(self, filename: str) -> None: """Read the given config file from disk.""" if not os.path.isabs(filename): filename = str(self.configdir / filename) @@ -416,7 +432,7 @@ class ConfigAPI: self.errors += e.errors @contextlib.contextmanager - def pattern(self, pattern): + def pattern(self, pattern: str) -> typing.Iterator[config.ConfigContainer]: """Get a ConfigContainer for the given pattern.""" # We need to propagate the exception so we don't need to return # something. @@ -430,17 +446,21 @@ class ConfigPyWriter: """Writer for config.py files from given settings.""" - def __init__(self, options, bindings, *, commented): + def __init__( + self, + options: typing.List, + bindings: typing.MutableMapping[str, typing.Mapping[str, str]], *, + commented: bool) -> None: self._options = options self._bindings = bindings self._commented = commented - def write(self, filename): + def write(self, filename: str) -> None: """Write the config to the given file.""" with open(filename, 'w', encoding='utf-8') as f: f.write('\n'.join(self._gen_lines())) - def _line(self, line): + def _line(self, line: str) -> str: """Get an (optionally commented) line.""" if self._commented: if line.startswith('#'): @@ -450,7 +470,7 @@ class ConfigPyWriter: else: return line - def _gen_lines(self): + def _gen_lines(self) -> typing.Iterator[str]: """Generate a config.py with the given settings/bindings. Yields individual lines. @@ -459,7 +479,7 @@ class ConfigPyWriter: yield from self._gen_options() yield from self._gen_bindings() - def _gen_header(self): + def _gen_header(self) -> typing.Iterator[str]: """Generate the initial header of the config.""" yield self._line("# Autogenerated config.py") yield self._line("# Documentation:") @@ -481,7 +501,7 @@ class ConfigPyWriter: yield self._line("# config.load_autoconfig()") yield '' - def _gen_options(self): + def _gen_options(self) -> typing.Iterator[str]: """Generate the options part of the config.""" for pattern, opt, value in self._options: if opt.name in ['bindings.commands', 'bindings.default']: @@ -509,7 +529,7 @@ class ConfigPyWriter: opt.name, value, str(pattern))) yield '' - def _gen_bindings(self): + def _gen_bindings(self) -> typing.Iterator[str]: """Generate the bindings part of the config.""" normal_bindings = self._bindings.pop('normal', {}) if normal_bindings: @@ -527,7 +547,7 @@ class ConfigPyWriter: yield '' -def read_config_py(filename, raising=False): +def read_config_py(filename: str, raising: bool = False) -> None: """Read a config.py file. Arguments; @@ -543,8 +563,8 @@ def read_config_py(filename, raising=False): basename = os.path.basename(filename) module = types.ModuleType('config') - module.config = api - module.c = container + module.config = api # type: ignore + module.c = container # type: ignore module.__file__ = filename try: @@ -589,7 +609,7 @@ def read_config_py(filename, raising=False): raise configexc.ConfigFileErrors('config.py', api.errors) -def read_autoconfig(): +def read_autoconfig() -> None: """Read the autoconfig.yml file.""" try: config.instance.read_yaml() @@ -601,7 +621,7 @@ def read_autoconfig(): @contextlib.contextmanager -def saved_sys_properties(): +def saved_sys_properties() -> typing.Iterator[None]: """Save various sys properties such as sys.path and sys.modules.""" old_path = sys.path.copy() old_modules = sys.modules.copy() @@ -614,7 +634,7 @@ def saved_sys_properties(): del sys.modules[module] -def init(): +def init() -> None: """Initialize config storage not related to the main config.""" global state state = StateConfig() diff --git a/qutebrowser/config/configinit.py b/qutebrowser/config/configinit.py index de9651064..ff0fd0e41 100644 --- a/qutebrowser/config/configinit.py +++ b/qutebrowser/config/configinit.py @@ -19,8 +19,10 @@ """Initialization of the configuration.""" +import argparse import os.path import sys +import typing from PyQt5.QtWidgets import QMessageBox @@ -30,14 +32,14 @@ from qutebrowser.config import (config, configdata, configfiles, configtypes, from qutebrowser.utils import (objreg, usertypes, log, standarddir, message, qtutils) from qutebrowser.config import configcache -from qutebrowser.misc import msgbox, objects +from qutebrowser.misc import msgbox, objects, savemanager # Error which happened during init, so we can show a message box. _init_errors = None -def early_init(args): +def early_init(args: argparse.Namespace) -> None: """Initialize the part of the config which works without a QApplication.""" configdata.init() @@ -85,7 +87,7 @@ def early_init(args): _init_envvars() -def _init_envvars(): +def _init_envvars() -> None: """Initialize environment variables which need to be set early.""" if objects.backend == usertypes.Backend.QtWebEngine: software_rendering = config.val.qt.force_software_rendering @@ -107,7 +109,7 @@ def _init_envvars(): @config.change_filter('fonts.monospace', function=True) -def _update_monospace_fonts(): +def _update_monospace_fonts() -> None: """Update all fonts if fonts.monospace was set.""" configtypes.Font.monospace_fonts = config.val.fonts.monospace for name, opt in configdata.DATA.items(): @@ -123,7 +125,7 @@ def _update_monospace_fonts(): config.instance.changed.emit(name) -def get_backend(args): +def get_backend(args: argparse.Namespace) -> usertypes.Backend: """Find out what backend to use based on available libraries.""" str_to_backend = { 'webkit': usertypes.Backend.QtWebKit, @@ -136,7 +138,7 @@ def get_backend(args): return str_to_backend[config.val.backend] -def late_init(save_manager): +def late_init(save_manager: savemanager.SaveManager) -> None: """Initialize the rest of the config after the QApplication is created.""" global _init_errors if _init_errors is not None: @@ -152,7 +154,7 @@ def late_init(save_manager): configfiles.state.init_save_manager(save_manager) -def qt_args(namespace): +def qt_args(namespace: argparse.Namespace) -> typing.List[str]: """Get the Qt QApplication arguments based on an argparse namespace. Args: @@ -178,7 +180,7 @@ def qt_args(namespace): return argv -def _qtwebengine_args(): +def _qtwebengine_args() -> typing.Iterator[str]: """Get the QtWebEngine arguments to use based on the config.""" if not qtutils.version_check('5.11', compiled=False): # WORKAROUND equivalent to @@ -224,7 +226,7 @@ def _qtwebengine_args(): '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/configutils.py b/qutebrowser/config/configutils.py index 96fc0f02d..9d4dc94ef 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -21,11 +21,19 @@ """Utilities and data structures used by various config code.""" -import attr +import typing -from qutebrowser.utils import utils +import attr +from PyQt5.QtCore import QUrl + +from qutebrowser.utils import utils, urlmatch from qutebrowser.config import configexc +MYPY = False +if MYPY: # pragma: no cover + # pylint: disable=unused-import,useless-suppression + from qutebrowser.config import configdata + class _UnsetObject: @@ -33,7 +41,7 @@ class _UnsetObject: __slots__ = () - def __repr__(self): + def __repr__(self) -> str: return '' @@ -50,8 +58,8 @@ class ScopedValue: pattern: The UrlPattern for the value, or None for global values. """ - value = attr.ib() - pattern = attr.ib() + value = attr.ib() # type: typing.Any + pattern = attr.ib() # type: typing.Optional[urlmatch.UrlPattern] class Values: @@ -73,15 +81,17 @@ class Values: opt: The Option being customized. """ - def __init__(self, opt, values=None): + def __init__(self, + opt: 'configdata.Option', + values: typing.MutableSequence = None) -> None: self.opt = opt self._values = values or [] - def __repr__(self): + def __repr__(self) -> str: return utils.get_repr(self, opt=self.opt, values=self._values, constructor=True) - def __str__(self): + def __str__(self) -> str: """Get the values as human-readable string.""" if not self: return '{}: '.format(self.opt.name) @@ -96,7 +106,7 @@ class Values: scoped.pattern, self.opt.name, str_value)) return '\n'.join(lines) - def __iter__(self): + def __iter__(self) -> typing.Iterator['ScopedValue']: """Yield ScopedValue elements. This yields in "normal" order, i.e. global and then first-set settings @@ -104,23 +114,25 @@ class Values: """ yield from self._values - def __bool__(self): + def __bool__(self) -> bool: """Check whether this value is customized.""" return bool(self._values) - def _check_pattern_support(self, arg): + def _check_pattern_support( + self, arg: typing.Optional[urlmatch.UrlPattern]) -> 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, pattern=None): + def add(self, value: typing.Any, + pattern: urlmatch.UrlPattern = None) -> None: """Add a value with the given pattern to the list of values.""" self._check_pattern_support(pattern) self.remove(pattern) scoped = ScopedValue(value, pattern) self._values.append(scoped) - def remove(self, pattern=None): + def remove(self, pattern: urlmatch.UrlPattern = None) -> bool: """Remove the value with the given pattern. If a matching pattern was removed, True is returned. @@ -131,11 +143,11 @@ class Values: self._values = [v for v in self._values if v.pattern != pattern] return old_len != len(self._values) - def clear(self): + def clear(self) -> None: """Clear all customization for this value.""" self._values = [] - def _get_fallback(self, fallback): + def _get_fallback(self, fallback: typing.Any) -> typing.Any: """Get the fallback global/default value.""" for scoped in self._values: if scoped.pattern is None: @@ -146,7 +158,8 @@ class Values: else: return UNSET - def get_for_url(self, url=None, *, fallback=True): + def get_for_url(self, url: QUrl = None, *, + fallback: bool = True) -> typing.Any: """Get a config value, falling back when needed. This first tries to find a value matching the URL (if given). @@ -165,7 +178,9 @@ class Values: return self._get_fallback(fallback) - def get_for_pattern(self, pattern, *, fallback=True): + def get_for_pattern(self, + pattern: typing.Optional[urlmatch.UrlPattern], *, + fallback: bool = True) -> typing.Any: """Get a value only if it's been overridden for the given pattern. This is useful when showing values to the user.