diff --git a/qutebrowser/api/config.py b/qutebrowser/api/config.py index c2831de40..4a5d73936 100644 --- a/qutebrowser/api/config.py +++ b/qutebrowser/api/config.py @@ -24,8 +24,6 @@ import typing from PyQt5.QtCore import QUrl from qutebrowser.config import config -# pylint: disable=unused-import -from qutebrowser.config.config import change_filter val = typing.cast('config.ConfigContainer', None) diff --git a/qutebrowser/api/hook.py b/qutebrowser/api/hook.py index ddec49e5f..a975438ea 100644 --- a/qutebrowser/api/hook.py +++ b/qutebrowser/api/hook.py @@ -26,14 +26,31 @@ import typing from qutebrowser.extensions import loader +def _add_module_info(func: typing.Callable) -> loader.ModuleInfo: + """Add module info to the given function.""" + module = importlib.import_module(func.__module__) + return loader.add_module_info(module) + + class init: # noqa: N801,N806 pylint: disable=invalid-name """Decorator to mark a function to run when initializing.""" def __call__(self, func: typing.Callable) -> typing.Callable: - module = importlib.import_module(func.__module__) - info = loader.add_module_info(module) + info = _add_module_info(func) if info.init_hook is not None: raise ValueError("init hook is already registered!") info.init_hook = func return func + + +class config_changed: + + """Decorator to get notified about changed configs.""" + + def __init__(self, option_filter=None): + self._filter = option_filter + + def __call__(self, func: typing.Callable) -> typing.Callable: + info = _add_module_info(func) + info.config_changed_hooks.append((self._filter, func)) diff --git a/qutebrowser/components/adblock.py b/qutebrowser/components/adblock.py index 85d8fe40b..ae71722d6 100644 --- a/qutebrowser/components/adblock.py +++ b/qutebrowser/components/adblock.py @@ -294,7 +294,6 @@ class HostBlocker: message.info("adblock: Read {} hosts from {} sources.".format( len(self._blocked_hosts), self._done_count)) - @config.change_filter('content.host_blocking.lists') def update_files(self): """Update files when the config changed.""" if not config.val.content.host_blocking.lists: @@ -337,6 +336,11 @@ def adblock_update(): _host_blocker.adblock_update() +@hook.config_changed('content.host_blocking.lists') +def on_config_changed(): + _host_blocker.update_files() + + @hook.init() def init(context): global _host_blocker @@ -344,6 +348,4 @@ def init(context): config_dir=context.config_dir, args=context.args) _host_blocker.read_hosts() - - context.signals.config_changed.connect(_host_blocker.update_files) requests.register_filter(_host_blocker.filter_request) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 80826beeb..201b87fde 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -86,7 +86,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name not configdata.is_valid_prefix(self._option)): raise configexc.NoOptionError(self._option) - def _check_match(self, option: typing.Optional[str]) -> bool: + def check_match(self, option: typing.Optional[str]) -> bool: """Check if the given option matches the filter.""" if option is None: # Called directly, not from a config change event. @@ -119,7 +119,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name @functools.wraps(func) def func_wrapper(option: str = None) -> typing.Any: """Call the underlying function.""" - if self._check_match(option): + if self.check_match(option): return func() return None return func_wrapper @@ -128,7 +128,7 @@ class change_filter: # noqa: N801,N806 pylint: disable=invalid-name def meth_wrapper(wrapper_self: typing.Any, option: str = None) -> typing.Any: """Call the underlying function.""" - if self._check_match(option): + if self.check_match(option): return func(wrapper_self) return None return meth_wrapper diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py index e7b93967b..bc4494bae 100644 --- a/qutebrowser/extensions/loader.py +++ b/qutebrowser/extensions/loader.py @@ -28,13 +28,17 @@ import pathlib import attr -from PyQt5.QtCore import pyqtSignal, QObject +from PyQt5.QtCore import pyqtSlot, QObject from qutebrowser import components from qutebrowser.config import config from qutebrowser.utils import log, standarddir, objreg +# ModuleInfo objects for all loaded plugins +_module_infos = [] + + @attr.s class InitContext: @@ -43,14 +47,6 @@ class InitContext: data_dir = attr.ib() # type: pathlib.Path config_dir = attr.ib() # type: pathlib.Path args = attr.ib() # type: argparse.Namespace - signals = attr.ib() # type: ExtensionSignals - - -class ExtensionSignals(QObject): - - """Signals exposed to an extension.""" - - config_changed = pyqtSignal(str) @attr.s @@ -61,7 +57,11 @@ class ModuleInfo: This gets used by qutebrowser.api.hook. """ + _ConfigChangedHooksType = typing.List[typing.Tuple[str, typing.Callable]] + init_hook = attr.ib(None) # type: typing.Optional[typing.Callable] + config_changed_hooks = attr.ib( + attr.Factory(list)) # type: _ConfigChangedHooksType @attr.s @@ -72,12 +72,6 @@ class ExtensionInfo: name = attr.ib() # type: str -# Global extension signals, shared between all extensions. -# At some point we might want to make this per-extension, but then we'll need -# to find out what to set as its Qt parent so it's kept alive. -_extension_signals = ExtensionSignals() - - def add_module_info(module: types.ModuleType) -> ModuleInfo: """Add ModuleInfo to a module (if not added yet).""" # pylint: disable=protected-access @@ -138,8 +132,7 @@ def _get_init_context() -> InitContext: """Get an InitContext object.""" return InitContext(data_dir=pathlib.Path(standarddir.data()), config_dir=pathlib.Path(standarddir.config()), - args=objreg.get('args'), - signals=_extension_signals) + args=objreg.get('args')) def _load_component(info: ExtensionInfo) -> types.ModuleType: @@ -153,8 +146,21 @@ def _load_component(info: ExtensionInfo) -> types.ModuleType: .format(mod_info.init_hook.__name__)) mod_info.init_hook(_get_init_context()) + _module_infos.append(mod_info) + return mod +@pyqtSlot(str) +def _on_config_changed(changed_name: str) -> None: + """Call config_changed hooks if the config changed.""" + for mod_info in _module_infos: + for option, hook in mod_info.config_changed_hooks: + cfilter = config.change_filter(option) + cfilter.validate() + if cfilter.check_match(changed_name): + hook() + + def init() -> None: - config.instance.changed.connect(_extension_signals.config_changed) + config.instance.changed.connect(_on_config_changed)