From 38ebaf0e79a76b3c172c98e7fa3824e97a69e5fe Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Fri, 17 Dec 2021 14:27:27 +0100 Subject: [PATCH] feat(hooks): Some more work on the hooks functions --- flaschengeist/app.py | 7 +- flaschengeist/plugins/__init__.py | 21 +++-- flaschengeist/plugins/auth_plain/__init__.py | 5 +- flaschengeist/utils/hook.py | 98 +++++++++++++------- 4 files changed, 84 insertions(+), 47 deletions(-) diff --git a/flaschengeist/app.py b/flaschengeist/app.py index 8259b68..a367fe8 100644 --- a/flaschengeist/app.py +++ b/flaschengeist/app.py @@ -12,6 +12,7 @@ from . import logger from .plugins import AuthPlugin from flaschengeist.config import config, configure_app from flaschengeist.controller import roleController +from flaschengeist.utils.hook import Hook class CustomJSONEncoder(JSONEncoder): @@ -35,6 +36,7 @@ class CustomJSONEncoder(JSONEncoder): return JSONEncoder.default(self, o) +@Hook("plugins.loaded") def __load_plugins(app): logger.debug("Search for plugins") @@ -77,23 +79,20 @@ def __load_plugins(app): raise RuntimeError("No authentication plugin configured or authentication plugin not found") +@Hook("plugins.installed") def install_all(): from flaschengeist.database import db db.create_all() db.session.commit() - installed = [] for name, plugin in current_app.config["FG_PLUGINS"].items(): if not plugin: logger.debug(f"Skip disabled plugin: {name}") continue logger.info(f"Install plugin {name}") plugin.install() - installed.append(plugin) if plugin.permissions: roleController.create_permissions(plugin.permissions) - for plugin in installed: - plugin.post_install() def create_app(test_config=None): diff --git a/flaschengeist/plugins/__init__.py b/flaschengeist/plugins/__init__.py index 0c33ea9..1b6f701 100644 --- a/flaschengeist/plugins/__init__.py +++ b/flaschengeist/plugins/__init__.py @@ -10,6 +10,21 @@ from flaschengeist.models.user import _Avatar, User from flaschengeist.models.setting import _PluginSetting from flaschengeist.utils.hook import HookBefore, HookAfter +plugins_installed = HookAfter("plugins.installed") +"""Hook decorator for when all plugins are installed + Possible use case would be to populate the database with some presets. + + Args: + hook_result: void (kwargs) +""" +plugins_loaded = HookAfter("plugins.loaded") +"""Hook decorator for when all plugins are loaded + Possible use case would be to check if a specific other plugin is loaded and change own behavior + + Args: + app: Current flask app instance (args) + hook_result: void (kwargs) +""" before_role_updated = HookBefore("update_role") """Hook decorator for when roles are modified Args: @@ -57,12 +72,6 @@ class Plugin: """ pass - def post_install(self): - """Fill database or do other stuff - Called after all plugins are installed - """ - pass - def get_setting(self, name: str, **kwargs): """Get plugin setting from database Args: diff --git a/flaschengeist/plugins/auth_plain/__init__.py b/flaschengeist/plugins/auth_plain/__init__.py index 4fce6c6..a92e5c6 100644 --- a/flaschengeist/plugins/auth_plain/__init__.py +++ b/flaschengeist/plugins/auth_plain/__init__.py @@ -7,13 +7,16 @@ import os import hashlib import binascii from werkzeug.exceptions import BadRequest -from flaschengeist.plugins import AuthPlugin +from flaschengeist.plugins import AuthPlugin, plugins_installed from flaschengeist.models.user import User, Role, Permission from flaschengeist.database import db from flaschengeist import logger class AuthPlain(AuthPlugin): + def install(self): + plugins_installed(self.post_install) + def post_install(self): if User.query.filter(User.deleted == False).count() == 0: logger.info("Installing admin user") diff --git a/flaschengeist/utils/hook.py b/flaschengeist/utils/hook.py index 02db156..b028f30 100644 --- a/flaschengeist/utils/hook.py +++ b/flaschengeist/utils/hook.py @@ -1,43 +1,69 @@ -_hook_dict = ({}, {}) +from functools import wraps -class Hook(object): - """Decorator for Hooks - Use to decorate system hooks where plugins should be able to hook in +_hooks_before = {} +_hooks_after = {} + + +def Hook(function=None, id=None): + """Hook decorator + Use to decorate functions as hooks, so plugins can hook up their custom functions. + """ + # `id` passed as `arg` not `kwarg` + if isinstance(function, str): + return Hook(id=function) + + def decorator(function): + @wraps(function) + def wrapped(*args, **kwargs): + _id = id if id is not None else function.__qualname__ + # Hooks before + for f in _hooks_before.get(_id, []): + f(*args, **kwargs) + # Main function + result = function(*args, **kwargs) + # Hooks after + for f in _hooks_after.get(_id, []): + f(*args, hook_result=result, **kwargs) + return result + + return wrapped + + # Called @Hook or we are in the second step + if callable(function): + return decorator(function) + else: + return decorator + + +def HookBefore(id: str): + """Decorator for functions to be called before a Hook-Function is called + The hooked up function must accept the same arguments as the function hooked onto, + as the functions are called with the same arguments. + Hint: This enables you to modify the arguments! + """ + if not id or not isinstance(id, str): + raise TypeError("HookBefore requires the ID of the function to hook up") + + def wrapped(function): + _hooks_before.setdefault(id, []).append(function) + return function + + return wrapped + + +def HookAfter(id: str): + """Decorator for functions to be called after a Hook-Function is called + As with the HookBefore, the hooked up function must accept the same + arguments as the function hooked onto, but also receives a + `hook_result` kwarg containing the result of the function. """ - def __init__(self, function): - self.function = function + if not id or not isinstance(id, str): + raise TypeError("HookAfter requires the ID of the function to hook up") - def __call__(self, *args, **kwargs): - # Hooks before - for function in _hook_dict[0].get(self.function.__name__, []): - function(*args, **kwargs) - # Main function - ret = self.function(*args, **kwargs) - # Hooks after - for function in _hook_dict[1].get(self.function.__name__, []): - function(*args, **kwargs) - return ret - - -class HookBefore(object): - """Decorator for functions to be called before a Hook-Function is called""" - - def __init__(self, name): - self.name = name - - def __call__(self, function): - _hook_dict[0].setdefault(self.name, []).append(function) + def wrapped(function): + _hooks_after.setdefault(id, []).append(function) return function - -class HookAfter(object): - """Decorator for functions to be called after a Hook-Function is called""" - - def __init__(self, name): - self.name = name - - def __call__(self, function): - _hook_dict[1].setdefault(self.name, []).append(function) - return function + return wrapped