diff --git a/mypy.ini b/mypy.ini index c61ca1f18..2e1d50558 100644 --- a/mypy.ini +++ b/mypy.ini @@ -98,6 +98,10 @@ disallow_incomplete_defs = True disallow_untyped_defs = True disallow_incomplete_defs = True +[mypy-qutebrowser.config.configtypes] +disallow_untyped_defs = True +disallow_incomplete_defs = True + [mypy-qutebrowser.api.*] disallow_untyped_defs = True disallow_incomplete_defs = True diff --git a/qutebrowser/config/configexc.py b/qutebrowser/config/configexc.py index b1dc04e09..80a2cedb2 100644 --- a/qutebrowser/config/configexc.py +++ b/qutebrowser/config/configexc.py @@ -76,7 +76,8 @@ class ValidationError(Error): msg: Additional error message. """ - def __init__(self, value: typing.Any, msg: str) -> None: + def __init__(self, value: typing.Any, + msg: typing.Union[str, Exception]) -> None: super().__init__("Invalid value '{}' - {}".format(value, msg)) self.option = None diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index d4ce33d21..61dba365e 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -52,13 +52,14 @@ import datetime import functools import operator import json -import typing # pylint: disable=unused-import +import typing import attr import yaml from PyQt5.QtCore import QUrl, Qt from PyQt5.QtGui import QColor, QFont from PyQt5.QtWidgets import QTabWidget, QTabBar +from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.misc import objects from qutebrowser.config import configexc, configutils @@ -67,13 +68,23 @@ from qutebrowser.utils import (standarddir, utils, qtutils, urlutils, urlmatch, from qutebrowser.keyinput import keyutils -SYSTEM_PROXY = object() # Return value for Proxy type +class _SystemProxy: + + pass + + +SYSTEM_PROXY = _SystemProxy() # Return value for Proxy type # Taken from configparser 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, configutils.Unset] +_StrUnsetNone = typing.Union[None, str, configutils.Unset] + + class ValidValues: """Container for valid values for a given type. @@ -84,11 +95,15 @@ class ValidValues: generate_docs: Whether to show the values in the docs. """ - def __init__(self, *values, generate_docs=True): + def __init__(self, + *values: typing.Union[str, + typing.Dict[str, str], + typing.Tuple[str, str]], + generate_docs: bool = True) -> None: if not values: raise ValueError("ValidValues with no values makes no sense!") - self.descriptions = {} - self.values = [] + self.descriptions = {} # type: typing.Dict[str, str] + self.values = [] # type: typing.List[str] self.generate_docs = generate_docs for value in values: if isinstance(value, str): @@ -105,17 +120,18 @@ class ValidValues: self.values.append(value[0]) self.descriptions[value[0]] = value[1] - def __contains__(self, val): + def __contains__(self, val: str) -> bool: return val in self.values - def __iter__(self): + def __iter__(self) -> typing.Iterator[str]: return self.values.__iter__() - def __repr__(self): + def __repr__(self) -> str: return utils.get_repr(self, values=self.values, descriptions=self.descriptions) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + assert isinstance(other, ValidValues) return (self.values == other.values and self.descriptions == other.descriptions) @@ -132,26 +148,28 @@ class BaseType: string. ValidValues instance. """ - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: self.none_ok = none_ok - self.valid_values = None + self.valid_values = None # type: typing.Optional[ValidValues] - def get_name(self): + def get_name(self) -> str: """Get a name for the type for documentation.""" return self.__class__.__name__ - def get_valid_values(self): + def get_valid_values(self) -> typing.Optional[ValidValues]: """Get the type's valid values for documentation.""" return self.valid_values - def _basic_py_validation(self, value, pytype): + def _basic_py_validation( + self, value: typing.Any, + pytype: typing.Union[type, typing.Tuple[type, ...]]) -> None: """Do some basic validation for Python values (emptyness, type). Arguments: value: The value to check. pytype: A Python type to check the value against. """ - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return if (value is None or (pytype == list and value == []) or @@ -174,7 +192,7 @@ class BaseType: if isinstance(value, str): self._basic_str_validation(value) - def _basic_str_validation(self, value): + def _basic_str_validation(self, value: str) -> None: """Do some basic validation for string values. This checks that the value isn't empty and doesn't contain any @@ -190,7 +208,8 @@ class BaseType: raise configexc.ValidationError( value, "may not contain unprintable chars!") - def _validate_surrogate_escapes(self, full_value, value): + def _validate_surrogate_escapes(self, full_value: typing.Any, + value: typing.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. @@ -201,7 +220,7 @@ class BaseType: raise configexc.ValidationError( full_value, "may not contain surrogate escapes!") - def _validate_valid_values(self, value): + def _validate_valid_values(self, value: str) -> None: """Validate value against possible values. The default implementation checks the value against self.valid_values @@ -216,7 +235,7 @@ class BaseType: value, "valid values: {}".format(', '.join(self.valid_values))) - def from_str(self, value): + def from_str(self, value: str) -> typing.Any: """Get the setting value from a string. By default this invokes to_py() for validation and returns the @@ -235,11 +254,11 @@ class BaseType: return None return value - def from_obj(self, value): + def from_obj(self, value: typing.Any) -> typing.Any: """Get the setting value from a config.py/YAML object.""" return value - def to_py(self, value): + def to_py(self, value: typing.Any) -> typing.Any: """Get the setting value from a Python value. Args: @@ -253,7 +272,7 @@ class BaseType: """ raise NotImplementedError - def to_str(self, value): + def to_str(self, value: typing.Any) -> str: """Get a string from the setting value. The resulting string should be parseable again by from_str. @@ -263,7 +282,7 @@ class BaseType: assert isinstance(value, str), value return value - def to_doc(self, value, indent=0): + def to_doc(self, value: typing.Any, indent: int = 0) -> str: """Get a string with the given value for the documentation. This currently uses asciidoc syntax. @@ -274,7 +293,7 @@ class BaseType: return 'empty' return '+pass:[{}]+'.format(html.escape(str_value)) - def complete(self): + def complete(self) -> _Completions: """Return a list of possible values for completion. The default implementation just returns valid_values, but it might be @@ -308,13 +327,14 @@ class MappingType(BaseType): MAPPING = {} # type: typing.Dict[str, typing.Any] - def __init__(self, none_ok=False, valid_values=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): + def to_py(self, value: typing.Any) -> typing.Any: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -335,9 +355,11 @@ class String(BaseType): completions: completions to be used, or None """ - def __init__(self, *, minlen=None, maxlen=None, forbidden=None, - encoding=None, none_ok=False, completions=None, - valid_values=None): + def __init__(self, *, minlen: int = None, maxlen: int = None, + forbidden: str = None, encoding: str = None, + none_ok: bool = False, + completions: _Completions = None, + valid_values: ValidValues = None) -> None: super().__init__(none_ok) self.valid_values = valid_values @@ -354,7 +376,7 @@ class String(BaseType): self._completions = completions self.encoding = encoding - def _validate_encoding(self, value): + def _validate_encoding(self, value: str) -> None: """Check if the given value fits into the configured encoding. Raises ValidationError if not. @@ -372,9 +394,9 @@ class String(BaseType): value, self.encoding, e) raise configexc.ValidationError(value, msg) - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -395,7 +417,7 @@ class String(BaseType): return value - def complete(self): + def complete(self) -> _Completions: if self._completions is not None: return self._completions else: @@ -406,19 +428,19 @@ class UniqueCharString(String): """A string which may not contain duplicate chars.""" - def to_py(self, value): - value = super().to_py(value) - if value is configutils.UNSET: - return value - elif not value: + def to_py(self, value: _StrUnset) -> _StrUnsetNone: + py_value = super().to_py(value) + if isinstance(py_value, configutils.Unset): + return py_value + elif not py_value: return None # Check for duplicate values - if len(set(value)) != len(value): + if len(set(py_value)) != len(py_value): raise configexc.ValidationError( - value, "String contains duplicate values!") + py_value, "String contains duplicate values!") - return value + return py_value class List(BaseType): @@ -430,21 +452,23 @@ class List(BaseType): _show_valtype = True - def __init__(self, valtype, none_ok=False, length=None): + def __init__(self, valtype: BaseType, + none_ok: bool = False, + length: int = None) -> None: super().__init__(none_ok) self.valtype = valtype self.length = length - def get_name(self): + def get_name(self) -> str: name = super().get_name() if self._show_valtype: name += " of " + self.valtype.get_name() return name - def get_valid_values(self): + def get_valid_values(self) -> typing.Optional[ValidValues]: return self.valtype.get_valid_values() - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[typing.List]: self._basic_str_validation(value) if not value: return None @@ -459,14 +483,17 @@ class List(BaseType): self.to_py(yaml_val) return yaml_val - def from_obj(self, value): + def from_obj(self, value: typing.Optional[typing.List]) -> typing.List: if value is None: return [] return [self.valtype.from_obj(v) for v in value] - def to_py(self, value): + def to_py( + self, + value: typing.Union[typing.List, configutils.Unset] + ) -> typing.Union[typing.List, configutils.Unset]: self._basic_py_validation(value, list) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return [] @@ -479,13 +506,13 @@ class List(BaseType): "be set!".format(self.length)) return [self.valtype.to_py(v) for v in value] - def to_str(self, value): + def to_str(self, value: typing.List) -> str: if not value: # An empty list is treated just like None -> empty string return '' return json.dumps(value) - def to_doc(self, value, indent=0): + def to_doc(self, value: typing.List, indent: int = 0) -> str: if not value: return 'empty' @@ -513,13 +540,16 @@ class ListOrValue(BaseType): _show_valtype = True - def __init__(self, valtype, *args, none_ok=False, **kwargs): + def __init__(self, valtype: BaseType, *, + none_ok: bool = False, + **kwargs: typing.Any) -> None: super().__init__(none_ok) assert not isinstance(valtype, (List, ListOrValue)), valtype - self.listtype = List(valtype, none_ok=none_ok, *args, **kwargs) + self.listtype = List(valtype, none_ok=none_ok, **kwargs) self.valtype = valtype - def _val_and_type(self, value): + def _val_and_type(self, + value: typing.Any) -> typing.Tuple[typing.Any, BaseType]: """Get the value and type to use for to_str/to_doc/from_str.""" if isinstance(value, list): if len(value) == 1: @@ -529,25 +559,25 @@ class ListOrValue(BaseType): else: return value, self.valtype - def get_name(self): + def get_name(self) -> str: return self.listtype.get_name() + ', or ' + self.valtype.get_name() - def get_valid_values(self): + def get_valid_values(self) -> typing.Optional[ValidValues]: return self.valtype.get_valid_values() - def from_str(self, value): + def from_str(self, value: str) -> typing.Any: try: return self.listtype.from_str(value) except configexc.ValidationError: return self.valtype.from_str(value) - def from_obj(self, value): + def from_obj(self, value: typing.Any) -> typing.Any: if value is None: return [] return value - def to_py(self, value): - if value is configutils.UNSET: + def to_py(self, value: typing.Any) -> typing.Any: + if isinstance(value, configutils.Unset): return value try: @@ -555,14 +585,14 @@ class ListOrValue(BaseType): except configexc.ValidationError: return self.listtype.to_py(value) - def to_str(self, value): + def to_str(self, value: typing.Any) -> str: if value is None: return '' val, typ = self._val_and_type(value) return typ.to_str(val) - def to_doc(self, value, indent=0): + def to_doc(self, value: typing.Any, indent: int = 0) -> str: if value is None: return 'empty' @@ -578,26 +608,31 @@ class FlagList(List): the valid values of the setting. """ - combinable_values = None # type: typing.Optional[typing.Iterable] + combinable_values = None # type: typing.Optional[typing.Sequence] _show_valtype = False - def __init__(self, none_ok=False, valid_values=None, length=None): + def __init__(self, none_ok: bool = False, + valid_values: ValidValues = None, + length: int = None) -> None: super().__init__(valtype=String(), none_ok=none_ok, length=length) self.valtype.valid_values = valid_values - def _check_duplicates(self, values): + def _check_duplicates(self, values: typing.List) -> None: if len(set(values)) != len(values): raise configexc.ValidationError( values, "List contains duplicate values!") - def to_py(self, value): + def to_py( + self, + value: typing.Union[configutils.Unset, typing.List], + ) -> typing.Union[configutils.Unset, typing.List]: vals = super().to_py(value) - if vals is not configutils.UNSET: + if not isinstance(vals, configutils.Unset): self._check_duplicates(vals) return vals - def complete(self): + def complete(self) -> _Completions: valid_values = self.valtype.valid_values if valid_values is None: return None @@ -626,15 +661,15 @@ class Bool(BaseType): while `0`, `no`, `off` and `false` count as false (case-insensitive). """ - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) self.valid_values = ValidValues('true', 'false', generate_docs=False) - def to_py(self, value): + def to_py(self, value: typing.Optional[bool]) -> typing.Optional[bool]: self._basic_py_validation(value, bool) return value - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[bool]: self._basic_str_validation(value) if not value: return None @@ -644,7 +679,7 @@ class Bool(BaseType): except KeyError: raise configexc.ValidationError(value, "must be a boolean!") - def to_str(self, value): + def to_str(self, value: typing.Optional[bool]) -> str: mapping = { None: '', True: 'true', @@ -657,25 +692,27 @@ class BoolAsk(Bool): """Like `Bool`, but `ask` is allowed as additional value.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) self.valid_values = ValidValues('true', 'false', 'ask') - def to_py(self, value): + def to_py(self, # type: ignore + value: typing.Union[bool, str]) -> typing.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': return 'ask' - return super().to_py(value) + return super().to_py(value) # type: ignore - def from_str(self, value): + def from_str(self, # type: ignore + value: str) -> typing.Union[bool, str, None]: # basic validation unneeded if it's == 'ask' and done by Bool if we # call super().from_str if isinstance(value, str) and value.lower() == 'ask': return 'ask' return super().from_str(value) - def to_str(self, value): + def to_str(self, value: typing.Union[bool, str, None]) -> str: mapping = { None: '', True: 'true', @@ -694,7 +731,9 @@ class _Numeric(BaseType): # pylint: disable=abstract-method maxval: Maximum value (inclusive). """ - def __init__(self, minval=None, maxval=None, none_ok=False): + def __init__(self, minval: int = None, + maxval: int = None, + none_ok: bool = False) -> None: super().__init__(none_ok) self.minval = self._parse_bound(minval) self.maxval = self._parse_bound(maxval) @@ -703,7 +742,9 @@ class _Numeric(BaseType): # pylint: disable=abstract-method raise ValueError("minval ({}) needs to be <= maxval ({})!" .format(self.minval, self.maxval)) - def _parse_bound(self, bound): + def _parse_bound( + self, bound: typing.Union[None, str, int, float] + ) -> typing.Union[None, int, float]: """Get a numeric bound from a string like 'maxint'.""" if bound == 'maxint': return qtutils.MAXVALS['int'] @@ -714,7 +755,8 @@ class _Numeric(BaseType): # pylint: disable=abstract-method assert isinstance(bound, (int, float)), bound return bound - def _validate_bounds(self, value, suffix=''): + def _validate_bounds(self, value: typing.Union[None, int, float], + suffix: str = '') -> None: """Validate self.minval and self.maxval.""" if value is None: return @@ -725,7 +767,7 @@ class _Numeric(BaseType): # pylint: disable=abstract-method raise configexc.ValidationError( value, "must be {}{} or smaller!".format(self.maxval, suffix)) - def to_str(self, value): + def to_str(self, value: typing.Union[None, int, float]) -> str: if value is None: return '' return str(value) @@ -735,7 +777,7 @@ class Int(_Numeric): """Base class for an integer setting.""" - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[int]: self._basic_str_validation(value) if not value: return None @@ -747,7 +789,7 @@ class Int(_Numeric): self.to_py(intval) return intval - def to_py(self, value): + def to_py(self, value: typing.Optional[int]) -> typing.Optional[int]: self._basic_py_validation(value, int) self._validate_bounds(value) return value @@ -757,7 +799,7 @@ class Float(_Numeric): """Base class for a float setting.""" - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[float]: self._basic_str_validation(value) if not value: return None @@ -769,7 +811,10 @@ class Float(_Numeric): self.to_py(floatval) return floatval - def to_py(self, value): + def to_py( + self, + value: typing.Union[None, int, float], + ) -> typing.Union[None, int, float]: self._basic_py_validation(value, (int, float)) self._validate_bounds(value) return value @@ -779,9 +824,12 @@ class Perc(_Numeric): """A percentage.""" - def to_py(self, value): + def to_py( + self, + value: typing.Union[None, float, int, str, configutils.Unset] + ) -> typing.Union[None, float, int, configutils.Unset]: self._basic_py_validation(value, (float, int, str)) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -796,7 +844,7 @@ class Perc(_Numeric): self._validate_bounds(value, suffix='%') return value - def to_str(self, value): + def to_str(self, value: typing.Union[None, float, int, str]) -> str: if value is None: return '' return value @@ -813,8 +861,9 @@ class PercOrInt(_Numeric): maxint: Maximum value for integer (inclusive). """ - def __init__(self, minperc=None, maxperc=None, minint=None, maxint=None, - none_ok=False): + def __init__(self, minperc: int = None, maxperc: int = None, + minint: int = None, maxint: int = None, + none_ok: bool = False) -> None: super().__init__(minval=minint, maxval=maxint, none_ok=none_ok) self.minperc = self._parse_bound(minperc) self.maxperc = self._parse_bound(maxperc) @@ -823,7 +872,7 @@ class PercOrInt(_Numeric): raise ValueError("minperc ({}) needs to be <= maxperc " "({})!".format(self.minperc, self.maxperc)) - def from_str(self, value): + def from_str(self, value: str) -> typing.Union[None, str, int]: self._basic_str_validation(value) if not value: return None @@ -840,7 +889,10 @@ class PercOrInt(_Numeric): self.to_py(intval) return intval - def to_py(self, value): + def to_py( + self, + value: typing.Union[None, str, int] + ) -> typing.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: @@ -880,13 +932,13 @@ class Command(BaseType): invalid commands (in bindings/aliases) fail when used. """ - def complete(self): + def complete(self) -> _Completions: out = [] for cmdname, obj in objects.commands.items(): out.append((cmdname, obj.desc)) return out - def to_py(self, value): + def to_py(self, value: str) -> str: self._basic_py_validation(value, str) return value @@ -895,7 +947,7 @@ class ColorSystem(MappingType): """The color system to use for color interpolation.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues( @@ -916,7 +968,7 @@ class IgnoreCase(MappingType): """Whether to search case insensitively.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues( @@ -946,7 +998,7 @@ class QtColor(BaseType): * `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359) """ - def _parse_value(self, val): + def _parse_value(self, val: str) -> int: try: return int(val) except ValueError: @@ -962,9 +1014,10 @@ class QtColor(BaseType): except ValueError: raise configexc.ValidationError(val, "must be a valid color value") - def to_py(self, value): + def to_py(self, value: _StrUnset) -> typing.Union[configutils.Unset, + None, QColor]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -973,15 +1026,15 @@ class QtColor(BaseType): openparen = value.index('(') kind = value[:openparen] vals = value[openparen+1:-1].split(',') - vals = [self._parse_value(v) for v in vals] - if kind == 'rgba' and len(vals) == 4: - return QColor.fromRgb(*vals) - elif kind == 'rgb' and len(vals) == 3: - return QColor.fromRgb(*vals) - elif kind == 'hsva' and len(vals) == 4: - return QColor.fromHsv(*vals) - elif kind == 'hsv' and len(vals) == 3: - return QColor.fromHsv(*vals) + int_vals = [self._parse_value(v) for v in vals] + if kind == 'rgba' and len(int_vals) == 4: + return QColor.fromRgb(*int_vals) + elif kind == 'rgb' and len(int_vals) == 3: + return QColor.fromRgb(*int_vals) + elif kind == 'hsva' and len(int_vals) == 4: + return QColor.fromHsv(*int_vals) + elif kind == 'hsv' and len(int_vals) == 3: + return QColor.fromHsv(*int_vals) else: raise configexc.ValidationError(value, "must be a valid color") @@ -1009,9 +1062,9 @@ class QssColor(BaseType): under ``Gradient'' """ - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1056,9 +1109,9 @@ class Font(BaseType): )* # 0-inf size/weight/style tags (?P.+) # mandatory font family""", re.VERBOSE) - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1077,9 +1130,9 @@ class FontFamily(Font): """A Qt font family.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1103,9 +1156,10 @@ class QtFont(Font): __doc__ = Font.__doc__ # for src2asciidoc.py - def to_py(self, value): + def to_py(self, value: _StrUnset) -> typing.Union[configutils.Unset, + None, QFont]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1176,18 +1230,19 @@ class Regex(BaseType): _regex_type: The Python type of a regex object. """ - def __init__(self, flags=0, none_ok=False): + def __init__(self, flags: str = None, + none_ok: bool = False) -> None: super().__init__(none_ok) self._regex_type = type(re.compile('')) # Parse flags from configdata.yml - if flags == 0: - self.flags = flags + if flags is None: + self.flags = 0 else: self.flags = functools.reduce( operator.or_, (getattr(re, flag.strip()) for flag in flags.split(' | '))) - def _compile_regex(self, pattern): + def _compile_regex(self, pattern: str) -> typing.Pattern[str]: """Check if the given regex is valid. This is more complicated than it could be since there's a warning on @@ -1206,6 +1261,8 @@ class Regex(BaseType): pattern, "must be a valid regex - recursion depth " "exceeded") + assert recorded_warnings is not None + for w in recorded_warnings: if (issubclass(w.category, DeprecationWarning) and str(w.message).startswith('bad escape')): @@ -1216,10 +1273,13 @@ class Regex(BaseType): return compiled - def to_py(self, value): + def to_py( + self, + value: typing.Union[str, typing.Pattern[str]] + ) -> typing.Union[configutils.Unset, None, typing.Pattern[str]]: """Get a compiled regex from either a string or a regex object.""" self._basic_py_validation(value, (str, self._regex_type)) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1228,12 +1288,14 @@ class Regex(BaseType): else: return value - def to_str(self, value): + def to_str(self, + value: typing.Union[None, str, typing.Pattern[str]]) -> str: if value is None: return '' elif isinstance(value, self._regex_type): return value.pattern else: + assert isinstance(value, str) return value @@ -1244,8 +1306,11 @@ class Dict(BaseType): When setting from a string, pass a json-like dict, e.g. `{"key", "value"}`. """ - def __init__(self, keytype, valtype, *, fixed_keys=None, - required_keys=None, none_ok=False): + def __init__(self, keytype: typing.Union[String, 'Key'], + valtype: BaseType, *, + fixed_keys: typing.Iterable = None, + required_keys: typing.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 # json in to_str() as json converts keys to strings. @@ -1255,7 +1320,7 @@ class Dict(BaseType): self.fixed_keys = fixed_keys self.required_keys = required_keys - def _validate_keys(self, value): + def _validate_keys(self, value: typing.Dict) -> None: if (self.fixed_keys is not None and not set(value.keys()).issubset(self.fixed_keys)): raise configexc.ValidationError( @@ -1266,7 +1331,7 @@ class Dict(BaseType): raise configexc.ValidationError( value, "Required keys {}".format(self.required_keys)) - def from_str(self, value): + def from_str(self, value: str) -> typing.Optional[typing.Dict]: self._basic_str_validation(value) if not value: return None @@ -1281,14 +1346,14 @@ class Dict(BaseType): self.to_py(yaml_val) return yaml_val - def from_obj(self, value): + def from_obj(self, value: typing.Optional[typing.Dict]) -> typing.Dict: 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): + def _fill_fixed_keys(self, value: typing.Dict) -> typing.Dict: """Fill missing fixed keys with a None-value.""" if self.fixed_keys is None: return value @@ -1297,9 +1362,12 @@ class Dict(BaseType): value[key] = self.valtype.to_py(None) return value - def to_py(self, value): + def to_py( + self, + value: typing.Union[typing.Dict, configutils.Unset, None] + ) -> typing.Union[typing.Dict, configutils.Unset]: self._basic_py_validation(value, dict) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return self._fill_fixed_keys({}) @@ -1313,13 +1381,13 @@ class Dict(BaseType): for key, val in value.items()} return self._fill_fixed_keys(d) - def to_str(self, value): + def to_str(self, value: typing.Dict) -> 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, indent=0): + def to_doc(self, value: typing.Dict, indent: int = 0) -> str: if not value: return 'empty' lines = ['\n'] @@ -1337,13 +1405,13 @@ class File(BaseType): """A file on the local filesystem.""" - def __init__(self, required=True, **kwargs): + def __init__(self, required: bool = True, **kwargs: typing.Any) -> None: super().__init__(**kwargs) self.required = required - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1369,9 +1437,9 @@ class Directory(BaseType): """A directory on the local filesystem.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1394,13 +1462,14 @@ class FormatString(BaseType): """A string with placeholders.""" - def __init__(self, fields, none_ok=False): + def __init__(self, fields: typing.Iterable[str], + none_ok: bool = False) -> None: super().__init__(none_ok) self.fields = fields - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1428,39 +1497,46 @@ class ShellCommand(List): _show_valtype = False - def __init__(self, placeholder=False, none_ok=False): + def __init__(self, placeholder: bool = False, + none_ok: bool = False) -> None: super().__init__(valtype=String(), none_ok=none_ok) self.placeholder = placeholder - def to_py(self, value): - value = super().to_py(value) - if value is configutils.UNSET: - return value - elif not value: + def to_py( + self, + value: typing.Union[typing.List, configutils.Unset], + ) -> typing.Union[typing.List, configutils.Unset]: + py_value = super().to_py(value) + if isinstance(py_value, configutils.Unset): + return py_value + elif not py_value: return [] if (self.placeholder and - '{}' not in ' '.join(value) and - '{file}' not in ' '.join(value)): - raise configexc.ValidationError(value, "needs to contain a " + '{}' not in ' '.join(py_value) and + '{file}' not in ' '.join(py_value)): + raise configexc.ValidationError(py_value, "needs to contain a " "{}-placeholder or a " "{file}-placeholder.") - return value + return py_value class Proxy(BaseType): """A proxy URL, or `system`/`none`.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) self.valid_values = ValidValues( ('system', "Use the system wide proxy."), ('none', "Don't use any proxy")) - def to_py(self, value): + def to_py( + self, + value: _StrUnset + ) -> typing.Union[configutils.Unset, None, QNetworkProxy, _SystemProxy]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1474,13 +1550,15 @@ class Proxy(BaseType): else: # If we add a special value to valid_values, we need to handle # it here! + assert self.valid_values is not None assert value not in self.valid_values, value url = QUrl(value) return urlutils.proxy_from_url(url) except (urlutils.InvalidUrlError, urlutils.InvalidProxyTypeError) as e: raise configexc.ValidationError(value, e) - def complete(self): + def complete(self) -> _Completions: + assert self.valid_values is not None out = [] for val in self.valid_values: out.append((val, self.valid_values.descriptions[val])) @@ -1496,9 +1574,9 @@ class SearchEngineUrl(BaseType): """A search engine URL.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1526,9 +1604,9 @@ class FuzzyUrl(BaseType): """A URL which gets interpreted as search if needed.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1544,10 +1622,10 @@ class PaddingValues: """Four padding values.""" - top = attr.ib() - bottom = attr.ib() - left = attr.ib() - right = attr.ib() + top = attr.ib() # type: int + bottom = attr.ib() # type: int + left = attr.ib() # type: int + right = attr.ib() # type: int class Padding(Dict): @@ -1556,15 +1634,18 @@ class Padding(Dict): _show_valtype = False - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(keytype=String(), valtype=Int(minval=0, none_ok=none_ok), fixed_keys=['top', 'bottom', 'left', 'right'], none_ok=none_ok) - def to_py(self, value): + def to_py( # type: ignore + self, + value: typing.Union[configutils.Unset, typing.Dict, None], + ) -> typing.Union[configutils.Unset, PaddingValues]: d = super().to_py(value) - if d is configutils.UNSET: + if isinstance(d, configutils.Unset): return d return PaddingValues(**d) @@ -1574,9 +1655,9 @@ class Encoding(BaseType): """Setting for a python encoding.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1598,7 +1679,7 @@ class Position(MappingType): 'right': QTabWidget.East, } - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues('top', 'bottom', 'left', 'right')) @@ -1614,7 +1695,7 @@ class TextAlignment(MappingType): 'center': Qt.AlignCenter, } - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues('left', 'right', 'center')) @@ -1624,7 +1705,7 @@ class VerticalPosition(String): """The position of the download bar.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok=none_ok) self.valid_values = ValidValues('top', 'bottom') @@ -1633,9 +1714,12 @@ class Url(BaseType): """A URL as a string.""" - def to_py(self, value): + def to_py( + self, + value: _StrUnset + ) -> typing.Union[configutils.Unset, None, QUrl]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1651,9 +1735,9 @@ class SessionName(BaseType): """The name of a session.""" - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1672,7 +1756,7 @@ class SelectOnRemove(MappingType): 'last-used': QTabBar.SelectPreviousTab, } - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__( none_ok, valid_values=ValidValues( @@ -1690,7 +1774,7 @@ class ConfirmQuit(FlagList): # Values that can be combined with commas combinable_values = ('multiple-tabs', 'downloads') - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok) self.valtype.none_ok = none_ok self.valtype.valid_values = ValidValues( @@ -1701,9 +1785,12 @@ class ConfirmQuit(FlagList): "downloads are running"), ('never', "Never show a confirmation.")) - def to_py(self, value): + def to_py( + self, + value: typing.Union[configutils.Unset, typing.List], + ) -> typing.Union[typing.List, configutils.Unset]: values = super().to_py(value) - if values is configutils.UNSET: + if isinstance(values, configutils.Unset): return values elif not values: return [] @@ -1724,7 +1811,7 @@ class NewTabPosition(String): """How new tabs are positioned.""" - def __init__(self, none_ok=False): + def __init__(self, none_ok: bool = False) -> None: super().__init__(none_ok=none_ok) self.valid_values = ValidValues( ('prev', "Before the current tab."), @@ -1740,9 +1827,9 @@ class TimestampTemplate(BaseType): See https://sqlite.org/lang_datefunc.html for reference. """ - def to_py(self, value): + def to_py(self, value: _StrUnset) -> _StrUnsetNone: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1762,13 +1849,16 @@ class Key(BaseType): """A name of a key.""" - def from_obj(self, value): + def from_obj(self, value: str) -> str: """Make sure key sequences are always normalized.""" return str(keyutils.KeySequence.parse(value)) - def to_py(self, value): + def to_py( + self, + value: _StrUnset + ) -> typing.Union[configutils.Unset, None, keyutils.KeySequence]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None @@ -1787,9 +1877,12 @@ class UrlPattern(BaseType): syntax. """ - def to_py(self, value): + def to_py( + self, + value: _StrUnset + ) -> typing.Union[configutils.Unset, None, urlmatch.UrlPattern]: self._basic_py_validation(value, str) - if value is configutils.UNSET: + if isinstance(value, configutils.Unset): return value elif not value: return None diff --git a/qutebrowser/config/configutils.py b/qutebrowser/config/configutils.py index efdc5611e..47cac4bff 100644 --- a/qutebrowser/config/configutils.py +++ b/qutebrowser/config/configutils.py @@ -35,7 +35,7 @@ if MYPY: from qutebrowser.config import configdata -class _UnsetObject: +class Unset: """Sentinel object.""" @@ -45,7 +45,7 @@ class _UnsetObject: return '' -UNSET = _UnsetObject() +UNSET = Unset() @attr.s diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index a9a61e952..76bdb9199 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -1542,7 +1542,7 @@ class TestRegex: regex.to_py('foo') @pytest.mark.parametrize('flags, expected', [ - (0, 0), + (None, 0), ('IGNORECASE', re.IGNORECASE), ('IGNORECASE | VERBOSE', re.IGNORECASE | re.VERBOSE), ]) diff --git a/tests/unit/config/test_configutils.py b/tests/unit/config/test_configutils.py index 587a0bd68..e8a7bfb38 100644 --- a/tests/unit/config/test_configutils.py +++ b/tests/unit/config/test_configutils.py @@ -26,7 +26,7 @@ from qutebrowser.utils import urlmatch def test_unset_object_identity(): - assert configutils._UnsetObject() is not configutils._UnsetObject() + assert configutils.Unset() is not configutils.Unset() assert configutils.UNSET is configutils.UNSET