def hookable(function=None, name=None):
    """Adds before and after hooks to any method.

    :param function: function to be decorated
    :param name: name of the method to be decorated (default to method name)
    :return: decorated function

    Usage:

        class MyHookableClass(Settings):
            @hookable
            def execute_loaders(....):
                # do whatever you want here
                return super().execute_loaders(....)

        settings = Dynaconf(_wrapper_class=MyHookableClass)

        def hook_function(temp_settings, value, ...):
            # do whatever you want here
            return value

        settings.add_hook("after_execute_loaders", Hook(function))

        settings.FOO
        # will trigger execute_loaders
        # -> will trigger the hookable method
        # -> will execute registered hooks

    see tests/test_hooking.py for more examples.
    """

    if function and not callable(function):
        raise TypeError("hookable must be applied with named arguments only")

    def dispatch(fun, self, *args, **kwargs):
        """calls the decorated function and its hooks"""

        # if object has no hooks, return the original
        if not (_registered_hooks := get_hooks(self)):
            return fun(self, *args, **kwargs)

        function_name = name or fun.__name__

        # function being called not in the list of hooks, return the original
        if not set(_registered_hooks).intersection(
            (f"before_{function_name}", f"after_{function_name}")
        ):
            return fun(self, *args, **kwargs)

        # Create an unhook-able (to avoid recursion)
        # temporary settings to pass to the hooked function
        temp_settings = TempSettingsHolder(self)

        def _hook(action: str, value: HookValue) -> HookValue:
            """executes the hooks for the given action"""
            hooks = _registered_hooks.get(f"{action}_{function_name}", [])
            for hook in hooks:
                value = hook.function(temp_settings, value, *args, **kwargs)
                value = HookValue.new(value)
            return value

        # Value starts as en empty value on the first before hook
        value = _hook("before", HookValue(EMPTY_VALUE))

        # If the value is EagerValue, it means main function should not be
        # executed and the value should go straight to the after hooks if any
        original_value = EMPTY_VALUE
        if not isinstance(value, EagerValue):
            value = MethodValue(fun(self, *args, **kwargs))
            original_value = value.value

        value = _hook("after", value)

        # track the loading history
        # adding inspect history like:
        # "identifier": "get_hook_(read_settings_from_cache_or_db)"
        if value.value != original_value and function_name == "get":
            hook_names = "_".join(
                [
                    hook.function.__name__
                    for list_of_hooks in _registered_hooks.values()
                    for hook in list_of_hooks
                ]
            )
            metadata = SourceMetadata(
                loader="hooking",
                identifier=f"{function_name}_hook_({hook_names})",
                merged=True,
            )
            history = self._loaded_by_loaders.setdefault(metadata, {})
            key = args[0] if args else kwargs.get("key")
            history[key] = value.value

        # unwrap the value from the HookValue so it can be returned
        # normally to the caller
        return value.value

    if function:
        # decorator applied without parameters e.g: @hookable
        @wraps(function)
        def wrapper(*args, **kwargs):
            return dispatch(function, *args, **kwargs)

        wrapper.original_function = function
        return wrapper

    def decorator(function):
        # decorator applied with parameters e.g: @hookable(before=False)
        @wraps(function)
        def wrapper(*args, **kwargs):
            return dispatch(function, *args, **kwargs)

        wrapper.original_function = function
        return wrapper

    return decorator
Adds before and after hooks to any method.

    :param function: function to be decorated
    :param name: name of the method to be decorated (default to method name)
    :return: decorated function

    Usage:

        class MyHookableClass(Settings):
            @hookable
            def execute_loaders(....):
                # do whatever you want here
                return super().execute_loaders(....)

        settings = Dynaconf(_wrapper_class=MyHookableClass)

        def hook_function(temp_settings, value, ...):
            # do whatever you want here
            return value

        settings.add_hook("after_execute_loaders", Hook(function))

        settings.FOO
        # will trigger execute_loaders
        # -> will trigger the hookable method
        # -> will execute registered hooks

    see tests/test_hooking.py for more examples.
    
dynaconf/hooking.py