Repository: qutebrowser/qutebrowser

Issue:
Handle config types more generically
get rid of unneeded specialized config types, but pass valid_values on construction instead



Discussion:

Resolving patch:
diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py
--- a/qutebrowser/config/configdata.py
+++ b/qutebrowser/config/configdata.py
@@ -209,7 +209,22 @@ def data(readonly=False):
              "be used."),
 
             ('new-instance-open-target',
-             SettingValue(typ.NewInstanceOpenTarget(), 'tab'),
+             SettingValue(typ.String(
+                 valid_values=typ.ValidValues(
+                     ('tab', "Open a new tab in the existing "
+                      "window and activate the window."),
+                     ('tab-bg', "Open a new background tab in the "
+                      "existing window and activate the "
+                      "window."),
+                     ('tab-silent', "Open a new tab in the existing "
+                      "window without activating "
+                      "the window."),
+                     ('tab-bg-silent', "Open a new background tab "
+                      "in the existing window "
+                      "without activating the "
+                      "window."),
+                     ('window', "Open in a new window.")
+                 )), 'tab'),
              "How to open links in an existing instance if a new one is "
              "launched."),
 
@@ -227,7 +242,10 @@ def data(readonly=False):
              "last loaded session."),
 
             ('url-incdec-segments',
-             SettingValue(typ.URLSegmentList(), 'path,query'),
+             SettingValue(
+                 typ.FlagList(valid_values=typ.ValidValues(
+                     'host', 'path', 'query', 'anchor')),
+                 'path,query'),
              "The URL segments where `:navigate increment/decrement` will "
              "search for a number."),
 
@@ -345,7 +363,15 @@ def data(readonly=False):
              "Value to send in the `accept-language` header."),
 
             ('referer-header',
-             SettingValue(typ.Referer(), 'same-domain'),
+             SettingValue(typ.String(
+                 valid_values=typ.ValidValues(
+                     ('always', "Always send."),
+                     ('never', "Never send; this is not recommended,"
+                      " as some sites may break."),
+                     ('same-domain', "Only send for the same domain."
+                      " This will still protect your privacy, but"
+                      " shouldn't break any sites.")
+                 )), 'same-domain'),
              "Send the Referer header"),
 
             ('user-agent',
@@ -379,7 +405,12 @@ def data(readonly=False):
              "Automatically open completion when typing."),
 
             ('download-path-suggestion',
-             SettingValue(typ.DownloadPathSuggestion(), 'path'),
+             SettingValue(
+                 typ.String(valid_values=typ.ValidValues(
+                     ('path', "Show only the download path."),
+                     ('filename', "Show only download filename."),
+                     ('both', "Show download path and filename."))),
+                 'path'),
              "What to display in the download filename input."),
 
             ('timestamp-format',
@@ -452,7 +483,13 @@ def data(readonly=False):
              "element is focused after page load."),
 
             ('forward-unbound-keys',
-             SettingValue(typ.ForwardUnboundKeys(), 'auto'),
+             SettingValue(typ.String(
+                 valid_values=typ.ValidValues(
+                     ('all', "Forward all unbound keys."),
+                     ('auto', "Forward unbound non-alphanumeric "
+                      "keys."),
+                     ('none', "Don't forward any keys.")
+                 )), 'auto'),
              "Whether to forward unbound keys to the webview in normal mode."),
 
             ('spatial-navigation',
@@ -502,11 +539,26 @@ def data(readonly=False):
              "How new tabs opened explicitly are positioned."),
 
             ('last-close',
-             SettingValue(typ.LastClose(), 'ignore'),
+             SettingValue(typ.String(
+                 valid_values=typ.ValidValues(
+                     ('ignore', "Don't do anything."),
+                     ('blank', "Load a blank page."),
+                     ('startpage', "Load the start page."),
+                     ('default-page', "Load the default page."),
+                     ('close', "Close the window.")
+                 )), 'ignore'),
              "Behavior when the last tab is closed."),
 
             ('show',
-             SettingValue(typ.TabBarShow(), 'always'),
+             SettingValue(
+                 typ.String(valid_values=typ.ValidValues(
+                     ('always', "Always show the tab bar."),
+                     ('never', "Always hide the tab bar."),
+                     ('multiple', "Hide the tab bar if only one tab "
+                      "is open."),
+                     ('switching', "Show the tab bar when switching "
+                      "tabs.")
+                 )), 'always'),
              "When to show the tab bar"),
 
             ('show-switching-delay',
@@ -523,7 +575,12 @@ def data(readonly=False):
              "Whether tabs should be movable."),
 
             ('close-mouse-button',
-             SettingValue(typ.CloseButton(), 'middle'),
+             SettingValue(typ.String(
+                 valid_values=typ.ValidValues(
+                     ('right', "Close tabs on right-click."),
+                     ('middle', "Close tabs on middle-click."),
+                     ('none', "Don't close tabs using the mouse.")
+                 )), 'middle'),
              "On which mouse button to close tabs."),
 
             ('position',
@@ -731,7 +788,16 @@ def data(readonly=False):
              "local urls."),
 
             ('cookies-accept',
-             SettingValue(typ.AcceptCookies(), 'no-3rdparty'),
+             SettingValue(typ.String(
+                 valid_values=typ.ValidValues(
+                     ('all', "Accept all cookies."),
+                     ('no-3rdparty', "Accept cookies from the same"
+                      " origin only."),
+                     ('no-unknown-3rdparty', "Accept cookies from "
+                      "the same origin only, unless a cookie is "
+                      "already set for the domain."),
+                     ('never', "Don't accept cookies at all.")
+                 )), 'no-3rdparty'),
              "Control which cookies to accept."),
 
             ('cookies-store',
@@ -779,7 +845,12 @@ def data(readonly=False):
              "Opacity for hints."),
 
             ('mode',
-             SettingValue(typ.HintMode(), 'letter'),
+             SettingValue(typ.String(
+                 valid_values=typ.ValidValues(
+                     ('number', "Use numeric hints."),
+                     ('letter', "Use the chars in the hints -> "
+                      "chars setting.")
+                 )), 'letter'),
              "Mode to use for hints."),
 
             ('chars',
diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py
--- a/qutebrowser/config/configtypes.py
+++ b/qutebrowser/config/configtypes.py
@@ -121,11 +121,11 @@ class BaseType:
                  mentioned in the config file.
     """
 
-    valid_values = None
     special = False
 
     def __init__(self, none_ok=False):
         self.none_ok = none_ok
+        self.valid_values = None
 
     def _basic_validation(self, value):
         """Do some basic validation for the value (empty, non-printable chars).
@@ -216,11 +216,10 @@ class MappingType(BaseType):
 
     MAPPING = {}
 
-    def __init__(self, none_ok=False):
+    def __init__(self, none_ok=False,
+                 valid_values=None):
         super().__init__(none_ok)
-        if list(sorted(self.MAPPING)) != list(sorted(self.valid_values)):
-            raise ValueError("Mapping {!r} doesn't match valid values "
-                             "{!r}".format(self.MAPPING, self.valid_values))
+        self.valid_values = valid_values
 
     def validate(self, value):
         super().validate(value.lower())
@@ -243,8 +242,10 @@ class String(BaseType):
     """
 
     def __init__(self, minlen=None, maxlen=None, forbidden=None,
-                 none_ok=False, completions=None):
+                 none_ok=False, completions=None, valid_values=None):
         super().__init__(none_ok)
+        self.valid_values = valid_values
+
         if minlen is not None and minlen < 1:
             raise ValueError("minlen ({}) needs to be >= 1!".format(minlen))
         elif maxlen is not None and maxlen < 1:
@@ -280,6 +281,10 @@ class List(BaseType):
 
     """Base class for a (string-)list setting."""
 
+    def __init__(self, none_ok=False, valid_values=None):
+        super().__init__(none_ok)
+        self.valid_values = valid_values
+
     def transform(self, value):
         if not value:
             return None
@@ -351,7 +356,9 @@ class Bool(BaseType):
 
     """Base class for a boolean setting."""
 
-    valid_values = ValidValues('true', 'false')
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = ValidValues('true', 'false')
 
     def transform(self, value):
         if not value:
@@ -371,7 +378,9 @@ class BoolAsk(Bool):
 
     """A yes/no/ask question."""
 
-    valid_values = ValidValues('true', 'false', 'ask')
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = ValidValues('true', 'false', 'ask')
 
     def transform(self, value):
         if value.lower() == 'ask':
@@ -650,10 +659,15 @@ class ColorSystem(MappingType):
     """Color systems for interpolation."""
 
     special = True
-    valid_values = ValidValues(('rgb', "Interpolate in the RGB color system."),
-                               ('hsv', "Interpolate in the HSV color system."),
-                               ('hsl', "Interpolate in the HSL color system."),
-                               ('none', "Don't show a gradient."))
+
+    def __init__(self, none_ok=False):
+        super().__init__(
+            none_ok,
+            valid_values=ValidValues(
+                ('rgb', "Interpolate in the RGB color system."),
+                ('hsv', "Interpolate in the HSV color system."),
+                ('hsl', "Interpolate in the HSL color system."),
+                ('none', "Don't show a gradient.")))
 
     MAPPING = {
         'rgb': QColor.Rgb,
@@ -1087,23 +1101,11 @@ def transform(self, value):
             return shlex.split(value)
 
 
-class HintMode(BaseType):
-
-    """Base class for the hints -> mode setting."""
-
-    special = True
-    valid_values = ValidValues(('number', "Use numeric hints."),
-                               ('letter', "Use the chars in the hints -> "
-                                          "chars setting."))
-
-
 class Proxy(BaseType):
 
     """A proxy URL or special value."""
 
     special = True
-    valid_values = ValidValues(('system', "Use the system wide proxy."),
-                               ('none', "Don't use any proxy"))
 
     PROXY_TYPES = {
         'http': QNetworkProxy.HttpProxy,
@@ -1111,6 +1113,12 @@ class Proxy(BaseType):
         'socks5': QNetworkProxy.Socks5Proxy,
     }
 
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = ValidValues(
+            ('system', "Use the system wide proxy."),
+            ('none', "Don't use any proxy"))
+
     def validate(self, value):
         self._basic_validation(value)
         if not value:
@@ -1297,13 +1305,14 @@ class AutoSearch(BaseType):
     """Whether to start a search when something else than a URL is entered."""
 
     special = True
-    valid_values = ValidValues(('naive', "Use simple/naive check."),
-                               ('dns', "Use DNS requests (might be slow!)."),
-                               ('false', "Never search automatically."))
 
     def __init__(self, none_ok=False):
         super().__init__(none_ok)
         self.booltype = Bool(none_ok=none_ok)
+        self.valid_values = ValidValues(
+            ('naive', "Use simple/naive check."),
+            ('dns', "Use DNS requests (might be slow!)."),
+            ('false', "Never search automatically."))
 
     def validate(self, value):
         self._basic_validation(value)
@@ -1330,8 +1339,6 @@ class Position(MappingType):
 
     """The position of the tab bar."""
 
-    valid_values = ValidValues('top', 'bottom', 'left', 'right')
-
     MAPPING = {
         'top': QTabWidget.North,
         'bottom': QTabWidget.South,
@@ -1339,12 +1346,19 @@ class Position(MappingType):
         'right': QTabWidget.East,
     }
 
+    def __init__(self, none_ok=False):
+        super().__init__(
+            none_ok,
+            valid_values=ValidValues('top', 'bottom', 'left', 'right'))
+
 
 class VerticalPosition(BaseType):
 
     """The position of the download bar."""
 
-    valid_values = ValidValues('top', 'bottom')
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = ValidValues('top', 'bottom')
 
 
 class UrlList(List):
@@ -1389,10 +1403,6 @@ class SelectOnRemove(MappingType):
     """Which tab to select when the focused tab is removed."""
 
     special = True
-    valid_values = ValidValues(
-        ('left', "Select the tab on the left."),
-        ('right', "Select the tab on the right."),
-        ('previous', "Select the previously selected tab."))
 
     MAPPING = {
         'left': QTabBar.SelectLeftTab,
@@ -1400,31 +1410,13 @@ class SelectOnRemove(MappingType):
         'previous': QTabBar.SelectPreviousTab,
     }
 
-
-class LastClose(BaseType):
-
-    """Behavior when the last tab is closed."""
-
-    special = True
-    valid_values = ValidValues(('ignore', "Don't do anything."),
-                               ('blank', "Load a blank page."),
-                               ('startpage', "Load the start page."),
-                               ('default-page', "Load the default page."),
-                               ('close', "Close the window."))
-
-
-class AcceptCookies(BaseType):
-
-    """Control which cookies to accept."""
-
-    special = True
-    valid_values = ValidValues(('all', "Accept all cookies."),
-                               ('no-3rdparty', "Accept cookies from the same"
-                                " origin only."),
-                               ('no-unknown-3rdparty', "Accept cookies from "
-                                "the same origin only, unless a cookie is "
-                                "already set for the domain."),
-                               ('never', "Don't accept cookies at all."))
+    def __init__(self, none_ok=False):
+        super().__init__(
+            none_ok,
+            valid_values=ValidValues(
+                ('left', "Select the tab on the left."),
+                ('right', "Select the tab on the right."),
+                ('previous', "Select the previously selected tab.")))
 
 
 class ConfirmQuit(FlagList):
@@ -1432,15 +1424,20 @@ class ConfirmQuit(FlagList):
     """Whether to display a confirmation when the window is closed."""
 
     special = True
-    valid_values = ValidValues(('always', "Always show a confirmation."),
-                               ('multiple-tabs', "Show a confirmation if "
-                                                 "multiple tabs are opened."),
-                               ('downloads', "Show a confirmation if "
-                                             "downloads are running"),
-                               ('never', "Never show a confirmation."))
+
     # Values that can be combined with commas
     combinable_values = ('multiple-tabs', 'downloads')
 
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = ValidValues(
+            ('always', "Always show a confirmation."),
+            ('multiple-tabs', "Show a confirmation if "
+             "multiple tabs are opened."),
+            ('downloads', "Show a confirmation if "
+             "downloads are running"),
+            ('never', "Never show a confirmation."))
+
     def validate(self, value):
         super().validate(value)
         if not value:
@@ -1457,36 +1454,19 @@ def validate(self, value):
                 value, "List cannot contain always!")
 
 
-class ForwardUnboundKeys(BaseType):
-
-    """Whether to forward unbound keys."""
-
-    special = True
-    valid_values = ValidValues(('all', "Forward all unbound keys."),
-                               ('auto', "Forward unbound non-alphanumeric "
-                                        "keys."),
-                               ('none', "Don't forward any keys."))
-
-
-class CloseButton(BaseType):
-
-    """Mouse button used to close tabs."""
-
-    special = True
-    valid_values = ValidValues(('right', "Close tabs on right-click."),
-                               ('middle', "Close tabs on middle-click."),
-                               ('none', "Don't close tabs using the mouse."))
-
-
 class NewTabPosition(BaseType):
 
     """How new tabs are positioned."""
 
     special = True
-    valid_values = ValidValues(('left', "On the left of the current tab."),
-                               ('right', "On the right of the current tab."),
-                               ('first', "At the left end."),
-                               ('last', "At the right end."))
+
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = ValidValues(
+            ('left', "On the left of the current tab."),
+            ('right', "On the right of the current tab."),
+            ('first', "At the left end."),
+            ('last', "At the right end."))
 
 
 class IgnoreCase(Bool):
@@ -1494,10 +1474,14 @@ class IgnoreCase(Bool):
     """Whether to ignore case when searching."""
 
     special = True
-    valid_values = ValidValues(('true', "Search case-insensitively"),
-                               ('false', "Search case-sensitively"),
-                               ('smart', "Search case-sensitively if there "
-                                         "are capital chars"))
+
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = ValidValues(
+            ('true', "Search case-insensitively"),
+            ('false', "Search case-sensitively"),
+            ('smart', "Search case-sensitively if there "
+             "are capital chars"))
 
     def transform(self, value):
         if value.lower() == 'smart':
@@ -1515,57 +1499,12 @@ def validate(self, value):
             super().validate(value)
 
 
-class NewInstanceOpenTarget(BaseType):
-
-    """How to open links in an existing instance if a new one is launched."""
-
-    special = True
-    valid_values = ValidValues(('tab', "Open a new tab in the existing "
-                                       "window and activate the window."),
-                               ('tab-bg', "Open a new background tab in the "
-                                          "existing window and activate the "
-                                          "window."),
-                               ('tab-silent', "Open a new tab in the existing "
-                                              "window without activating "
-                                              "the window."),
-                               ('tab-bg-silent', "Open a new background tab "
-                                                 "in the existing window "
-                                                 "without activating the "
-                                                 "window."),
-                               ('window', "Open in a new window."))
-
-
-class DownloadPathSuggestion(BaseType):
-
-    """How to format the question when downloading."""
-
-    special = True
-    valid_values = ValidValues(('path', "Show only the download path."),
-                               ('filename', "Show only download filename."),
-                               ('both', "Show download path and filename."))
-
-
-class Referer(BaseType):
-
-    """Send the Referer header."""
-
-    valid_values = ValidValues(('always', "Always send."),
-                               ('never', "Never send; this is not recommended,"
-                                   " as some sites may break."),
-                               ('same-domain', "Only send for the same domain."
-                                   " This will still protect your privacy, but"
-                                   " shouldn't break any sites."))
-
-
 class UserAgent(BaseType):
 
     """The user agent to use."""
 
     special = True
 
-    def __init__(self, none_ok=False):
-        super().__init__(none_ok)
-
     def validate(self, value):
         self._basic_validation(value)
 
@@ -1620,25 +1559,6 @@ def complete(self):
         return out
 
 
-class TabBarShow(BaseType):
-
-    """When to show the tab bar."""
-
-    valid_values = ValidValues(('always', "Always show the tab bar."),
-                               ('never', "Always hide the tab bar."),
-                               ('multiple', "Hide the tab bar if only one tab "
-                                            "is open."),
-                               ('switching', "Show the tab bar when switching "
-                                             "tabs."))
-
-
-class URLSegmentList(FlagList):
-
-    """A list of URL segments."""
-
-    valid_values = ValidValues('host', 'path', 'query', 'anchor')
-
-
 class TimestampTemplate(BaseType):
 
     """A strftime-like template for timestamps.


Test patch:
diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py
--- a/tests/unit/config/test_configtypes.py
+++ b/tests/unit/config/test_configtypes.py
@@ -225,27 +225,18 @@ def test_complete_without_desc(self, basetype, valid_values, completions):
         assert basetype.complete() == completions
 
 
-class GoodMappingSubclass(configtypes.MappingType):
+class MappingSubclass(configtypes.MappingType):
 
     """A MappingType we use in TestMappingType which is valid/good."""
 
-    valid_values = configtypes.ValidValues('one', 'two')
-
     MAPPING = {
         'one': 1,
         'two': 2,
     }
 
-
-class BadMappingSubclass(configtypes.MappingType):
-
-    """A MappingType which is missing a value in MAPPING."""
-
-    valid_values = configtypes.ValidValues('one', 'two')
-
-    MAPPING = {
-        'one': 1,
-    }
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = configtypes.ValidValues('one', 'two')
 
 
 class TestMappingType:
@@ -261,7 +252,7 @@ class TestMappingType:
 
     @pytest.fixture
     def klass(self):
-        return GoodMappingSubclass
+        return MappingSubclass
 
     @pytest.mark.parametrize('val', TESTS.keys())
     def test_validate_valid(self, klass, val):
@@ -276,9 +267,11 @@ def test_validate_invalid(self, klass, val):
     def test_transform(self, klass, val, expected):
         assert klass().transform(val) == expected
 
-    def test_bad_subclass_init(self):
-        with pytest.raises(ValueError):
-            BadMappingSubclass()
+    @pytest.mark.parametrize('typ', [configtypes.ColorSystem(),
+                                     configtypes.Position(),
+                                     configtypes.SelectOnRemove()])
+    def test_mapping_type_matches_valid_values(self, typ):
+        assert list(sorted(typ.MAPPING)) == list(sorted(typ.valid_values))
 
 
 class TestString:
@@ -383,9 +376,12 @@ class FlagListSubclass(configtypes.FlagList):
     Valid values are 'foo', 'bar' and 'baz'.
     """
 
-    valid_values = configtypes.ValidValues('foo', 'bar', 'baz')
     combinable_values = ['foo', 'bar']
 
+    def __init__(self, none_ok=False):
+        super().__init__(none_ok)
+        self.valid_values = configtypes.ValidValues('foo', 'bar', 'baz')
+
 
 class TestFlagList: