Skip to main content

New pre and post command plugin hooks

ยท 4 min read
Travis Hathaway
Conda maintainer ๐Ÿ‘ท๐Ÿ”ง

With the latest conda release (23.7.2 at the time this post was written), the ability to define two new plugin hooks was introduced: "pre command" and "post command". These two new plugin hooks give plugin authors the ability to execute code before and after conda commands are run. In this blog post, we provide more details on how and why you may use these to extend the default behavior of conda.

info

For a fully functional example of how these plugin hooks are used in practice, check out the conda-protect project.


To explain how these plugin hooks can be used, we will cover two examples.

Conda protect and the "pre command" hookโ€‹

The project linked to above is called conda-protect, and it extends conda by adding functionality to "protect" a conda environment from being changed. Sometimes users may want to do this to protect themselves from unintentionally modifying their base environments.

Using the "pre command" plugin hook, we can first specify which commands we want to be protected and then run a function that gets called before the command gets executed. By doing this, we can figure out whether the environment is protected or not and exit early if so. Below is how such a plugin hook would be defined:

from conda import plugins

def conda_protect_pre_commands_action(command: str) -> None:
"""Checks to see if the current environment is protected"""
environment = get_current_environment()

if is_guarded(environment):
raise CondaError(
f"Current environment is protect. Run `conda guard {environment}` to "
"remove protection."
)

@plugins.hookimpl
def conda_pre_commands():
yield plugins.CondaPreCommand(
name=f"conda_protect_pre_command",
action=conda_protect_pre_commands_action,
run_for={"install", "remove", "update", "env_update", "env_remove"},
)

In the example above, we first register our plugin hook by defining a function called conda_pre_commands and then decorating it with the hookimpl function provided by conda. Inside the hook function itself we return a CondaPreCommand object, which defines the following:

  • name: how this plugin hook will be internally reference by conda
  • action: a callable object that will be invoked before the commands defined in run_for are executed
  • run_for: a set of commands that this plugin hook should be used with

Going back to our example, if we wanted to create a plugin that would prevent us from mistakenly modifying protected environments, it would make sense to define run_for as all the conda commands that could potentially modify an environment (e.g. install, remove, etc.).

To actually prevent us from modifying our protected environments, the action function checks if the current environment is protected and then throws a CondaError if it is to make sure that the program exits early with a meaningful error message for the user.

info

For the full implementation, please see the conda-protect project.

Simple command counter with the "post command" hookโ€‹

The "post command" hook functions in exactly the same way as the "pre command" except that it gets called once a conda command has successfully finished running. It is important to note that if the command exits early for any reason, this plugin hook will not be invoked.

To illustrate how this is used, we will create a simple plugin that counts how many times we have run a particular conda command. We can use this later to analyze our usage of conda ๐Ÿค“ ๐Ÿ“Š

Here is a snippet of how that plugin might look:

from conda.cli.conda_argparse import BUILTIN_COMMANDS
from conda import plugins

ENV_COMMANDS = {
"env_config", "env_create",
"env_export", "env_list",
"env_remove", "env_update"
}

def conda_stats_post_commands_action(command: str):
"""Counts how many times we have run a particular conda command"""
database = get_database()
database.add_command(command)

@plugins.hookimpl
def conda_post_commands():
yield plugins.CondaPostCommand(
name=f"conda_stats_post_command",
action=conda_stats_post_commands_action,
run_for=BUILTIN_COMMANDS.union(ENV_COMMANDS),
)

The example above registers our "post command" hook by defining a function called conda_post_commands. Very similarly to the "pre command" hook, this returns a CondaPostCommand object which defines name, action and run_for. The action function counts the usage of the command each time it is run and the run_for property is configured to run for every built-in conda command, including the conda env * commands.

note

The implementation of the database has been purposely left out of the example as it falls out of scope of this blog post (implement it yourself for fun! ๐Ÿ˜…).

Wrapping upโ€‹

If you would like to get started with using these new plugins, please check out the conda-protect project. This project can also be used as a starting template for your plugins.

For even more information about all plugin hooks currently available in conda, head over to the relevant documentation page.

As always, you are more than welcome to visit us on our Matrix Chat to ask any questions or provide feedback.

Happy coding โœŒ๏ธ