diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 863ecd5c5..c4257d0cf 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -419,7 +419,7 @@ class ConfigManager(QObject): for optname, option in sect.items(): lines.append('#') - typestr = ' ({})'.format(option.typ.__class__.__name__) + typestr = ' ({})'.format(option.typ.get_name()) lines.append("# {}{}:".format(optname, typestr)) try: @@ -430,7 +430,7 @@ class ConfigManager(QObject): continue for descline in desc.splitlines(): lines += wrapper.wrap(descline) - valid_values = option.typ.valid_values + valid_values = option.typ.get_valid_values() if valid_values is not None: if valid_values.descriptions: for val in valid_values: diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 4898c7ab1..447624a8c 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -135,7 +135,7 @@ def data(readonly=False): "Whether to find text on a page case-insensitively."), ('startpage', - SettingValue(typ.List(), 'https://duckduckgo.com'), + SettingValue(typ.List(typ.String()), 'https://duckduckgo.com'), "The default page(s) to open at the start, separated by commas."), ('default-page', @@ -254,7 +254,7 @@ def data(readonly=False): ('ui', sect.KeyValue( ('zoom-levels', - SettingValue(typ.PercList(minval=0), + SettingValue(typ.List(typ.Perc(minval=0)), '25%,33%,50%,67%,75%,90%,100%,110%,125%,150%,175%,' '200%,250%,300%,400%,500%'), "The available zoom levels, separated by commas."), @@ -352,7 +352,7 @@ def data(readonly=False): "(requires restart)"), ('keyhint-blacklist', - SettingValue(typ.List(none_ok=True), ''), + SettingValue(typ.List(typ.String(), none_ok=True), ''), "Keychains that shouldn't be shown in the keyhint dialog\n\n" "Globs are supported, so ';*' will blacklist all keychains" "starting with ';'. Use '*' to disable keyhints"), @@ -684,8 +684,8 @@ def data(readonly=False): ('object-cache-capacities', SettingValue( - typ.WebKitBytesList(length=3, maxsize=MAXVALS['int'], - none_ok=True), ''), + typ.List(typ.WebKitBytes(maxsize=MAXVALS['int'], + none_ok=True), none_ok=True, length=3), ''), "The capacities for the global memory cache for dead objects " "such as stylesheets or scripts. Syntax: cacheMinDeadCapacity, " "cacheMaxDead, totalCapacity.\n\n" @@ -826,7 +826,7 @@ def data(readonly=False): ('host-block-lists', SettingValue( - typ.UrlList(none_ok=True), + typ.List(typ.Url(), none_ok=True), 'http://www.malwaredomainlist.com/hostslist/hosts.txt,' 'http://someonewhocares.org/hosts/hosts,' 'http://winhelp2002.mvps.org/hosts.zip,' @@ -845,7 +845,7 @@ def data(readonly=False): "Whether host blocking is enabled."), ('host-blocking-whitelist', - SettingValue(typ.List(none_ok=True), 'piwik.org'), + SettingValue(typ.List(typ.String(), none_ok=True), 'piwik.org'), "List of domains that should always be loaded, despite being " "ad-blocked.\n\n" "Domains may contain * and ? wildcards and are otherwise " @@ -916,13 +916,13 @@ def data(readonly=False): "auto-follow."), ('next-regexes', - SettingValue(typ.RegexList(flags=re.IGNORECASE), + SettingValue(typ.List(typ.Regex(flags=re.IGNORECASE)), r'\bnext\b,\bmore\b,\bnewer\b,\b[>→≫]\b,\b(>>|»)\b,' r'\bcontinue\b'), "A comma-separated list of regexes to use for 'next' links."), ('prev-regexes', - SettingValue(typ.RegexList(flags=re.IGNORECASE), + SettingValue(typ.List(typ.Regex(flags=re.IGNORECASE)), r'\bprev(ious)?\b,\bback\b,\bolder\b,\b[<←≪]\b,' r'\b(<<|«)\b'), "A comma-separated list of regexes to use for 'prev' links."), diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index de1bbdf27..398a5c9bc 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -124,6 +124,14 @@ class BaseType: self.none_ok = none_ok self.valid_values = None + def get_name(self): + """Get a name for the type for documentation""" + return self.__class__.__name__ + + def get_valid_values(self): + """Get the type's valid values for documentation""" + return self.valid_values + def _basic_validation(self, value): """Do some basic validation for the value (empty, non-printable chars). @@ -303,23 +311,39 @@ class List(BaseType): """Base class for a (string-)list setting.""" - def __init__(self, none_ok=False, valid_values=None): + _show_inner_type = True + + def __init__(self, inner_type, none_ok=False, length=None): super().__init__(none_ok) - self.valid_values = valid_values + self.inner_type = inner_type + self.length = length + + def get_name(self): + name = super().get_name() + if self._show_inner_type: + name += " of " + self.inner_type.get_name() + return name + + def get_valid_values(self): + return self.inner_type.get_valid_values() def transform(self, value): if not value: return None else: - return [v if v else None for v in value.split(',')] + return [self.inner_type.transform(v.strip()) + for v in value.split(',')] def validate(self, value): self._basic_validation(value) if not value: return - vals = self.transform(value) - if None in vals: - raise configexc.ValidationError(value, "items may not be empty!") + vals = value.split(',') + if self.length is not None and len(vals) != self.length: + raise configexc.ValidationError(value, "Exactly {} values need to " + "be set!".format(self.length)) + for val in vals: + self.inner_type.validate(val.strip()) class FlagList(List): @@ -332,41 +356,40 @@ class FlagList(List): combinable_values = None + _show_inner_type = False + + def __init__(self, none_ok=False, valid_values=None): + super().__init__(BaseType(), none_ok) + self.inner_type.valid_values = valid_values + def validate(self, value): - self._basic_validation(value) + if self.inner_type.valid_values is not None: + super().validate(value) + else: + self._basic_validation(value) if not value: return - - vals = self.transform(value) - if None in vals and not self.none_ok: - raise configexc.ValidationError( - value, "May not contain empty values!") + vals = super().transform(value) # Check for duplicate values if len(set(vals)) != len(vals): raise configexc.ValidationError( value, "List contains duplicate values!") - # Check if each value is valid, ignores None values - set_vals = set(val for val in vals if val) - if (self.valid_values is not None and - not set_vals.issubset(set(self.valid_values))): - raise configexc.ValidationError( - value, "List contains invalid values!") - def complete(self): - if self.valid_values is None: + valid_values = self.inner_type.valid_values + if valid_values is None: return None out = [] # Single value completions - for value in self.valid_values: - desc = self.valid_values.descriptions.get(value, "") + for value in valid_values: + desc = valid_values.descriptions.get(value, "") out.append((value, desc)) combinables = self.combinable_values if combinables is None: - combinables = list(self.valid_values) + combinables = list(valid_values) # Generate combinations of each possible value combination for size in range(2, len(combinables) + 1): for combination in itertools.combinations(combinables, size): @@ -456,29 +479,6 @@ class Int(BaseType): "smaller!".format(self.maxval)) -class IntList(List): - - """Base class for an int-list setting.""" - - def transform(self, value): - if not value: - return None - vals = super().transform(value) - return [int(v) if v is not None else None for v in vals] - - def validate(self, value): - self._basic_validation(value) - if not value: - return - try: - vals = self.transform(value) - except ValueError: - raise configexc.ValidationError(value, "must be a list of " - "integers!") - if None in vals and not self.none_ok: - raise configexc.ValidationError(value, "items may not be empty!") - - class Float(BaseType): """Base class for a float setting. @@ -559,50 +559,6 @@ class Perc(BaseType): "less!".format(self.maxval)) -class PercList(List): - - """Base class for a list of percentages. - - Attributes: - minval: Minimum value (inclusive). - maxval: Maximum value (inclusive). - """ - - def __init__(self, minval=None, maxval=None, none_ok=False): - super().__init__(none_ok) - if maxval is not None and minval is not None and maxval < minval: - raise ValueError("minval ({}) needs to be <= maxval ({})!".format( - minval, maxval)) - self.minval = minval - self.maxval = maxval - - def transform(self, value): - if not value: - return None - vals = super().transform(value) - return [int(v[:-1]) if v is not None else None for v in vals] - - def validate(self, value): - self._basic_validation(value) - if not value: - return - vals = super().transform(value) - perctype = Perc(minval=self.minval, maxval=self.maxval) - try: - for val in vals: - if val is None: - if self.none_ok: - continue - else: - raise configexc.ValidationError(value, "items may not " - "be empty!") - else: - perctype.validate(val) - except configexc.ValidationError: - raise configexc.ValidationError(value, "must be a list of " - "percentages!") - - class PercOrInt(BaseType): """Percentage or integer. @@ -885,36 +841,6 @@ class Regex(BaseType): return re.compile(value, self.flags) -class RegexList(List): - - """A list of regexes.""" - - def __init__(self, flags=0, none_ok=False): - super().__init__(none_ok) - self.flags = flags - - def transform(self, value): - if not value: - return None - vals = super().transform(value) - return [re.compile(v, self.flags) if v is not None else None - for v in vals] - - def validate(self, value): - self._basic_validation(value) - if not value: - return - vals = super().transform(value) - - for val in vals: - if val is None: - if not self.none_ok: - raise configexc.ValidationError( - value, "items may not be empty!") - else: - _validate_regex(val, self.flags) - - class File(BaseType): """A file on the local filesystem.""" @@ -1061,39 +987,6 @@ class WebKitBytes(BaseType): return int(val) * multiplicator -class WebKitBytesList(List): - - """A size with an optional suffix. - - Attributes: - length: The length of the list. - bytestype: The webkit bytes type. - """ - - def __init__(self, maxsize=None, length=None, none_ok=False): - super().__init__(none_ok) - self.length = length - self.bytestype = WebKitBytes(maxsize, none_ok=none_ok) - - def transform(self, value): - if value == '': - return None - else: - vals = super().transform(value) - return [self.bytestype.transform(val) for val in vals] - - def validate(self, value): - self._basic_validation(value) - if not value: - return - vals = super().transform(value) - for val in vals: - self.bytestype.validate(val) - if self.length is not None and len(vals) != self.length: - raise configexc.ValidationError(value, "exactly {} values need to " - "be set!".format(self.length)) - - class ShellCommand(BaseType): """A shellcommand which is split via shlex. @@ -1243,25 +1136,16 @@ PaddingValues = collections.namedtuple('PaddingValues', ['top', 'bottom', 'left', 'right']) -class Padding(IntList): +class Padding(List): """Setting for paddings around elements.""" - def validate(self, value): - self._basic_validation(value) - if not value: - return - try: - vals = self.transform(value) - except (ValueError, TypeError): - raise configexc.ValidationError(value, "must be a list of 4 " - "integers!") - if None in vals and not self.none_ok: - raise configexc.ValidationError(value, "items may not be empty!") - elems = self.transform(value) - if any(e is not None and e < 0 for e in elems): - raise configexc.ValidationError(value, "Values need to be " - "positive!") + _show_inner_type = False + + def __init__(self, none_ok=False, valid_values=None): + super().__init__(Int(minval=0, none_ok=none_ok), + none_ok=none_ok, length=4) + self.inner_type.valid_values = valid_values def transform(self, value): elems = super().transform(value) @@ -1401,29 +1285,24 @@ class VerticalPosition(BaseType): self.valid_values = ValidValues('top', 'bottom') -class UrlList(List): +class Url(BaseType): - """A list of URLs.""" + """A URL.""" def transform(self, value): if not value: return None else: - return [QUrl.fromUserInput(v) if v else None - for v in value.split(',')] + return QUrl.fromUserInput(value) def validate(self, value): self._basic_validation(value) if not value: return - vals = self.transform(value) - for val in vals: - if val is None: - raise configexc.ValidationError(value, "values may not be " - "empty!") - elif not val.isValid(): - raise configexc.ValidationError(value, "invalid URL - " - "{}".format(val.errorString())) + val = self.transform(value) + if not val.isValid(): + raise configexc.ValidationError(value, "invalid URL - " + "{}".format(val.errorString())) class HeaderDict(BaseType): @@ -1519,7 +1398,8 @@ class ConfirmQuit(FlagList): def __init__(self, none_ok=False): super().__init__(none_ok) - self.valid_values = ValidValues( + self.inner_type.none_ok = none_ok + self.inner_type.valid_values = ValidValues( ('always', "Always show a confirmation."), ('multiple-tabs', "Show a confirmation if " "multiple tabs are opened."), diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py index d8e36303b..543a21855 100755 --- a/scripts/dev/src2asciidoc.py +++ b/scripts/dev/src2asciidoc.py @@ -357,7 +357,7 @@ def _generate_setting_section(f, sectname, sect): f.write("=== {}".format(optname) + "\n") f.write(sect.descriptions[optname] + "\n") f.write("\n") - valid_values = option.typ.valid_values + valid_values = option.typ.get_valid_values() if valid_values is not None: f.write("Valid values:\n") f.write("\n") diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index 38958be5d..391ae757a 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -354,18 +354,31 @@ class TestString: assert klass(valid_values=valid_values).complete() == expected +class ListSubclass(configtypes.List): + + """A subclass of List which we use in tests. Similar to FlagList. + + Valid values are 'foo', 'bar' and 'baz'. + """ + + def __init__(self, none_ok_inner=False, none_ok_outer=False, length=None): + super().__init__(configtypes.BaseType(none_ok_inner), + none_ok=none_ok_outer, length=length) + self.inner_type.valid_values = configtypes.ValidValues('foo', + 'bar', 'baz') + + class TestList: """Test List.""" @pytest.fixture def klass(self): - return configtypes.List + return ListSubclass - @pytest.mark.parametrize('val', - ['', 'foo', 'foo,bar', 'foo, bar']) + @pytest.mark.parametrize('val', ['', 'foo', 'foo,bar', 'foo, bar']) def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) + klass(none_ok_outer=True).validate(val) @pytest.mark.parametrize('val', ['', 'foo,,bar']) def test_validate_invalid(self, klass, val): @@ -374,14 +387,24 @@ class TestList: def test_invalid_empty_value_none_ok(self, klass): with pytest.raises(configexc.ValidationError): - klass(none_ok=True).validate('foo,,bar') + klass(none_ok_outer=True).validate('foo,,bar') + with pytest.raises(configexc.ValidationError): + klass(none_ok_inner=True).validate('') + + @pytest.mark.parametrize('val', ['', 'foo,bar', 'foo, bar']) + def test_validate_length(self, klass, val): + klass(none_ok_outer=True, length=2).validate(val) + + @pytest.mark.parametrize('val', ['bar', 'foo,bar', 'foo,bar,foo,bar']) + def test_wrong_length(self, klass, val): + with pytest.raises(configexc.ValidationError): + klass(length=3).validate(val) @pytest.mark.parametrize('val, expected', [ ('foo', ['foo']), ('foo,bar,baz', ['foo', 'bar', 'baz']), ('', None), - # Not implemented yet - pytest.mark.xfail(('foo, bar', ['foo', 'bar'])), + ('foo, bar', ['foo', 'bar']) ]) def test_transform(self, klass, val, expected): assert klass().transform(val) == expected @@ -398,7 +421,9 @@ class FlagListSubclass(configtypes.FlagList): def __init__(self, none_ok=False): super().__init__(none_ok) - self.valid_values = configtypes.ValidValues('foo', 'bar', 'baz') + self.inner_type.valid_values = configtypes.ValidValues('foo', + 'bar', 'baz') + self.inner_type.none_ok = none_ok class TestFlagList: @@ -580,37 +605,6 @@ class TestInt: assert klass(none_ok=True).transform(val) == expected -class TestIntList: - - """Test IntList.""" - - @pytest.fixture - def klass(self): - return configtypes.IntList - - @pytest.mark.parametrize('val', ['', '1,2', '1', '23,1337']) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', ['', '1,,2', '23,foo,1337']) - def test_validate_invalid(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass().validate(val) - - def test_invalid_empty_value_none_ok(self, klass): - klass(none_ok=True).validate('1,,2') - - @pytest.mark.parametrize('val, expected', [ - ('1', [1]), - ('23,42', [23, 42]), - ('', None), - ('1,,2', [1, None, 2]), - ('23, 42', [23, 42]), - ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - class TestFloat: """Test Float.""" @@ -703,52 +697,6 @@ class TestPerc: assert klass().transform(val) == expected -class TestPercList: - - """Test PercList.""" - - @pytest.fixture - def klass(self): - return configtypes.PercList - - def test_minval_gt_maxval(self, klass): - with pytest.raises(ValueError): - klass(minval=2, maxval=1) - - @pytest.mark.parametrize('kwargs, val', [ - ({}, '23%,42%,1337%'), - ({'minval': 2}, '2%,3%'), - ({'maxval': 2}, '1%,2%'), - ({'minval': 2, 'maxval': 3}, '2%,3%'), - ({'none_ok': True}, '42%,,23%'), - ({'none_ok': True}, ''), - ]) - def test_validate_valid(self, klass, kwargs, val): - klass(**kwargs).validate(val) - - @pytest.mark.parametrize('kwargs, val', [ - ({}, '23%,42,1337%'), - ({'minval': 2}, '1%,2%'), - ({'maxval': 2}, '2%,3%'), - ({'minval': 2, 'maxval': 3}, '1%,2%'), - ({'minval': 2, 'maxval': 3}, '3%,4%'), - ({}, '42%,,23%'), - ({}, ''), - ]) - def test_validate_invalid(self, klass, kwargs, val): - with pytest.raises(configexc.ValidationError): - klass(**kwargs).validate(val) - - @pytest.mark.parametrize('val, expected', [ - ('', None), - ('1337%', [1337]), - ('23%,42%,1337%', [23, 42, 1337]), - ('23%,,42%', [23, None, 42]), - ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - class TestPercOrInt: """Test PercOrInt.""" @@ -1224,59 +1172,6 @@ class TestRegex: klass().validate('foo') -class TestRegexList: - - """Test RegexList.""" - - @pytest.fixture - def klass(self): - return configtypes.RegexList - - @pytest.mark.parametrize('val', [ - r'(foo|bar),[abcd]?,1337{42}', - r'(foo|bar),,1337{42}', - r'', - ]) - def test_validate_valid(self, klass, val): - klass(none_ok=True).validate(val) - - @pytest.mark.parametrize('val', [ - r'(foo|bar),,1337{42}', - r'', - r'(foo|bar),((),1337{42}', - r'(' * 500, - ], ids=['empty value', 'empty', 'unmatched parens', 'too many parens']) - def test_validate_invalid(self, klass, val): - with pytest.raises(configexc.ValidationError): - klass().validate(val) - - @pytest.mark.parametrize('val', [ - r'foo\Xbar', - r'foo\Cbar', - ]) - def test_validate_maybe_valid(self, klass, val): - """Those values are valid on some Python versions (and systems?). - - On others, they raise a DeprecationWarning because of an invalid - escape. This tests makes sure this gets translated to a - ValidationError. - """ - try: - klass().validate(val) - except configexc.ValidationError: - pass - - @pytest.mark.parametrize('val, expected', [ - ('foo', [RegexEq('foo')]), - ('foo,bar,baz', [RegexEq('foo'), RegexEq('bar'), - RegexEq('baz')]), - ('foo,,bar', [RegexEq('foo'), None, RegexEq('bar')]), - ('', None), - ]) - def test_transform(self, klass, val, expected): - assert klass().transform(val) == expected - - def unrequired_class(**kwargs): return configtypes.File(required=False, **kwargs) @@ -1552,49 +1447,6 @@ class TestWebKitByte: assert klass().transform(val) == expected -class TestWebKitBytesList: - - """Test WebKitBytesList.""" - - @pytest.fixture - def klass(self): - return configtypes.WebKitBytesList - - @pytest.mark.parametrize('kwargs, val', [ - ({}, '23,56k,1337'), - ({'maxsize': 2}, '2'), - ({'maxsize': 2048}, '2k'), - ({'length': 3}, '1,2,3'), - ({'none_ok': True}, '23,,42'), - ({'none_ok': True}, ''), - ]) - def test_validate_valid(self, klass, kwargs, val): - klass(**kwargs).validate(val) - - @pytest.mark.parametrize('kwargs, val', [ - ({}, '23,56kk,1337'), - ({'maxsize': 2}, '3'), - ({'maxsize': 2}, '3k'), - ({}, '23,,42'), - ({'length': 3}, '1,2'), - ({'length': 3}, '1,2,3,4'), - ({}, '23,,42'), - ({}, ''), - ]) - def test_validate_invalid(self, klass, kwargs, val): - with pytest.raises(configexc.ValidationError): - klass(**kwargs).validate(val) - - @pytest.mark.parametrize('val, expected', [ - ('1k', [1024]), - ('23,2k,1337', [23, 2048, 1337]), - ('23,,42', [23, None, 42]), - ('', None), - ]) - def test_transform_single(self, klass, val, expected): - assert klass().transform(val) == expected - - class TestShellCommand: """Test ShellCommand.""" @@ -1961,39 +1813,29 @@ class TestEncoding: assert klass().transform(val) == expected -class TestUrlList: +class TestUrl: - """Test UrlList.""" + """Test Url.""" TESTS = { - 'http://qutebrowser.org/': [QUrl('http://qutebrowser.org/')], - 'http://qutebrowser.org/,http://heise.de/': - [QUrl('http://qutebrowser.org/'), QUrl('http://heise.de/')], + 'http://qutebrowser.org/': QUrl('http://qutebrowser.org/'), + 'http://heise.de/': QUrl('http://heise.de/'), '': None, } @pytest.fixture def klass(self): - return configtypes.UrlList + return configtypes.Url @pytest.mark.parametrize('val', sorted(TESTS)) def test_validate_valid(self, klass, val): klass(none_ok=True).validate(val) - @pytest.mark.parametrize('val', [ - '', - 'foo,,bar', - '+', # invalid URL with QUrl.fromUserInput - ]) + @pytest.mark.parametrize('val', ['', '+']) def test_validate_invalid(self, klass, val): with pytest.raises(configexc.ValidationError): klass().validate(val) - def test_validate_empty_item(self, klass): - """Test validate with empty item and none_ok = False.""" - with pytest.raises(configexc.ValidationError): - klass().validate('foo,,bar') - @pytest.mark.parametrize('val, expected', sorted(TESTS.items())) def test_transform_single(self, klass, val, expected): assert klass().transform(val) == expected diff --git a/tests/unit/config/test_configtypes_hypothesis.py b/tests/unit/config/test_configtypes_hypothesis.py index c5a5b9ac0..329d7357c 100644 --- a/tests/unit/config/test_configtypes_hypothesis.py +++ b/tests/unit/config/test_configtypes_hypothesis.py @@ -36,6 +36,9 @@ def gen_classes(): pass elif member is configtypes.MappingType: pass + elif member is configtypes.List: + yield functools.partial(member, inner_type=configtypes.Int()) + yield functools.partial(member, inner_type=configtypes.Url()) elif member is configtypes.FormatString: yield functools.partial(member, fields=['a', 'b']) elif issubclass(member, configtypes.BaseType):