Source code for evariste.hooks
# Copyright Louis Paternault 2015-2022
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Implement hook mechanism.
See :ref:`hooks` for more information.
.. note::
This implementation of hooks rely on other parts of Évariste
(:mod:`~evariste.plugins` for example),
and cannot be used separatedly.
Example
-------
.. code-block:: python
:caption: Example of hook mechanism
import contextlib
from evariste import hooks
class A:
@hooks.setmethodhook()
def a(self):
print("Running A.a()…")
class B:
@hooks.contexthook("A.a"):
@contextlib.contextmanager
def b(self):
print("Before running A.a()…").
yield
print("After running A.a()…").
# Let's go!
A.a()
In this example, the ``A.a()`` method has been marked as accepting hooks,
and the ``B.b()`` method has been registered as a hook for ``A.a()``.
When ``A.a()`` is run (last line of the example),
although ``B`` has not been called directly,
``B.b()`` is called as well, as a registered hook. The output of this example is::
Before running A.a()…
Running A.a()…
After running A.a()…
Get functions registered as hooks
---------------------------------
Method hooks
------------
Methods can be marked to accept hooks using the following function.
.. autofunction:: setmethodhook
Context hooks
-------------
Context hooks cannot be directly defined: every method hook is also a context hook.
Iteration hooks
---------------
Iteration hooks can be executed using :meth:`~evariste.plugins.Loader.applyiterhook`.
Register functions as hooks
---------------------------
.. autofunction:: hook
.. autofunction:: contexthook
.. autofunction:: methodhook
.. autofunction:: iterhook
"""
import collections
import functools
import typing
[docs]
def hook(hooktype: str, name: str) -> typing.Callable:
"""Decorator to register a function or method as a hook.
:param str hooktype:
Type of hook (``"methodhook"`` or ``"contexthook"``, or whatever string you want).
:param str name:
Name of the target hook, of the form ``Class.methodname``
(or ``Class`` only for the ``__init__`` method).
"""
def wrapper(function):
@functools.wraps(function)
def wrapped(*args, **kwargs):
return function(*args, **kwargs)
hooked = getattr(function, "hooked", collections.defaultdict(set))
hooked[hooktype].add(name)
setattr(wrapped, "hooked", hooked)
return wrapped
return wrapper
[docs]
def methodhook(name: str) -> typing.Callable:
"""Decorator to register a function or method as a method hook.
For any string ``name``, ``methodhook(name)`` is a shortcut for ``hook("methodhook", name)``.
"""
return hook("methodhook", name)
[docs]
def contexthook(name: str) -> typing.Callable:
"""Decorator to register a function or method as a context hook.
For any string ``name``, ``contexthook(name)`` is a shortcut for ``hook("contexthook", name)``.
"""
return hook("contexthook", name)
[docs]
def iterhook(name: str) -> typing.Callable:
"""Decorator to register a function or method as an iter hook.
For any string ``name``, ``iterhook(name)`` is a shortcut for ``hook("iterhook", name)``.
"""
return hook("iterhook", name)
def iter_hooks(instance):
"""Iterates over the method of `instance` registered as hooks."""
for attrname in dir(instance):
attr = getattr(instance, attrname)
if callable(attr) and hasattr(attr, "hooked"):
for hooktype, hooks in getattr(attr, "hooked").items():
for item in hooks:
yield hooktype, item, attr
[docs]
def setmethodhook(
*, getter: typing.Union[None, typing.Callable] = None
) -> typing.Callable:
"""Decorator to mark that a method can accept method and context :ref:`hooks`.
:param function getter:
Function that, given the instance object as argument,
returns a :class:`plugins.Loader` object.
If ``None``, the default ``self.shared.builder.plugins`` is used
(``self`` is supposed to have this attribute).
"""
def decorator(function):
"""Actual decorator."""
if function.__name__ == "__init__":
hookname = function.__qualname__[: -len(function.__name__) - 1]
else:
hookname = function.__qualname__
@functools.wraps(function)
def wrapped(*args, **kwargs):
"""Wrapped function."""
self = args[0]
if getter is None:
plugins = self.shared.builder.plugins
else:
plugins = getter(*args, **kwargs)
return plugins.applymethodhook(hookname, function, *args, **kwargs)
return wrapped
return decorator