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
2 changes: 2 additions & 0 deletions changelog.d/20250429_092254_regis_contrib.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- [Feature] Enable the "contrib" plugin index by default. This enables platform users to view 3rd party plugins without having to run `tutor plugins index add contrib`. (by @regisb)
- [Feature] Automatically update the plugin index cache on all commands that need it. Including: `plugins install/upgrade/search`. This makes it slightly easier for end users to use plugins. (by @regisb)
11 changes: 5 additions & 6 deletions docs/plugins/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,11 @@ The full plugins CLI is described in the :ref:`reference documentation <cli_plug
Existing plugins
================

Many plugins are available from plugin indexes. These indexes are lists of plugins, similar to the `pypi <https://pypi.org>`__ or `npm <npmjs.com/>`__ indexes. By default, Tutor comes with the "main" plugin index. You can check available plugins from this index by running::
Many plugins are available from plugin indexes. These indexes are lists of plugins, similar to the `pypi <https://pypi.org>`__ or `npm <npmjs.com/>`__ indexes. By default, Tutor comes with the "main" and "contrib" plugin indexes. You can check available plugins from this index by running::

tutor plugins update
tutor plugins search

More plugins can be downloaded from the "contrib" index::

tutor plugins index add contrib
tutor plugins search

The "main" and "contrib" indexes include a curated list of plugins that are well maintained and introduce useful features to Open edX. These indexes are maintained by `Edly <https://edly.io>`__. For more information about these indexes, refer to the official `overhangio/tpi <https://github.com/overhangio/tpi>`__ repository.

Thanks to these indexes, it is very easy to download and upgrade plugins. For instance, to install the `notes plugin <https://github.com/overhangio/tutor-notes/>`__::
Expand All @@ -60,4 +55,8 @@ To list indexes that you are downloading plugins from, run::

tutor plugins index list

To disable an index, for instance "contrib", use the ``plugins index remove`` command::

tutor plugins index remove contrib

For more information about these indexes, check the `official Tutor plugin indexes (TPI) <https://github.com/overhangio/tpi/>`__ repository.
46 changes: 37 additions & 9 deletions tutor/commands/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,10 @@ def update(context: Context) -> None:
update_indexes(config)


def update_indexes(config: Config) -> None:
all_plugins = indexes.fetch(config)
cache_path = indexes.save_cache(all_plugins)
fmt.echo_info(f"Plugin index local cache: {cache_path}")


@click.command()
@click.argument("names", metavar="name", type=IndexPluginNameOrLocation(), nargs=-1)
def install(names: list[str]) -> None:
@click.pass_obj
def install(context: Context, names: list[str]) -> None:
"""
Install one or more plugins.

Expand All @@ -199,18 +194,24 @@ def install(names: list[str]) -> None:
In cases 2. and 3., the plugin root corresponds to the path given by `tutor plugins
printroot`.
"""
config = tutor_config.load(context.root)
check_cache_exists(config)
find_and_install(names, [])


@click.command()
@click.argument("names", metavar="name", type=IndexPluginName(), nargs=-1)
def upgrade(names: list[str]) -> None:
@click.pass_obj
def upgrade(context: Context, names: list[str]) -> None:
"""
Upgrade one or more plugins.

Specify "all" to upgrade all installed plugins. This command will only print a
warning for plugins which cannot be found.
"""
config = tutor_config.load(context.root)
check_cache_exists(config)

if "all" in names:
names = list(plugins.iter_installed())
available_names = []
Expand All @@ -227,6 +228,29 @@ def upgrade(names: list[str]) -> None:
find_and_install(available_names, ["--upgrade"])


def check_cache_exists(config: Config) -> None:
"""
Check that the plugin cache exists prior to any operation requiring the cache. If it
doesn't exist, update it.
"""
try:
indexes.load_cache()
except indexes.CacheNotFound:
fmt.echo_info(
f"Plugin cache not found in {indexes.Indexes.CACHE_PATH}. Updating..."
)
update_indexes(config)


def update_indexes(config: Config) -> None:
"""
Fetch index content and update the local cache.
"""
all_plugins = indexes.fetch(config)
cache_path = indexes.save_cache(all_plugins)
fmt.echo_info(f"Plugin index local cache: {cache_path}")


def find_and_install(names: list[str], pip_install_opts: t.List[str]) -> None:
"""
Find and install a list of plugins, given by name. Single-file plugins are
Expand Down Expand Up @@ -285,10 +309,14 @@ def install_single_file_plugin(location: str) -> None:

@click.command()
@click.argument("pattern", default="")
def search(pattern: str) -> None:
@click.pass_obj
def search(context: Context, pattern: str) -> None:
"""
Search in plugin descriptions.
"""
config = tutor_config.load(context.root)
check_cache_exists(config)

results: list[tuple[str, ...]] = [("NAME", "STATUS", "DESCRIPTION")]
for plugin in indexes.iter_cache_entries():
if plugin.match(pattern):
Expand Down
12 changes: 9 additions & 3 deletions tutor/plugins/indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ class Indexes:
CACHE_PATH = ""


class CacheNotFound(TutorError):
def __init__(self) -> None:
super().__init__(
f"Local index cache could not be found in {Indexes.CACHE_PATH}. Run `tutor plugins update`."
)


@hooks.Actions.PROJECT_ROOT_READY.add()
def _set_indexes_cache_path(root: str) -> None:
Indexes.CACHE_PATH = env.pathjoin(root, "plugins", "index", "cache.yml")
Expand Down Expand Up @@ -233,12 +240,11 @@ def save_cache(plugins: list[dict[str, str]]) -> str:
return Indexes.CACHE_PATH


@hooks.lru_cache
def load_cache() -> list[dict[str, str]]:
try:
with open(Indexes.CACHE_PATH, encoding="utf8") as cache_if:
plugins = serialize.load(cache_if)
except FileNotFoundError as e:
raise TutorError(
f"Local index cache could not be found in {Indexes.CACHE_PATH}. Run `tutor plugins update`."
) from e
raise CacheNotFound() from e
return validate_index(plugins)
3 changes: 2 additions & 1 deletion tutor/templates/config/base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ PLUGINS:
- indigo
PLUGIN_INDEXES:
# Indexes in this list will be suffixed with the Open edX named version and
# "plugins.yml". E.g: https://overhang.io/tutor/main/olive/plugins.yml
# "plugins.yml". E.g: https://overhang.io/tutor/main/sumac/plugins.yml
- https://overhang.io/tutor/main
- https://overhang.io/tutor/contrib