Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/20230106_190620_regis_hooks_api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- 💥[Feature] Simplify the hooks API. Plugin developers who were previously using `hooks.actions`, `hooks.filters` or `hooks.contexts` should now import these modules explicitely. (by @regisb)
- 💥[Feature] Simplify the hooks API. The modules `tutor.hooks.actions`, `tutor.hooks.filters`, and `tutor.hooks.contexts` are no longer part of the API. This change should affect mosst developers, who only use the `Actions` and `Filters` classes (notice the plural) from `tutor.hooks`. (by @regisb)
- Instead of `tutor.hooks.actions.get("some:action")`, use `tutor.hooks.Actions.SOME_ACTION`.
- Instead of `tutor.hooks.filters.get("some:filter")`, use `tutor.hooks.Filters.SOME_FILTER`.
- Instead of `tutor.hooks.actions.add("some:action")`, use `tutor.hooks.Actions.SOME_ACTION.add()`. The same applies to the `do` method.
- Instead of `tutor.hooks.filters.add("some:filter")`, use `tutor.hooks.Filters.SOME_FILTER.add()`. The same applies to the `add_item`, `add_items`, `apply`, and `iterate` methods.
- Instead of `tutor.hooks.contexts.enter`, use `tutor.core.hooks.contexts.enter`.
49 changes: 33 additions & 16 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import io
import os
import sys
Expand All @@ -9,8 +11,8 @@
# -- Project information -----------------------------------------------------

project = "Tutor"
copyright = ""
author = "Overhang.io"
copyright = "" # pylint: disable=redefined-builtin
author = "Overhang.IO"

# The short X.Y version
version = ""
Expand All @@ -32,19 +34,36 @@
autodoc_typehints = "description"
# For the life of me I can't get the docs to compile in nitpicky mode without these
# ignore statements. You are most welcome to try and remove them.
# To make matters worse, some ignores are only required for some versions of Python,
# from 3.7 to 3.10...
nitpick_ignore = [
("py:class", "Config"),
# Sphinx does not handle ParamSpec arguments
("py:class", "T.args"),
("py:class", "T.kwargs"),
("py:class", "T2.args"),
("py:class", "T2.kwargs"),
# Sphinx doesn't know about the following classes
("py:class", "click.Command"),
("py:class", "tutor.hooks.filters.P"),
("py:class", "tutor.hooks.filters.T"),
("py:class", "tutor.hooks.actions.P"),
("py:class", "P"),
("py:class", "P.args"),
("py:class", "P.kwargs"),
("py:class", "T"),
("py:class", "t.Any"),
("py:class", "t.Callable"),
("py:class", "t.Iterator"),
("py:class", "t.Optional"),
# python 3.7
("py:class", "Concatenate"),
# python 3.10
("py:class", "NoneType"),
("py:class", "click.core.Command"),
]
# Resolve type aliases here
# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_type_aliases
autodoc_type_aliases: dict[str, str] = {
"T1": "tutor.core.hooks.filters.T1",
"L": "tutor.core.hooks.filters.L",
# python 3.10
"T": "tutor.core.hooks.actions.T",
"T2": "tutor.core.hooks.filters.T2",
}


# -- Sphinx-Click configuration
# https://sphinx-click.readthedocs.io/
Expand Down Expand Up @@ -87,13 +106,11 @@
with io.open(
os.path.join(here, "..", "tutor", "__about__.py"), "rt", encoding="utf-8"
) as f:
# pylint: disable=exec-used
exec(f.read(), about)
rst_prolog = """
.. |tutor_version| replace:: {}
""".format(
about["__version__"],
)

rst_prolog = f"""
.. |tutor_version| replace:: {about["__version__"]}
"""

# Custom directives
def youtube(
Expand Down
2 changes: 2 additions & 0 deletions docs/plugins/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Tutor comes with a plugin system that allows anyone to customise the deployment

For simple changes, it may be extremely easy to create a Tutor plugin: even non-technical users may get started with our :ref:`plugin_development_tutorial` tutorial. We also provide a list of :ref:`simple example plugins <plugins_examples>`.

To learn about the different ways in which plugins can extend Tutor, check out the :ref:`hooks catalog <hooks_catalog>`.

Plugin commands cheatsheet
==========================

Expand Down
17 changes: 8 additions & 9 deletions docs/reference/api/hooks/actions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ Actions

Actions are one of the two types of hooks (the other being :ref:`filters`) that can be used to extend Tutor. Each action represents an event that can occur during the application life cycle. Each action has a name, and callback functions can be attached to it. When an action is triggered, these callback functions are called in sequence. Each callback function can trigger side effects, independently from one another.

.. autofunction:: tutor.hooks.actions::get
.. autofunction:: tutor.hooks.actions::get_template
.. autofunction:: tutor.hooks.actions::add
.. autofunction:: tutor.hooks.actions::do
.. autofunction:: tutor.hooks.actions::do_from_context
.. autofunction:: tutor.hooks.actions::clear
.. autofunction:: tutor.hooks.actions::clear_all
.. autoclass:: tutor.core.hooks.Action
:members:

.. autoclass:: tutor.hooks.actions.Action
.. autoclass:: tutor.hooks.actions.ActionTemplate
.. autoclass:: tutor.core.hooks.ActionTemplate
:members:

.. The following are only to ensure that the docs build without warnings
.. class:: tutor.core.hooks.actions.T
.. class:: tutor.types.Config
18 changes: 18 additions & 0 deletions docs/reference/api/hooks/catalog.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.. _hooks_catalog:

=============
Hooks catalog
=============

Tutor can be extended by making use of "hooks". Hooks are either "actions" or "filters". Here, we list all instances of actions and filters that are used across Tutor. Plugin developers can leverage these hooks to modify the behaviour of Tutor.

The underlying Python hook classes and API are documented :ref:`here <hooks_api>`.

.. autoclass:: tutor.hooks.Actions
:members:

.. autoclass:: tutor.hooks.Filters
:members:

.. autoclass:: tutor.hooks.Contexts
:members:
23 changes: 0 additions & 23 deletions docs/reference/api/hooks/consts.rst

This file was deleted.

5 changes: 2 additions & 3 deletions docs/reference/api/hooks/contexts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@ Contexts

Contexts are a feature of the hook-based extension system in Tutor, which allows us to keep track of which components of the code created which callbacks. Contexts are very much an internal concept that most plugin developers should not have to worry about.

.. autofunction:: tutor.hooks.contexts::enter

.. autoclass:: tutor.hooks.contexts.Context
.. autoclass:: tutor.core.hooks.Context
.. autofunction:: tutor.core.hooks.contexts::enter
22 changes: 9 additions & 13 deletions docs/reference/api/hooks/filters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@ Filters

Filters are one of the two types of hooks (the other being :ref:`actions`) that can be used to extend Tutor. Filters allow one to modify the application behavior by transforming data. Each filter has a name, and callback functions can be attached to it. When a filter is applied, these callback functions are called in sequence; the result of each callback function is passed as the first argument to the next callback function. The result of the final callback function is returned to the application as the filter's output.

.. autofunction:: tutor.hooks.filters::get
.. autofunction:: tutor.hooks.filters::get_template
.. autofunction:: tutor.hooks.filters::add
.. autofunction:: tutor.hooks.filters::add_item
.. autofunction:: tutor.hooks.filters::add_items
.. autofunction:: tutor.hooks.filters::apply
.. autofunction:: tutor.hooks.filters::apply_from_context
.. autofunction:: tutor.hooks.filters::iterate
.. autofunction:: tutor.hooks.filters::iterate_from_context
.. autofunction:: tutor.hooks.filters::clear
.. autofunction:: tutor.hooks.filters::clear_all
.. autoclass:: tutor.core.hooks.Filter
:members:

.. autoclass:: tutor.hooks.filters.Filter
.. autoclass:: tutor.hooks.filters.FilterTemplate
.. autoclass:: tutor.core.hooks.FilterTemplate
:members:

.. The following are only to ensure that the docs build without warnings
.. class:: tutor.core.hooks.filters.T1
.. class:: tutor.core.hooks.filters.T2
.. class:: tutor.core.hooks.filters.L
11 changes: 7 additions & 4 deletions docs/reference/api/hooks/index.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
=========
Hooks API
=========
.. _hooks_api:

==========
Hook types
==========

This is the Python documentation of the two types of hooks (actions and filters) as well as the contexts system which is used to instrument them. Understanding how Tutor hooks work is useful to create plugins that modify the behaviour of Tutor. However, plugin developers should almost certainly not import these hook types directly. Instead, use the reference :ref:`hooks catalog <hooks_catalog>`.

.. toctree::
:maxdepth: 1

actions
filters
contexts
consts
3 changes: 2 additions & 1 deletion docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ Reference
:maxdepth: 2

api/hooks/index
cli/index
api/hooks/catalog
patches
cli/index
6 changes: 3 additions & 3 deletions docs/reference/patches.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
.. _patches:

================
Template patches
================
======================
Template patch catalog
======================

This is the list of all patches used across Tutor (outside of any plugin). Alternatively, you can search for patches in Tutor templates by grepping the source code::

Expand Down
6 changes: 4 additions & 2 deletions docs/tutorials/plugin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ You may be thinking that creating a plugin might be overkill for your use case.

A plugin can be created either as a simple, single Python module (a ``*.py`` file) or as a full-blown Python package. Single Python modules are easier to write, while Python packages can be distributed more easily with ``pip install ...``. We'll start by writing our plugin as a single Python module.

Plugins work by making extensive use of the Tutor hooks API. The list of available hooks is available from the :ref:`hooks catalog <hooks_catalog>`. Developers who want to understand how hooks work should check the :ref:`hooks API <hooks_api>`.

Writing a plugin as a single Python module
==========================================

Expand Down Expand Up @@ -45,7 +47,7 @@ Modifying existing files with patches

We'll start by modifying some of our Open edX settings files. It's a frequent requirement to modify the ``FEATURES`` setting from the LMS or the CMS in edx-platform. In the legacy native installation, this was done by modifying the ``lms.env.yml`` and ``cms.env.yml`` files. Here we'll modify the Python setting files that define the edx-platform configuration. To achieve that we'll make use of two concepts from the Tutor API: :ref:`patches` and :ref:`filters`.

If you have not already read :ref:`how_does_tutor_work` now would be a good time :-) Tutor uses templates to generate various files, such as settings, Dockerfiles, etc. These templates include ``{{ patch("patch-name") }}`` statements that allow plugins to insert arbitrary content in there. These patches are located at strategic locations. See :ref:`patches` for more information.
If you have not already read :ref:`how_does_tutor_work` now would be a good time ☺️ Tutor uses templates to generate various files, such as settings, Dockerfiles, etc. These templates include ``{{ patch("patch-name") }}`` statements that allow plugins to insert arbitrary content in there. These patches are located at strategic locations. See :ref:`patches` for more information.

Let's say that we would like to limit access to our brand new Open edX platform. It is not ready for prime-time yet, so we want to prevent users from registering new accounts. There is a feature flag for that in the LMS: `FEATURES['ALLOW_PUBLIC_ACCOUNT_CREATION'] <https://edx.readthedocs.io/projects/edx-platform-technical/en/latest/featuretoggles.html#featuretoggle-FEATURES['ALLOW_PUBLIC_ACCOUNT_CREATION']>`__. By default this flag is set to a true value, enabling anyone to create an account. In the following we'll set it to false.

Expand Down Expand Up @@ -73,7 +75,7 @@ This imports the ``hooks`` module from Tutor, which grants us access to ``hooks.
<content>
)

This means "add ``<content>`` to the ``{{ patch("<name>") }}`` statement, thanks to the ENV_PATCHES filter". In our case, we want to modify the LMS settings, both in production and development. The right patch for that is :patch:`openedx-lms-common-settings`. We add one item, which is a single Python-formatted line of code::
This means "add ``<content>`` to the ``{{ patch("<name>") }}`` statement, thanks to the :py:data:`tutor.hooks.Filters.ENV_PATCHES` filter". In our case, we want to modify the LMS settings, both in production and development. The right patch for that is :patch:`openedx-lms-common-settings`. We add one item, which is a single Python-formatted line of code::

"FEATURES['ALLOW_PUBLIC_ACCOUNT_CREATION'] = False"

Expand Down
6 changes: 3 additions & 3 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is autogenerated by pip-compile with Python 3.8
# by the following command:
# This file is autogenerated by pip-compile with python 3.8
# To update, run:
#
# pip-compile requirements/dev.in
#
Expand Down Expand Up @@ -62,7 +62,7 @@ idna==3.4
# via
# -r requirements/base.txt
# requests
importlib-metadata==5.1.0
importlib-metadata==6.0.0
# via
# keyring
# twine
Expand Down
1 change: 1 addition & 0 deletions tests/commands/test_compose.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from __future__ import annotations

import typing as t
import unittest
from io import StringIO
Expand Down
File renamed without changes.
Empty file added tests/core/hooks/__init__.py
Empty file.
20 changes: 10 additions & 10 deletions tests/hooks/test_actions.py → tests/core/hooks/test_actions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import typing as t
import unittest

from tutor import hooks
from tutor.core.hooks import actions, contexts


class PluginActionsTests(unittest.TestCase):
Expand All @@ -10,14 +10,14 @@ def setUp(self) -> None:

def tearDown(self) -> None:
super().tearDown()
hooks.actions.clear_all(context="tests")
actions.clear_all(context="tests")

def run(self, result: t.Any = None) -> t.Any:
with hooks.contexts.enter("tests"):
with contexts.enter("tests"):
return super().run(result=result)

def test_do(self) -> None:
action: hooks.actions.Action[int] = hooks.actions.get("test-action")
action: actions.Action[int] = actions.get("test-action")

@action.add()
def _test_action_1(increment: int) -> None:
Expand All @@ -31,29 +31,29 @@ def _test_action_2(increment: int) -> None:
self.assertEqual(3, self.side_effect_int)

def test_priority(self) -> None:
@hooks.actions.add("test-action", priority=2)
@actions.add("test-action", priority=2)
def _test_action_1() -> None:
self.side_effect_int += 4

@hooks.actions.add("test-action", priority=1)
@actions.add("test-action", priority=1)
def _test_action_2() -> None:
self.side_effect_int = self.side_effect_int // 2

# Action 2 must be performed before action 1
self.side_effect_int = 4
hooks.actions.do("test-action")
actions.do("test-action")
self.assertEqual(6, self.side_effect_int)

def test_equal_priority(self) -> None:
@hooks.actions.add("test-action", priority=2)
@actions.add("test-action", priority=2)
def _test_action_1() -> None:
self.side_effect_int += 4

@hooks.actions.add("test-action", priority=2)
@actions.add("test-action", priority=2)
def _test_action_2() -> None:
self.side_effect_int = self.side_effect_int // 2

# Action 2 must be performed after action 1
self.side_effect_int = 4
hooks.actions.do("test-action")
actions.do("test-action")
self.assertEqual(4, self.side_effect_int)
Loading