diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml index 0a98a7b6e..ac0aea9d3 100644 --- a/qutebrowser/config/configdata.yml +++ b/qutebrowser/config/configdata.yml @@ -83,10 +83,10 @@ new_instance_open_target_window: searchengines: default: - # FIXME:conf what if the user deletes/renames DEFAULT? DEFAULT: https://duckduckgo.com/?q={} type: name: Dict + required_keys: ['DEFAULT'] keytype: String valtype: SearchEngineUrl desc: >- diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index d8c689e28..8a93b2305 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1030,7 +1030,8 @@ class Dict(BaseType): """A dictionary of values.""" - def __init__(self, keytype, valtype, *, fixed_keys=None, none_ok=False): + def __init__(self, keytype, valtype, *, fixed_keys=None, + required_keys=None, none_ok=False): 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. @@ -1038,12 +1039,17 @@ class Dict(BaseType): self.keytype = keytype self.valtype = valtype self.fixed_keys = fixed_keys + self.required_keys = required_keys def _validate_keys(self, value): if (self.fixed_keys is not None and value.keys() != set(self.fixed_keys)): raise configexc.ValidationError( value, "Expected keys {}".format(self.fixed_keys)) + if (self.required_keys is not None and not + set(self.required_keys).issubset(value.keys())): + raise configexc.ValidationError( + value, "Required keys {}".format(self.required_keys)) def _none_value(self, value=None): """Return the value to be used when the setting is None. diff --git a/tests/helpers/test_helper_utils.py b/tests/helpers/test_helper_utils.py index d7eadb894..a783d98f8 100644 --- a/tests/helpers/test_helper_utils.py +++ b/tests/helpers/test_helper_utils.py @@ -73,3 +73,8 @@ def test_partial_compare_not_equal(val1, val2, error): ]) def test_pattern_match(pattern, value, expected): assert utils.pattern_match(pattern=pattern, value=value) == expected + + +def test_nop_contextmanager(): + with utils.nop_contextmanager(): + pass diff --git a/tests/helpers/utils.py b/tests/helpers/utils.py index 7dbd7dd25..e6e3d37c8 100644 --- a/tests/helpers/utils.py +++ b/tests/helpers/utils.py @@ -23,6 +23,7 @@ import re import pprint import os.path +import contextlib import pytest @@ -170,3 +171,8 @@ def abs_datapath(): """Get the absolute path to the end2end data directory.""" file_abs = os.path.abspath(os.path.dirname(__file__)) return os.path.join(file_abs, '..', 'end2end', 'data') + + +@contextlib.contextmanager +def nop_contextmanager(): + yield diff --git a/tests/unit/config/test_configtypes.py b/tests/unit/config/test_configtypes.py index c34666a4b..64d05e7cd 100644 --- a/tests/unit/config/test_configtypes.py +++ b/tests/unit/config/test_configtypes.py @@ -40,6 +40,7 @@ from PyQt5.QtNetwork import QNetworkProxy from qutebrowser.config import configtypes, configexc from qutebrowser.utils import debug, utils, qtutils from qutebrowser.browser.network import pac +from tests.helpers import utils as testutils class Font(QFont): @@ -1390,16 +1391,35 @@ class TestDict: valtype=configtypes.Int()) assert typ.from_str('{"answer": 42}') == {"answer": 42} - @pytest.mark.parametrize('val', [ - {"one": "1"}, # missing key - {"one": "1", "two": "2", "three": "3"}, # extra key + @pytest.mark.parametrize('kind, val, ok', [ + ('fixed', {"one": "1"}, False), # missing key + ('fixed', {"one": "1", "two": "2", "three": "3"}, False), # extra key + ('fixed', {"one": "1", "two": "2"}, True), + + ('required', {"one": "1"}, False), # missing key + ('required', {"one": "1", "two": "2", "three": "3"}, True), # extra + ('required', {"one": "1", "two": "2"}, True), ]) @pytest.mark.parametrize('from_str', [True, False]) - def test_fixed_keys(self, klass, val, from_str): - d = klass(keytype=configtypes.String(), valtype=configtypes.String(), - fixed_keys=['one', 'two']) + def test_keys(self, klass, kind, val, ok, from_str): + if kind == 'fixed': + d = klass(keytype=configtypes.String(), + valtype=configtypes.String(), + fixed_keys=['one', 'two']) + message = 'Expected keys .*' + elif kind == 'required': + d = klass(keytype=configtypes.String(), + valtype=configtypes.String(), + required_keys=['one', 'two']) + message = 'Required keys .*' - with pytest.raises(configexc.ValidationError): + if ok: + expectation = testutils.nop_contextmanager() + else: + expectation = pytest.raises(configexc.ValidationError, + match=message) + + with expectation: if from_str: d.from_str(json.dumps(val)) else: