From cb806aefa3b1a367fb6e79332504466a9e07781f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 14 Sep 2017 16:16:14 +0200 Subject: [PATCH] Initial config.py support See #2795 --- .pylintrc | 1 + qutebrowser/config/config.py | 9 ++- qutebrowser/config/configfiles.py | 58 +++++++++++++++++++ tests/unit/config/test_config.py | 30 +++++++--- tests/unit/config/test_configfiles.py | 83 ++++++++++++++++++++++++++- 5 files changed, 168 insertions(+), 13 deletions(-) diff --git a/.pylintrc b/.pylintrc index 04d689ffd..31e91a3d3 100644 --- a/.pylintrc +++ b/.pylintrc @@ -30,6 +30,7 @@ disable=no-self-use, broad-except, bare-except, eval-used, + exec-used, ungrouped-imports, suppressed-message, too-many-return-statements, diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 67e44fa8a..58359a335 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -625,13 +625,16 @@ def init(parent=None): val = ConfigContainer(instance) key_instance = KeyConfig(instance) + for cf in _change_filters: + cf.validate() + configtypes.Font.monospace_fonts = val.fonts.monospace config_commands = ConfigCommands(instance, key_instance) objreg.register('config-commands', config_commands) - for cf in _change_filters: - cf.validate() - instance.read_yaml() + config_api = configfiles.read_config_py() + if getattr(config_api, 'load_autoconfig', True): + instance.read_yaml() configfiles.init(instance) diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 515d9e28d..1ac6668c8 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -19,6 +19,7 @@ """Configuration files residing on disk.""" +import types import os.path import textwrap import configparser @@ -90,6 +91,63 @@ class YamlConfig: 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): """Initialize config storage not related to the main config.""" state = StateConfig() diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 5a67651da..9ddf5d373 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -852,16 +852,24 @@ def init_patch(qapp, fake_save_manager, monkeypatch, config_tmpdir, monkeypatch.setattr(config, 'key_instance', None) monkeypatch.setattr(config, '_change_filters', []) yield - objreg.delete('config-commands') - try: - objreg.delete('state-config') - except KeyError: - pass + for obj in ['config-commands', 'state-config', 'command-history']: + try: + objreg.delete(obj) + except KeyError: + pass -def test_init(init_patch, fake_save_manager, config_tmpdir): - (config_tmpdir / 'autoconfig.yml').write_text( - 'global:\n colors.hints.fg: magenta', 'utf-8', ensure=True) +@pytest.mark.parametrize('load_autoconfig', [True, False]) +def test_init(init_patch, fake_save_manager, config_tmpdir, load_autoconfig): + 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() @@ -875,7 +883,11 @@ def test_init(init_patch, fake_save_manager, config_tmpdir): fake_save_manager.add_saveable.assert_any_call( '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): diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index f20d19759..d077e9918 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -22,7 +22,7 @@ import sys import pytest -from qutebrowser.config import configfiles +from qutebrowser.config import config, configfiles from qutebrowser.utils import objreg 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 +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 def init_patch(qapp, fake_save_manager, config_tmpdir, data_tmpdir, config_stub):