From 439d51875f2ec4fcef56eafef1740e7d0c865741 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 20 Feb 2018 20:54:26 +0100 Subject: [PATCH] Add config.pattern() --- doc/help/configuring.asciidoc | 9 ++++++++ qutebrowser/config/config.py | 16 +++++++++---- qutebrowser/config/configfiles.py | 10 ++++++++ tests/unit/config/test_config.py | 6 +++++ tests/unit/config/test_configfiles.py | 33 +++++++++++++++++++-------- 5 files changed, 59 insertions(+), 15 deletions(-) diff --git a/doc/help/configuring.asciidoc b/doc/help/configuring.asciidoc index 1dcbbe9a6..266315d56 100644 --- a/doc/help/configuring.asciidoc +++ b/doc/help/configuring.asciidoc @@ -185,6 +185,15 @@ https://developer.chrome.com/apps/match_patterns[URL pattern]: config.set('content.images', False, '*://example.com/') ---- +Alternatively, you can use `with config.pattern(...) as p:` to get a shortcut +similar to `c.` which is scoped to the given domain: + +[source,python] +---- +with config.pattern('*://example.com/') as p: + p.content.images = False +---- + Binding keys ~~~~~~~~~~~~ diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 9b90562c4..010eeb3d5 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -466,16 +466,21 @@ class ConfigContainer: _prefix: The __getattr__ chain leading up to this object. _configapi: If given, get values suitable for config.py and add errors to the given ConfigAPI object. + _pattern: The URL pattern to be used. """ - def __init__(self, config, configapi=None, prefix=''): + def __init__(self, config, configapi=None, prefix='', pattern=None): self._config = config self._prefix = prefix self._configapi = configapi + self._pattern = pattern + if configapi is None and pattern is not None: + raise TypeError("Can't use pattern without configapi!") def __repr__(self): return utils.get_repr(self, constructor=True, config=self._config, - configapi=self._configapi, prefix=self._prefix) + configapi=self._configapi, prefix=self._prefix, + pattern=self._pattern) @contextlib.contextmanager def _handle_error(self, action, name): @@ -503,7 +508,7 @@ class ConfigContainer: if configdata.is_valid_prefix(name): return ConfigContainer(config=self._config, configapi=self._configapi, - prefix=name) + prefix=name, pattern=self._pattern) with self._handle_error('getting', name): if self._configapi is None: @@ -511,7 +516,8 @@ class ConfigContainer: return self._config.get(name) else: # access from config.py - return self._config.get_mutable_obj(name) + return self._config.get_mutable_obj( + name, pattern=self._pattern) def __setattr__(self, attr, value): """Set the given option in the config.""" @@ -521,7 +527,7 @@ class ConfigContainer: name = self._join(attr) with self._handle_error('setting', name): - self._config.set_obj(name, value) + self._config.set_obj(name, value, pattern=self._pattern) def _join(self, attr): """Get the prefix joined with the given attribute.""" diff --git a/qutebrowser/config/configfiles.py b/qutebrowser/config/configfiles.py index 622e116fd..8b16c701a 100644 --- a/qutebrowser/config/configfiles.py +++ b/qutebrowser/config/configfiles.py @@ -348,6 +348,16 @@ class ConfigAPI: except configexc.ConfigFileErrors as e: self.errors += e.errors + @contextlib.contextmanager + def pattern(self, pattern): + """Get a ConfigContainer for the given pattern.""" + # We need to propagate the exception so we don't need to return + # something. + urlpattern = urlmatch.UrlPattern(pattern) + container = config.ConfigContainer(config=self._config, configapi=self, + pattern=urlpattern) + yield container + class ConfigPyWriter: diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py index 4be4d8dec..095bfa78d 100644 --- a/tests/unit/config/test_config.py +++ b/tests/unit/config/test_config.py @@ -637,6 +637,12 @@ class TestContainer: assert error.text == "While getting 'tabs.foobar'" assert str(error.exception) == "No option 'tabs.foobar'" + def test_pattern_no_configapi(self, config_stub): + pattern = urlmatch.UrlPattern('https://example.com/') + with pytest.raises(TypeError, + match="Can't use pattern without configapi!"): + config.ConfigContainer(config_stub, pattern=pattern) + class StyleObj(QObject): diff --git a/tests/unit/config/test_configfiles.py b/tests/unit/config/test_configfiles.py index 56ed1c635..0e173e8ff 100644 --- a/tests/unit/config/test_configfiles.py +++ b/tests/unit/config/test_configfiles.py @@ -507,17 +507,29 @@ class TestConfigPy: confpy.read() assert config.instance.get_obj('colors.hints.bg') == 'red' - def test_set_with_pattern(self, confpy): + @pytest.mark.parametrize('template', [ + "config.set({opt!r}, False, {pattern!r})", + "with config.pattern({pattern!r}) as p: p.{opt} = False", + ]) + def test_set_with_pattern(self, confpy, template): option = 'content.javascript.enabled' pattern = 'https://www.example.com/' - confpy.write('config.set({!r}, False, {!r})'.format(option, pattern)) + confpy.write(template.format(opt=option, pattern=pattern)) confpy.read() assert config.instance.get_obj(option) assert not config.instance.get_obj_for_pattern( option, pattern=urlmatch.UrlPattern(pattern)) + def test_set_context_manager_global(self, confpy): + """When "with config.pattern" is used, "c." should still be global.""" + option = 'content.javascript.enabled' + confpy.write('with config.pattern("https://www.example.com/") as p:' + ' c.{} = False'.format(option)) + confpy.read() + assert not config.instance.get_obj(option) + @pytest.mark.parametrize('set_first', [True, False]) @pytest.mark.parametrize('get_line', [ 'c.colors.hints.fg', @@ -703,20 +715,21 @@ class TestConfigPy: "'qt.args')") assert str(error.exception) == expected - @pytest.mark.parametrize('line, action', [ - ('config.get("content.images", "://")', 'getting'), - ('config.set("content.images", False, "://")', 'setting'), + @pytest.mark.parametrize('line, text', [ + ('config.get("content.images", "://")', + "While getting 'content.images' and parsing pattern"), + ('config.set("content.images", False, "://")', + "While setting 'content.images' and parsing pattern"), + ('with config.pattern("://"): pass', + "Unhandled exception"), ]) - def test_invalid_pattern(self, confpy, line, action): + def test_invalid_pattern(self, confpy, line, text): confpy.write(line) - error = confpy.read(error=True) - assert error.text == ("While {} 'content.images' and parsing pattern" - .format(action)) + assert error.text == text assert isinstance(error.exception, urlmatch.ParseError) assert str(error.exception) == "No scheme given" - assert error.traceback is None def test_multiple_errors(self, confpy): confpy.write("c.foo = 42", "config.set('foo', 42)", "1/0")