qutebrowser / qutebrowser-extensions

Extension ideas (and at some point code) for qutebrowser
31 stars 1 forks source link

Find out how to load extensions dynamically #5

Closed The-Compiler closed 5 years ago

The-Compiler commented 5 years ago

First approach with the "components" package shipped with qutebrowser:

From 8dca7e07e98cb4e724ac1f82c7e3de0be8592c3b Mon Sep 17 00:00:00 2001
From: Florian Bruhin <me@the-compiler.org>
Date: Mon, 10 Dec 2018 09:38:23 +0100
Subject: [PATCH 1/3] Load components dynamically

---
 qutebrowser/app.py                 |  4 ++--
 qutebrowser/extensions/__init__.py |  0
 qutebrowser/extensions/loader.py   | 35 ++++++++++++++++++++++++++++++
 qutebrowser/utils/log.py           |  3 ++-
 scripts/dev/run_vulture.py         |  3 +++
 scripts/dev/src2asciidoc.py        |  2 ++
 6 files changed, 44 insertions(+), 3 deletions(-)
 create mode 100644 qutebrowser/extensions/__init__.py
 create mode 100644 qutebrowser/extensions/loader.py

diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index 6c948e10c8..65c7395eb0 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -68,6 +68,7 @@
 from qutebrowser.browser.network import proxy
 from qutebrowser.browser.webkit import cookies, cache
 from qutebrowser.browser.webkit.network import networkmanager
+from qutebrowser.extensions import loader
 from qutebrowser.keyinput import macros
 from qutebrowser.mainwindow import mainwindow, prompt
 from qutebrowser.misc import (readline, ipc, savemanager, sessions,
@@ -77,8 +78,6 @@
                                usertypes, standarddir, error, qtutils)
 # pylint: disable=unused-import
 # We import those to run the cmdutils.register decorators.
-from qutebrowser.components import (scrollcommands, caretcommands,
-                                    zoomcommands, misccommands)
 from qutebrowser.mainwindow.statusbar import command
 from qutebrowser.misc import utilcmds
 # pylint: enable=unused-import
@@ -166,6 +165,7 @@ def init(args, crash_handler):
     qApp.setQuitOnLastWindowClosed(False)
     _init_icon()

+    loader.load_components()
     try:
         _init_modules(args, crash_handler)
     except (OSError, UnicodeDecodeError, browsertab.WebTabError) as e:
diff --git a/qutebrowser/extensions/__init__.py b/qutebrowser/extensions/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py
new file mode 100644
index 0000000000..9b5aadd25c
--- /dev/null
+++ b/qutebrowser/extensions/loader.py
@@ -0,0 +1,35 @@
+# 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 pkgutil
+
+from qutebrowser import components
+from qutebrowser.utils import log
+
+
+def load_components() -> None:
+    """Load everything from qutebrowser.components."""
+    for info in pkgutil.walk_packages(components.__path__):
+        if info.ispkg:
+            continue
+        log.extensions.debug("Importing {}".format(info.name))
+        loader = info.module_finder.find_module(info.name)
+        loader.load_module(info.name)
diff --git a/qutebrowser/utils/log.py b/qutebrowser/utils/log.py
index bbc0255158..115c53352f 100644
--- a/qutebrowser/utils/log.py
+++ b/qutebrowser/utils/log.py
@@ -137,6 +137,7 @@ def vdebug(self, msg, *args, **kwargs):
 network = logging.getLogger('network')
 sql = logging.getLogger('sql')
 greasemonkey = logging.getLogger('greasemonkey')
+extensions = logging.getLogger('extensions')

 LOGGER_NAMES = [
     'statusbar', 'completion', 'init', 'url',
@@ -146,7 +147,7 @@ def vdebug(self, msg, *args, **kwargs):
     'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
     'save', 'message', 'config', 'sessions',
     'webelem', 'prompt', 'network', 'sql',
-    'greasemonkey'
+    'greasemonkey', 'extensions',
 ]

diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index f3217694ee..7874f6a796 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -30,6 +30,7 @@
 import vulture

 import qutebrowser.app  # pylint: disable=unused-import
+from qutebrowser.extensions import loader
 from qutebrowser.misc import objects
 from qutebrowser.utils import utils
 from qutebrowser.browser.webkit import rfc6266
@@ -43,6 +44,8 @@

 def whitelist_generator():  # noqa
     """Generator which yields lines to add to a vulture whitelist."""
+    loader.load_components()
+
     # qutebrowser commands
     for cmd in objects.commands.values():
         yield utils.qualname(cmd.handler)
diff --git a/scripts/dev/src2asciidoc.py b/scripts/dev/src2asciidoc.py
index ba4e9b69c7..f0536c045b 100755
--- a/scripts/dev/src2asciidoc.py
+++ b/scripts/dev/src2asciidoc.py
@@ -35,6 +35,7 @@
 # We import qutebrowser.app so all @cmdutils-register decorators are run.
 import qutebrowser.app
 from qutebrowser import qutebrowser, commands
+from qutebrowser.extensions import loader
 from qutebrowser.commands import argparser
 from qutebrowser.config import configdata, configtypes
 from qutebrowser.utils import docutils, usertypes
@@ -549,6 +550,7 @@ def regenerate_cheatsheet():
 def main():
     """Regenerate all documentation."""
     utils.change_cwd()
+    loader.load_components()
     print("Generating manpage...")
     regenerate_manpage('doc/qutebrowser.1.asciidoc')
     print("Generating settings help...")

From f0ea11b8e2f8f0307a32627e4b5614965996e3ae Mon Sep 17 00:00:00 2001
From: Florian Bruhin <me@the-compiler.org>
Date: Mon, 10 Dec 2018 10:06:07 +0100
Subject: [PATCH 2/3] Add types to extensions.loader

---
 mypy.ini                         |  4 ++++
 qutebrowser/extensions/loader.py | 18 +++++++++++++-----
 2 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/mypy.ini b/mypy.ini
index 4526e4e48c..8fb8d89ae3 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -73,3 +73,7 @@ disallow_incomplete_defs = True
 [mypy-qutebrowser.components.*]
 disallow_untyped_defs = True
 disallow_incomplete_defs = True
+
+[mypy-qutebrowser.extensions.*]
+disallow_untyped_defs = True
+disallow_incomplete_defs = True
diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py
index 9b5aadd25c..9674ad7079 100644
--- a/qutebrowser/extensions/loader.py
+++ b/qutebrowser/extensions/loader.py
@@ -19,7 +19,9 @@

 """Loader for qutebrowser extensions."""

+import importlib.abc
 import pkgutil
+import types

 from qutebrowser import components
 from qutebrowser.utils import log
@@ -27,9 +29,15 @@

 def load_components() -> None:
     """Load everything from qutebrowser.components."""
-    for info in pkgutil.walk_packages(components.__path__):
-        if info.ispkg:
+    for finder, name, ispkg in pkgutil.walk_packages(components.__path__):
+        if ispkg:
             continue
-        log.extensions.debug("Importing {}".format(info.name))
-        loader = info.module_finder.find_module(info.name)
-        loader.load_module(info.name)
+        _load_module(finder, name)
+
+
+def _load_module(finder: importlib.abc.PathEntryFinder,
+                 name: str) -> types.ModuleType:
+    log.extensions.debug("Importing {}".format(name))
+    loader = finder.find_module(name)
+    assert loader is not None
+    return loader.load_module(name)

From a17dde31964c3c81cbeb4c33c036d9a86303d81e Mon Sep 17 00:00:00 2001
From: Florian Bruhin <me@the-compiler.org>
Date: Mon, 10 Dec 2018 10:26:25 +0100
Subject: [PATCH 3/3] Add components to pyinstaller hiddenimports

---
 misc/qutebrowser.spec            | 11 ++++++++++-
 qutebrowser/extensions/loader.py | 27 +++++++++++++++++++++------
 2 files changed, 31 insertions(+), 7 deletions(-)

diff --git a/misc/qutebrowser.spec b/misc/qutebrowser.spec
index ff1b10577a..b40172754f 100644
--- a/misc/qutebrowser.spec
+++ b/misc/qutebrowser.spec
@@ -6,6 +6,8 @@ import os
 sys.path.insert(0, os.getcwd())
 from scripts import setupcommon

+from qutebrowser.extensions import loader
+
 block_cipher = None

@@ -27,6 +29,13 @@ def get_data_files():
     return data_files

+def get_hidden_imports():
+    imports = ['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0']
+    for info in loader.walk_components():
+        imports.append('qutebrowser.components.' + info.name)
+    return imports
+
+
 setupcommon.write_git_file()

@@ -42,7 +51,7 @@ a = Analysis(['../qutebrowser/__main__.py'],
              pathex=['misc'],
              binaries=None,
              datas=get_data_files(),
-             hiddenimports=['PyQt5.QtOpenGL', 'PyQt5._QOpenGLFunctions_2_0'],
+             hiddenimports=get_hidden_imports(),
              hookspath=[],
              runtime_hooks=[],
              excludes=['tkinter'],
diff --git a/qutebrowser/extensions/loader.py b/qutebrowser/extensions/loader.py
index 9674ad7079..d6fdc675ee 100644
--- a/qutebrowser/extensions/loader.py
+++ b/qutebrowser/extensions/loader.py
@@ -22,22 +22,37 @@
 import importlib.abc
 import pkgutil
 import types
+import typing
+
+import attr

 from qutebrowser import components
 from qutebrowser.utils import log

+@attr.s
+class ComponentInfo:
+
+    name = attr.ib()  # type: str
+    finder = attr.ib()  # type: importlib.abc.PathEntryFinder
+
+
 def load_components() -> None:
     """Load everything from qutebrowser.components."""
+    for info in walk_components():
+        _load_component(info)
+
+
+def walk_components() -> typing.Iterator[ComponentInfo]:
+    """Yield ComponentInfo objects for all modules."""
     for finder, name, ispkg in pkgutil.walk_packages(components.__path__):
         if ispkg:
             continue
-        _load_module(finder, name)
+        yield ComponentInfo(name=name, finder=finder)

-def _load_module(finder: importlib.abc.PathEntryFinder,
-                 name: str) -> types.ModuleType:
-    log.extensions.debug("Importing {}".format(name))
-    loader = finder.find_module(name)
+def _load_component(info: ComponentInfo) -> types.ModuleType:
+    log.extensions.debug("Importing {}".format(info.name))
+    loader = info.finder.find_module(info.name)
     assert loader is not None
-    return loader.load_module(name)
+    return loader.load_module(info.name)

However, that fails to find any modules on macOS/Windows with the frozen package. On macOS, components.__path__ shows /Volumes/qutebrowser/qutebrowser.app/Contents/MacOS/qutebrowser/components with qutebrowser being the executable (i.e., that path doesn't exist at all). As a result, pkgutil.walk_packages(components.__path__) doesn't yield anything.

The-Compiler commented 5 years ago

Related: https://github.com/pyinstaller/pyinstaller/issues/1905