diff --git a/qutebrowser/completion/models/configmodel.py b/qutebrowser/completion/models/configmodel.py index 6ee2f9566..3a225fedf 100644 --- a/qutebrowser/completion/models/configmodel.py +++ b/qutebrowser/completion/models/configmodel.py @@ -27,12 +27,7 @@ from qutebrowser.keyinput import keyutils def option(*, info): """A CompletionModel filled with settings and their descriptions.""" - model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) - options = ((opt.name, opt.description, info.config.get_str(opt.name)) - for opt in configdata.DATA.values() - if not opt.no_autoconfig) - model.add_category(listcategory.ListCategory("Options", options)) - return model + return _option(info, "Options", lambda opt: not opt.no_autoconfig) def customized_option(*, info): @@ -47,6 +42,35 @@ def customized_option(*, info): return model +def list_option(*, info): + """A CompletionModel filled with settings whose values are lists.""" + predicate = lambda opt: isinstance(info.config.get_obj(opt.name), list) + return _option(info, "List options", predicate) + + +def dict_option(*, info): + """A CompletionModel filled with settings whose values are dicts.""" + predicate = lambda opt: isinstance(info.config.get_obj(opt.name), dict) + return _option(info, "Dict options", predicate) + + +def _option(info, title, predicate): + """A CompletionModel that is generified for several option sets. + + Args: + info: The config info that can be passed through. + title: The title of the options. + predicate: The function for filtering out the options. Takes a single + argument. + """ + model = completionmodel.CompletionModel(column_widths=(20, 70, 10)) + options = ((opt.name, opt.description, info.config.get_str(opt.name)) + for opt in configdata.DATA.values() + if predicate(opt)) + model.add_category(listcategory.ListCategory(title, options)) + return model + + def value(optname, *values, info): """A CompletionModel filled with setting values. diff --git a/qutebrowser/config/configcommands.py b/qutebrowser/config/configcommands.py index 10e16c370..228cee59d 100644 --- a/qutebrowser/config/configcommands.py +++ b/qutebrowser/config/configcommands.py @@ -245,11 +245,63 @@ class ConfigCommands: Args: option: The name of the option. - temp: Don't touch autoconfig.yml. + temp: Set value temporarily until qutebrowser is closed. """ with self._handle_config_error(): self._config.unset(option, save_yaml=not temp) + @cmdutils.register(instance='config-commands') + @cmdutils.argument('option', completion=configmodel.list_option) + def config_add_list(self, option, value, temp=False): + """Append a value to a config option that is a list. + + Args: + option: The name of the option. + value: The value to append to the end of the list. + temp: Set value temporarily until qutebrowser is closed. + """ + opt = self._config.get_opt(option) + valid_list_types = (configtypes.List, configtypes.ListOrValue) + if not isinstance(opt.typ, valid_list_types): + raise cmdexc.CommandError(":config-add-list can only be used for " + "lists") + + with self._handle_config_error(): + option_value = self._config.get_mutable_obj(option) + option_value.append(value) + self._config.update_mutables(save_yaml=not temp) + + @cmdutils.register(instance='config-commands') + @cmdutils.argument('option', completion=configmodel.dict_option) + def config_add_dict(self, option, key, value, temp=False, replace=False): + """Add a value at the key within the option specified. + + This adds an element to a dictionary. --replace is needed to override + existing values. + + Args: + option: The name of the option. + key: The key to use. + value: The value to place in the dictionary. + temp: Set value temporarily until qutebrowser is closed. + replace: Whether or not we should replace, default is not. + """ + opt = self._config.get_opt(option) + if not isinstance(opt.typ, configtypes.Dict): + raise cmdexc.CommandError(":config-add-dict can only be used for " + "dicts") + + with self._handle_config_error(): + option_value = self._config.get_mutable_obj(option) + + if key in option_value and not replace: + raise cmdexc.CommandError("{} already exists in {} - use " + "--replace to overwrite!" + .format(key, option)) + + option_value[key] = value + self._config.update_mutables(save_yaml=not temp) + @cmdutils.register(instance='config-commands') def config_clear(self, save=False): """Set all settings back to their default. diff --git a/tests/unit/config/test_configcommands.py b/tests/unit/config/test_configcommands.py index f70586152..3ed1f4620 100644 --- a/tests/unit/config/test_configcommands.py +++ b/tests/unit/config/test_configcommands.py @@ -282,6 +282,103 @@ class TestCycle: assert msg.text == 'auto_save.session = true' +class TestAdd: + + """Test :config-add-list and :config-add-dict.""" + + @pytest.mark.parametrize('temp', [True, False]) + @pytest.mark.parametrize('value', ['test1', 'test2']) + def test_add_list(self, commands, config_stub, yaml_value, temp, value): + name = 'content.host_blocking.whitelist' + + commands.config_add_list(name, value, temp=temp) + + assert str(config_stub.get(name)[-1]) == value + if temp: + assert yaml_value(name) == configutils.UNSET + else: + assert yaml_value(name)[-1] == value + + def test_add_list_non_list(self, commands): + name = 'history_gap_interval' + value = 'value' + with pytest.raises( + cmdexc.CommandError, + match=":config-add-list can only be used for lists"): + commands.config_add_list(name, value) + + def test_add_list_empty_value(self, commands): + name = 'content.host_blocking.whitelist' + value = '' + with pytest.raises( + cmdexc.CommandError, + match="Invalid value '{}' - may not be empty!".format(value)): + commands.config_add_list(name, value) + + def test_add_list_none_value(self, commands): + name = 'content.host_blocking.whitelist' + value = None + with pytest.raises( + cmdexc.CommandError, + match="Invalid value 'None' - may not be null!"): + commands.config_add_list(name, value) + + @pytest.mark.parametrize('value', ['test1', 'test2']) + @pytest.mark.parametrize('temp', [True, False]) + def test_add_dict(self, commands, config_stub, yaml_value, value, temp): + name = 'aliases' + key = 'missingkey' + + commands.config_add_dict(name, key, value, temp=temp) + + assert str(config_stub.get(name)[key]) == value + if temp: + assert yaml_value(name) == configutils.UNSET + else: + assert yaml_value(name)[key] == value + + @pytest.mark.parametrize('replace', [True, False]) + def test_add_dict_replace(self, commands, config_stub, replace): + name = 'aliases' + key = 'w' + value = 'anything' + + if replace: + commands.config_add_dict(name, key, value, replace=True) + assert str(config_stub.get(name)[key]) == value + else: + with pytest.raises( + cmdexc.CommandError, + match="w already exists in aliases - use --replace to " + "overwrite!"): + commands.config_add_dict(name, key, value, replace=False) + + def test_add_dict_non_dict(self, commands): + name = 'history_gap_interval' + key = 'value' + value = 'value' + with pytest.raises( + cmdexc.CommandError, + match=":config-add-dict can only be used for dicts"): + commands.config_add_dict(name, key, value) + + def test_add_dict_empty_value(self, commands): + name = 'aliases' + key = 'missingkey' + value = '' + with pytest.raises(cmdexc.CommandError, + match="Invalid value '' - may not be empty!"): + commands.config_add_dict(name, key, value) + + def test_add_dict_none_value(self, commands): + name = 'aliases' + key = 'missingkey' + value = None + with pytest.raises(cmdexc.CommandError, + match="Invalid value 'None' - may not be null!"): + commands.config_add_dict(name, key, value) + + class TestUnsetAndClear: """Test :config-unset and :config-clear."""