qutebrowser/qutebrowser/extensions/loader.py
Florian Bruhin a3279772d5 Fix lint
2018-12-10 18:34:11 +01:00

188 lines
5.5 KiB
Python

# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
"""Loader for qutebrowser extensions."""
import importlib.abc
import pkgutil
import types
import typing
import sys
import pathlib
import attr
from PyQt5.QtCore import pyqtSlot
from qutebrowser import components
from qutebrowser.config import config
from qutebrowser.utils import log, standarddir, objreg
MYPY = False
if MYPY:
# pylint: disable=unused-import,useless-suppression
import argparse
# ModuleInfo objects for all loaded plugins
_module_infos = []
@attr.s
class InitContext:
"""Context an extension gets in its init hook."""
data_dir = attr.ib() # type: pathlib.Path
config_dir = attr.ib() # type: pathlib.Path
args = attr.ib() # type: argparse.Namespace
@attr.s
class ModuleInfo:
"""Information attached to an extension module.
This gets used by qutebrowser.api.hook.
"""
_ConfigChangedHooksType = typing.List[typing.Tuple[typing.Optional[str],
typing.Callable]]
skip_hooks = attr.ib(False) # type: bool
init_hook = attr.ib(None) # type: typing.Optional[typing.Callable]
config_changed_hooks = attr.ib(
attr.Factory(list)) # type: _ConfigChangedHooksType
@attr.s
class ExtensionInfo:
"""Information about a qutebrowser extension."""
name = attr.ib() # type: str
def add_module_info(module: types.ModuleType) -> ModuleInfo:
"""Add ModuleInfo to a module (if not added yet)."""
# pylint: disable=protected-access
if not hasattr(module, '__qute_module_info'):
module.__qute_module_info = ModuleInfo() # type: ignore
return module.__qute_module_info # type: ignore
def load_components(*, skip_hooks: bool = False) -> None:
"""Load everything from qutebrowser.components."""
for info in walk_components():
_load_component(info, skip_hooks=skip_hooks)
def walk_components() -> typing.Iterator[ExtensionInfo]:
"""Yield ExtensionInfo objects for all modules."""
if hasattr(sys, 'frozen'):
yield from _walk_pyinstaller()
else:
yield from _walk_normal()
def _on_walk_error(name: str) -> None:
raise ImportError("Failed to import {}".format(name))
def _walk_normal() -> typing.Iterator[ExtensionInfo]:
"""Walk extensions when not using PyInstaller."""
for _finder, name, ispkg in pkgutil.walk_packages(
# Only packages have a __path__ attribute,
# but we're sure this is one.
path=components.__path__, # type: ignore
prefix=components.__name__ + '.',
onerror=_on_walk_error):
if ispkg:
continue
yield ExtensionInfo(name=name)
def _walk_pyinstaller() -> typing.Iterator[ExtensionInfo]:
"""Walk extensions when using PyInstaller.
See https://github.com/pyinstaller/pyinstaller/issues/1905
Inspired by:
https://github.com/webcomics/dosage/blob/master/dosagelib/loader.py
"""
toc = set() # type: typing.Set[str]
for importer in pkgutil.iter_importers('qutebrowser'):
if hasattr(importer, 'toc'):
toc |= importer.toc
for name in toc:
if name.startswith(components.__name__ + '.'):
yield ExtensionInfo(name=name)
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'))
def _load_component(info: ExtensionInfo, *,
skip_hooks: bool = False) -> types.ModuleType:
"""Load the given extension and run its init hook (if any).
Args:
skip_hooks: Whether to skip all hooks for this module.
This is used to only run @cmdutils.register decorators.
"""
log.extensions.debug("Importing {}".format(info.name))
mod = importlib.import_module(info.name)
mod_info = add_module_info(mod)
if skip_hooks:
mod_info.skip_hooks = True
if mod_info.init_hook is not None and not skip_hooks:
log.extensions.debug("Running init hook {!r}"
.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:
if mod_info.skip_hooks:
continue
for option, hook in mod_info.config_changed_hooks:
if option is None:
hook()
else:
cfilter = config.change_filter(option)
cfilter.validate()
if cfilter.check_match(changed_name):
hook()
def init() -> None:
config.instance.changed.connect(_on_config_changed)