New pre and post command plugin hooks
Image credit: Kier in Sight Archives on Unsplash
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.
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.
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.
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 โ๏ธ