From b597af481cb4bfabcde2b9784aed356ed61a49e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Tue, 30 Jan 2024 11:41:25 +0100 Subject: [PATCH 1/2] chore: remove useless test file This test fixture has been happily living in the test folder for 4 years... --- tests/openedx-lms-common-settings | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tests/openedx-lms-common-settings diff --git a/tests/openedx-lms-common-settings b/tests/openedx-lms-common-settings deleted file mode 100644 index c1253bb..0000000 --- a/tests/openedx-lms-common-settings +++ /dev/null @@ -1 +0,0 @@ -ORA2_FILEUPLOAD_BACKEND = "s3" From 60a5f25c9b9f301c05622d6cbc0f895221f3a9f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Behmo?= Date: Tue, 30 Jan 2024 13:02:02 +0100 Subject: [PATCH 2/2] 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. --- docs/reference/api/hooks/index.rst | 1 + tests/test_plugins_v0.py | 2 +- tutor/commands/plugins.py | 1 - tutor/hooks/__init__.py | 27 +++++++++++++++++ tutor/plugins/__init__.py | 47 +++++++++--------------------- tutor/plugins/openedx.py | 3 +- 6 files changed, 44 insertions(+), 37 deletions(-) diff --git a/docs/reference/api/hooks/index.rst b/docs/reference/api/hooks/index.rst index 8a5700e..f6f130e 100644 --- a/docs/reference/api/hooks/index.rst +++ b/docs/reference/api/hooks/index.rst @@ -23,6 +23,7 @@ Functions --------- .. autofunction:: tutor.core.hooks::clear_all +.. autofunction:: tutor.hooks::lru_cache Priorities ---------- diff --git a/tests/test_plugins_v0.py b/tests/test_plugins_v0.py index 248cf80..5d737d0 100644 --- a/tests/test_plugins_v0.py +++ b/tests/test_plugins_v0.py @@ -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())) diff --git a/tutor/commands/plugins.py b/tutor/commands/plugins.py index 45d8bd6..a740985 100644 --- a/tutor/commands/plugins.py +++ b/tutor/commands/plugins.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -import sys import tempfile import typing as t diff --git a/tutor/hooks/__init__.py b/tutor/hooks/__init__.py index 9853e57..d075ab4 100644 --- a/tutor/hooks/__init__.py +++ b/tutor/hooks/__init__.py @@ -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 + `__ 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 diff --git a/tutor/plugins/__init__.py b/tutor/plugins/__init__.py index b2a3dcb..360e9fb 100644 --- a/tutor/plugins/__init__.py +++ b/tutor/plugins/__init__.py @@ -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: diff --git a/tutor/plugins/openedx.py b/tutor/plugins/openedx.py index 1ef4eff..284d53f 100644 --- a/tutor/plugins/openedx.py +++ b/tutor/plugins/openedx.py @@ -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__