Initial config.py support

See #2795
This commit is contained in:
Florian Bruhin 2017-09-14 16:16:14 +02:00
parent ed6933a839
commit cb806aefa3
5 changed files with 168 additions and 13 deletions

View File

@ -30,6 +30,7 @@ disable=no-self-use,
broad-except, broad-except,
bare-except, bare-except,
eval-used, eval-used,
exec-used,
ungrouped-imports, ungrouped-imports,
suppressed-message, suppressed-message,
too-many-return-statements, too-many-return-statements,

View File

@ -625,13 +625,16 @@ def init(parent=None):
val = ConfigContainer(instance) val = ConfigContainer(instance)
key_instance = KeyConfig(instance) key_instance = KeyConfig(instance)
for cf in _change_filters:
cf.validate()
configtypes.Font.monospace_fonts = val.fonts.monospace configtypes.Font.monospace_fonts = val.fonts.monospace
config_commands = ConfigCommands(instance, key_instance) config_commands = ConfigCommands(instance, key_instance)
objreg.register('config-commands', config_commands) objreg.register('config-commands', config_commands)
for cf in _change_filters: config_api = configfiles.read_config_py()
cf.validate() if getattr(config_api, 'load_autoconfig', True):
instance.read_yaml() instance.read_yaml()
configfiles.init(instance) configfiles.init(instance)

View File

@ -19,6 +19,7 @@
"""Configuration files residing on disk.""" """Configuration files residing on disk."""
import types
import os.path import os.path
import textwrap import textwrap
import configparser import configparser
@ -90,6 +91,63 @@ class YamlConfig:
pass pass
class ConfigAPI:
"""Object which gets passed to config.py as "config" object.
This is a small wrapper over the Config object, but with more
straightforward method names (get/set call get_obj/set_obj) and a more
shallow API.
Attributes:
_config: The main Config object to use.
_keyconfig: The KeyConfig object.
val: A matching ConfigContainer object.
load_autoconfig: Whether autoconfig.yml should be loaded.
"""
def __init__(self, config, keyconfig, container):
self._config = config
self._keyconfig = keyconfig
self.val = container
self.load_autoconfig = True
def get(self, name):
return self._config.get_obj(name)
def set(self, name, value):
self._config.set_obj(name, value)
def bind(self, key, command, *, mode, force=False):
self._keyconfig.bind(key, command, mode=mode, force=force)
def unbind(self, key, *, mode):
self._keyconfig.unbind(key, mode=mode)
def read_config_py(filename=None):
"""Read a config.py file."""
from qutebrowser.config import config
# FIXME:conf error handling
if filename is None:
filename = os.path.join(standarddir.config(), 'config.py')
if not os.path.exists(filename):
return None
api = ConfigAPI(config.instance, config.key_instance, config.val)
module = types.ModuleType('config')
module.config = api
module.c = api.val
module.__file__ = filename
with open(filename, mode='rb') as f:
source = f.read()
code = compile(source, filename, 'exec')
exec(code, module.__dict__)
return api
def init(config): def init(config):
"""Initialize config storage not related to the main config.""" """Initialize config storage not related to the main config."""
state = StateConfig() state = StateConfig()

View File

@ -852,16 +852,24 @@ def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir,
monkeypatch.setattr(config, 'key_instance', None) monkeypatch.setattr(config, 'key_instance', None)
monkeypatch.setattr(config, '_change_filters', []) monkeypatch.setattr(config, '_change_filters', [])
yield yield
objreg.delete('config-commands') for obj in ['config-commands', 'state-config', 'command-history']:
try: try:
objreg.delete('state-config') objreg.delete(obj)
except KeyError: except KeyError:
pass pass
def test_init(init_patch, fake_save_manager, config_tmpdir): @pytest.mark.parametrize('load_autoconfig', [True, False])
(config_tmpdir / 'autoconfig.yml').write_text( def test_init(init_patch, fake_save_manager, config_tmpdir, load_autoconfig):
'global:\n colors.hints.fg: magenta', 'utf-8', ensure=True) autoconfig_file = config_tmpdir / 'autoconfig.yml'
config_py_file = config_tmpdir / 'config.py'
autoconfig_file.write_text('global:\n colors.hints.fg: magenta\n',
'utf-8', ensure=True)
config_py_lines = ['c.colors.hints.bg = "red"']
if not load_autoconfig:
config_py_lines.append('config.load_autoconfig = False')
config_py_file.write_text('\n'.join(config_py_lines), 'utf-8', ensure=True)
config.init() config.init()
@ -875,7 +883,11 @@ def test_init(init_patch, fake_save_manager, config_tmpdir):
fake_save_manager.add_saveable.assert_any_call( fake_save_manager.add_saveable.assert_any_call(
'yaml-config', unittest.mock.ANY) 'yaml-config', unittest.mock.ANY)
assert config.instance._values['colors.hints.fg'] == 'magenta' assert config.instance._values['colors.hints.bg'] == 'red'
if load_autoconfig:
assert config.instance._values['colors.hints.fg'] == 'magenta'
else:
assert 'colors.hints.fg' not in config.instance._values
def test_init_invalid_change_filter(init_patch): def test_init_invalid_change_filter(init_patch):

View File

@ -22,7 +22,7 @@ import sys
import pytest import pytest
from qutebrowser.config import configfiles from qutebrowser.config import config, configfiles
from qutebrowser.utils import objreg from qutebrowser.utils import objreg
from PyQt5.QtCore import QSettings from PyQt5.QtCore import QSettings
@ -91,6 +91,87 @@ def test_yaml_config(fake_save_manager, config_tmpdir, old_config, insert):
assert ' tabs.show: never' in lines assert ' tabs.show: never' in lines
class TestConfigPy:
"""Tests for ConfigAPI and read_config_py()."""
pytestmark = pytest.mark.usefixtures('config_stub', 'key_config_stub')
class ConfPy:
"""Helper class to get a confpy fixture."""
def __init__(self, tmpdir):
self._confpy = tmpdir / 'config.py'
self.filename = str(self._confpy)
def write(self, *lines):
text = '\n'.join(lines)
self._confpy.write_text(text, 'utf-8', ensure=True)
@pytest.fixture
def confpy(self, tmpdir):
return self.ConfPy(tmpdir)
@pytest.mark.parametrize('line', [
'c.colors.hints.bg = "red"',
'config.val.colors.hints.bg = "red"',
'config.set("colors.hints.bg", "red")',
])
def test_set(self, confpy, line):
confpy.write(line)
configfiles.read_config_py(confpy.filename)
assert config.instance._values['colors.hints.bg'] == 'red'
@pytest.mark.parametrize('set_first', [True, False])
@pytest.mark.parametrize('get_line', [
'c.colors.hints.fg',
'config.get("colors.hints.fg")',
])
def test_get(self, confpy, set_first, get_line):
"""Test whether getting options works correctly.
We test this by doing the following:
- Set colors.hints.fg to some value (inside the config.py with
set_first, outside of it otherwise).
- In the config.py, read .fg and set .bg to the same value.
- Verify that .bg has been set correctly.
"""
# pylint: disable=bad-config-option
config.val.colors.hints.fg = 'green'
if set_first:
confpy.write('c.colors.hints.fg = "red"',
'c.colors.hints.bg = {}'.format(get_line))
expected = 'red'
else:
confpy.write('c.colors.hints.bg = {}'.format(get_line))
expected = 'green'
configfiles.read_config_py(confpy.filename)
assert config.instance._values['colors.hints.bg'] == expected
def test_bind(self, confpy):
confpy.write('config.bind(",a", "message-info foo", mode="normal")')
configfiles.read_config_py(confpy.filename)
expected = {'normal': {',a': 'message-info foo'}}
assert config.instance._values['bindings.commands'] == expected
def test_unbind(self, confpy):
confpy.write('config.unbind("o", mode="normal")')
configfiles.read_config_py(confpy.filename)
expected = {'normal': {'o': None}}
assert config.instance._values['bindings.commands'] == expected
def test_reading_default_location(self, config_tmpdir):
(config_tmpdir / 'config.py').write_text(
'c.colors.hints.bg = "red"', 'utf-8')
configfiles.read_config_py()
assert config.instance._values['colors.hints.bg'] == 'red'
def test_reading_missing_default_location(self, config_tmpdir):
assert not (config_tmpdir / 'config.py').exists()
configfiles.read_config_py() # Should not crash
@pytest.fixture @pytest.fixture
def init_patch(qapp, fake_save_manager, config_tmpdir, data_tmpdir, def init_patch(qapp, fake_save_manager, config_tmpdir, data_tmpdir,
config_stub): config_stub):