Implement deleting/renaming values in configdata.yml

This is needed for #3077, but also is used for the deletion in #2847 now.
See #2772.
This commit is contained in:
Florian Bruhin 2017-10-11 07:13:51 +02:00
parent 211de6d664
commit abbd69f604
6 changed files with 126 additions and 11 deletions

View File

@ -31,6 +31,7 @@ from qutebrowser.config import configtypes
from qutebrowser.utils import usertypes, qtutils, utils from qutebrowser.utils import usertypes, qtutils, utils
DATA = None DATA = None
MIGRATIONS = None
@attr.s @attr.s
@ -49,6 +50,15 @@ class Option:
description = attr.ib() description = attr.ib()
@attr.s
class Migrations:
"""Nigrated options in configdata.yml."""
renamed = attr.ib(default=attr.Factory(dict))
deleted = attr.ib(default=attr.Factory(list))
def _raise_invalid_node(name, what, node): def _raise_invalid_node(name, what, node):
"""Raise an exception for an invalid configdata YAML node. """Raise an exception for an invalid configdata YAML node.
@ -172,14 +182,27 @@ def _read_yaml(yaml_data):
yaml_data: The YAML string to parse. yaml_data: The YAML string to parse.
Return: Return:
A dict mapping option names to Option elements. A tuple with two elements:
- A dict mapping option names to Option elements.
- A Migrations object.
""" """
parsed = {} parsed = {}
migrations = Migrations()
data = utils.yaml_load(yaml_data) data = utils.yaml_load(yaml_data)
keys = {'type', 'default', 'desc', 'backend'} keys = {'type', 'default', 'desc', 'backend'}
for name, option in data.items(): for name, option in data.items():
if set(option.keys()) == {'renamed'}:
migrations.renamed[name] = option['renamed']
continue
if set(option.keys()) == {'deleted'}:
value = option['deleted']
if value is not True:
raise ValueError("Invalid deleted value: {}".format(value))
migrations.deleted.append(name)
continue
if not set(option.keys()).issubset(keys): if not set(option.keys()).issubset(keys):
raise ValueError("Invalid keys {} for {}".format( raise ValueError("Invalid keys {} for {}".format(
option.keys(), name)) option.keys(), name))
@ -200,7 +223,12 @@ def _read_yaml(yaml_data):
if key2.startswith(key1 + '.'): if key2.startswith(key1 + '.'):
raise ValueError("Shadowing keys {} and {}".format(key1, key2)) raise ValueError("Shadowing keys {} and {}".format(key1, key2))
return parsed # Make sure rename targets actually exist.
for old, new in migrations.renamed.items():
if new not in parsed:
raise ValueError("Renaming {} to unknown {}".format(old, new))
return parsed, migrations
@functools.lru_cache(maxsize=256) @functools.lru_cache(maxsize=256)
@ -211,5 +239,5 @@ def is_valid_prefix(prefix):
def init(): def init():
"""Initialize configdata from the YAML file.""" """Initialize configdata from the YAML file."""
global DATA global DATA, MIGRATIONS
DATA = _read_yaml(utils.read_file('config/configdata.yml')) DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml'))

View File

@ -1252,6 +1252,9 @@ tabs.width.indicator:
minval: 0 minval: 0
desc: Width of the progress indicator (0 to disable). desc: Width of the progress indicator (0 to disable).
tabs.width.pinned:
deleted: true
tabs.wrap: tabs.wrap:
default: true default: true
type: Bool type: Bool

View File

@ -33,7 +33,7 @@ from PyQt5.QtCore import pyqtSignal, QObject, QSettings
import qutebrowser import qutebrowser
from qutebrowser.config import configexc, config, configdata from qutebrowser.config import configexc, config, configdata
from qutebrowser.utils import standarddir, utils, qtutils from qutebrowser.utils import standarddir, utils, qtutils, log
# The StateConfig instance # The StateConfig instance
@ -162,11 +162,23 @@ class YamlConfig(QObject):
"'global' object is not a dict") "'global' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
# Delete unknown values # Handle unknown/renamed keys
# (e.g. options which were removed from configdata.yml)
for name in list(global_obj): for name in list(global_obj):
if name not in configdata.DATA: if name in configdata.MIGRATIONS.renamed:
new_name = configdata.MIGRATIONS.renamed[name]
log.config.debug("Renaming {} to {}".format(name, new_name))
global_obj[new_name] = global_obj[name]
del global_obj[name] del global_obj[name]
elif name in configdata.MIGRATIONS.deleted:
log.config.debug("Removing {}".format(name))
del global_obj[name]
elif name in configdata.DATA:
pass
else:
desc = configexc.ConfigErrorDesc(
"While loading options",
"Unknown option {}".format(name))
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
self._values = global_obj self._values = global_obj
self._dirty = False self._dirty = False

View File

@ -57,7 +57,7 @@ def test_is_valid_prefix(monkeypatch):
class TestReadYaml: class TestReadYaml:
def test_valid(self): def test_valid(self):
data = textwrap.dedent(""" yaml_data = textwrap.dedent("""
test1: test1:
type: Bool type: Bool
default: true default: true
@ -69,7 +69,7 @@ class TestReadYaml:
backend: QtWebKit backend: QtWebKit
desc: Hello World 2 desc: Hello World 2
""") """)
data = configdata._read_yaml(data) data, _migrations = configdata._read_yaml(yaml_data)
assert data.keys() == {'test1', 'test2'} assert data.keys() == {'test1', 'test2'}
assert data['test1'].description == "Hello World" assert data['test1'].description == "Hello World"
assert data['test2'].default == "foo" assert data['test2'].default == "foo"
@ -113,6 +113,45 @@ class TestReadYaml:
else: else:
configdata._read_yaml(data) configdata._read_yaml(data)
def test_rename(self):
yaml_data = textwrap.dedent("""
test:
renamed: test_new
test_new:
type: Bool
default: true
desc: Hello World
""")
data, migrations = configdata._read_yaml(yaml_data)
assert data.keys() == {'test_new'}
assert migrations.renamed == {'test': 'test_new'}
def test_rename_unknown_target(self):
yaml_data = textwrap.dedent("""
test:
renamed: test2
""")
with pytest.raises(ValueError, match='Renaming test to unknown test2'):
configdata._read_yaml(yaml_data)
def test_delete(self):
yaml_data = textwrap.dedent("""
test:
deleted: true
""")
data, migrations = configdata._read_yaml(yaml_data)
assert not data.keys()
assert migrations.deleted == ['test']
def test_delete_invalid_value(self):
yaml_data = textwrap.dedent("""
test:
deleted: false
""")
with pytest.raises(ValueError, match='Invalid deleted value: False'):
configdata._read_yaml(yaml_data)
class TestParseYamlType: class TestParseYamlType:

View File

@ -119,16 +119,45 @@ class TestYaml:
'yaml-config', unittest.mock.ANY, unittest.mock.ANY) 'yaml-config', unittest.mock.ANY, unittest.mock.ANY)
def test_unknown_key(self, yaml, config_tmpdir): def test_unknown_key(self, yaml, config_tmpdir):
"""An unknown setting should be deleted.""" """An unknown setting should show an error."""
autoconfig = config_tmpdir / 'autoconfig.yml' autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text('global:\n hello: world', encoding='utf-8') autoconfig.write_text('global:\n hello: world', encoding='utf-8')
with pytest.raises(configexc.ConfigFileErrors) as excinfo:
yaml.load()
assert len(excinfo.value.errors) == 1
error = excinfo.value.errors[0]
assert error.text == "While loading options"
assert str(error.exception) == "Unknown option hello"
def test_deleted_key(self, monkeypatch, yaml, config_tmpdir):
"""A key marked as deleted should be removed."""
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text('global:\n hello: world', encoding='utf-8')
monkeypatch.setattr(configdata.MIGRATIONS, 'deleted', ['hello'])
yaml.load() yaml.load()
yaml._save() yaml._save()
lines = autoconfig.read_text('utf-8').splitlines() lines = autoconfig.read_text('utf-8').splitlines()
assert ' hello:' not in lines assert ' hello:' not in lines
def test_renamed_key(self, monkeypatch, yaml, config_tmpdir):
"""A key marked as renamed should be renamed properly."""
autoconfig = config_tmpdir / 'autoconfig.yml'
autoconfig.write_text('global:\n old: value', encoding='utf-8')
monkeypatch.setattr(configdata.MIGRATIONS, 'renamed', {'old': 'new'})
yaml.load()
yaml._save()
lines = autoconfig.read_text('utf-8').splitlines()
assert ' old:' not in lines
assert ' new:' not in lines
@pytest.mark.parametrize('old_config', [ @pytest.mark.parametrize('old_config', [
None, None,
'global:\n colors.hints.fg: magenta', 'global:\n colors.hints.fg: magenta',

View File

@ -149,6 +149,10 @@ class TestEarlyInit:
error = ("Error{}: Invalid value 'True' - expected a value of " error = ("Error{}: Invalid value 'True' - expected a value of "
"type str but got bool.".format(suffix)) "type str but got bool.".format(suffix))
expected_errors.append(error) expected_errors.append(error)
elif invalid_yaml == 'unknown':
error = ("While loading options{}: Unknown option "
"colors.foobar".format(suffix))
expected_errors.append(error)
if config_py == 'error': if config_py == 'error':
expected_errors.append("While setting 'foo': No option 'foo'") expected_errors.append("While setting 'foo': No option 'foo'")