config: Improve error handling for missing sections

When the user does something like this with the new adblocking config:

    c.content.host_blocking.lists = ...

Then we didn't notify her about the setting being renamed. Instead, she
got:

    While getting 'content.host_blocking': No option 'content.host_blocking'

    Unhandled exception: 'NoneType' object has no attribute 'lists'
    Traceback (most recent call last):
      File ".../qutebrowser/config/configfiles.py", line 805, in read_config_py
        exec(code, module.__dict__)
      File "/tmp/config.py", line 1, in <module>
        c.content.host_blocking.lists = []
    AttributeError: 'NoneType' object has no attribute 'lists'

This happens because we did something like (simplified):

    with self._handle_error(...):
        return self._config.get(...)
    # End of method

Thus, if there was an error, nothing is returned and the method ends,
therefore returning an implicit None. When then trying to access .lists
on that None, we get the AttributeError.

Instead, we now permit this kind of wrong usage in config.py files.
If this is a qutebrowser-internal ConfigContainer, we would've already
raised in _handle_error() anyways.

What we now do instead is returning a new ConfigContainer, i.e.
allowing further access (while still capturing the error message).
Thus, this now leads to:

    While getting 'content.host_blocking':
    No option 'content.host_blocking'

    While setting 'content.host_blocking.lists':
    No option 'content.host_blocking.lists'
    (this option was renamed to 'content.blocking.hosts.lists')

This still isn't optimal, but the best we can do without more magic:
At the point the first failure happens, we can't tell whether the user
wants to get an option or is just getting a prefix.

Fixes #5991
This commit is contained in:
Florian Bruhin 2021-01-20 13:57:09 +01:00
parent 729e098bae
commit 2c382a761d
2 changed files with 24 additions and 0 deletions

View File

@ -603,6 +603,13 @@ class ConfigContainer:
return self._config.get_mutable_obj(
name, pattern=self._pattern)
# If we arrived here, there was an error while getting the config option. Most
# likely, someone did something like "c.content.host_blocking.lists" but
# "c.content.host_blocking" doesn't actually exist. To avoid an AttributeError
# which leads to a confusing error message, return another ConfigContainer so
# that the chain can keep going.
return self._with_prefix(name)
def __setattr__(self, attr: str, value: Any) -> None:
"""Set the given option in the config."""
if attr.startswith('_'):

View File

@ -761,6 +761,23 @@ class TestContainer:
assert error.text == "While getting 'tabs.foobar'"
assert str(error.exception) == "No option 'tabs.foobar'"
def test_confapi_missing_prefix(self, container):
configapi = types.SimpleNamespace(errors=[])
container._configapi = configapi
container.content.host_blocking.lists = []
assert len(configapi.errors) == 2
error1 = configapi.errors[0]
assert error1.text == "While getting 'content.host_blocking'"
assert str(error1.exception) == "No option 'content.host_blocking'"
error2 = configapi.errors[1]
assert error2.text == "While setting 'content.host_blocking.lists'"
assert str(error2.exception) == (
"No option 'content.host_blocking.lists' (this option was renamed to "
"'content.blocking.hosts.lists')")
def test_pattern_no_configapi(self, config_stub):
pattern = urlmatch.UrlPattern('https://example.com/')
with pytest.raises(TypeError,