6
0
mirror of https://github.com/ChristianLight/tutor.git synced 2024-11-13 08:46:34 +00:00

fix: save env on plugins enable

Environment was not updated correctly on `tutor plugins enable ...`
because of a caching issue. To bypass this issue, we improve the caching
mechanism and introduce a new `lru_cache` decorator.

Close #989.
This commit is contained in:
Régis Behmo 2024-01-30 13:02:02 +01:00 committed by Régis Behmo
parent b597af481c
commit 60a5f25c9b
6 changed files with 44 additions and 37 deletions

View File

@ -23,6 +23,7 @@ Functions
---------
.. autofunction:: tutor.core.hooks::clear_all
.. autofunction:: tutor.hooks::lru_cache
Priorities
----------

View File

@ -9,7 +9,7 @@ from tutor.plugins import v0 as plugins_v0
from tutor.types import Config, get_typed
class PluginsTests(PluginsTestCase):
class PluginsV0Tests(PluginsTestCase):
def test_iter_installed(self) -> None:
self.assertEqual([], list(plugins.iter_installed()))

View File

@ -1,7 +1,6 @@
from __future__ import annotations
import os
import sys
import tempfile
import typing as t

View File

@ -2,8 +2,35 @@
__license__ = "Apache 2.0"
import typing as t
import functools
from typing_extensions import ParamSpec
# The imports that follow are the hooks API
from tutor.core.hooks import clear_all, priorities
from tutor.types import Config
from .catalog import Actions, Contexts, Filters
def lru_cache(func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]:
"""
LRU cache decorator similar to `functools.lru_cache
<https://docs.python.org/3/library/functools.html#functools.lru_cache>`__ that is
automatically cleared whenever plugins are updated.
Use this to decorate functions that need to be called multiple times with a return
value that depends on which plugins are loaded. Typically: functions that depend on
the output of filters.
"""
decorated = functools.lru_cache(func)
@Actions.PLUGIN_LOADED.add()
def _clear_func_cache_on_load(_plugin: str) -> None:
decorated.cache_clear()
@Actions.PLUGIN_UNLOADED.add()
def _clear_func_cache_on_unload(_plugin: str, _root: str, _config: Config) -> None:
decorated.cache_clear()
return decorated

View File

@ -3,6 +3,7 @@ Provide API for plugin features.
"""
from __future__ import annotations
import functools
import typing as t
from copy import deepcopy
@ -12,38 +13,6 @@ from tutor.types import Config, get_typed
# Import modules to trigger hook creation
from . import openedx, v0, v1
# Cache of plugin patches, for efficiency
ENV_PATCHES_DICT: dict[str, list[str]] = {}
@hooks.Actions.PLUGINS_LOADED.add()
def _fill_patch_cache_on_load() -> None:
"""
This action is run after plugins have been loaded.
"""
_fill_patches_cache()
@hooks.Actions.PLUGIN_UNLOADED.add()
def _fill_patch_cache_on_unload(plugin: str, root: str, _config: Config) -> None:
"""
This action is run after plugins have been unloaded.
"""
_fill_patches_cache()
def _fill_patches_cache() -> None:
"""
Some patches are added as (name, content) tuples with the ENV_PATCHES
filter. We convert these patches to add them to ENV_PATCHES_DICT. This makes it
easier for end-user to declare patches, and it's more performant.
"""
ENV_PATCHES_DICT.clear()
patches: t.Iterable[tuple[str, str]] = hooks.Filters.ENV_PATCHES.iterate()
for name, content in patches:
ENV_PATCHES_DICT.setdefault(name, [])
ENV_PATCHES_DICT[name].append(content)
def is_installed(name: str) -> bool:
"""
@ -127,7 +96,19 @@ def iter_patches(name: str) -> t.Iterator[str]:
"""
Yields: patch (str)
"""
yield from ENV_PATCHES_DICT.get(name, [])
yield from _env_patches().get(name, [])
@hooks.lru_cache
def _env_patches() -> dict[str, list[str]]:
"""
Dictionary of patches, implemented for performance reasons.
"""
patches: dict[str, list[str]] = {}
for name, content in hooks.Filters.ENV_PATCHES.iterate():
patches.setdefault(name, [])
patches[name].append(content)
return patches
def unload(plugin: str) -> None:

View File

@ -4,8 +4,7 @@ import os
import re
import typing as t
from tutor import bindmount
from tutor import hooks
from tutor import bindmount, hooks
from tutor.__about__ import __version_suffix__