Skip to content
52 changes: 30 additions & 22 deletions docs/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,33 @@ There are two possible ways to distribute plugins (file plugins and
namespace plugins), but a given plugin script could be distributed either
way.

Writing a plugin script
-----------------------

An OPS plugin is simply a Python module that follows a few rules.

* It must define a variable ``CLI`` that is the main CLI function is
assigned to.
* It must define a variable ``SECTION`` to determine where to show it in
help (what kind of command it is). Valid values are ``"Simulation"``,
``"Analysis"``, ``"Miscellaneous"``, or ``"Workflow"``. If ``SECTION`` is
defined but doesn't have one of these values, it won't show in
``openpathsampling --help``, but might still be usable. If your command
doesn't show in the help, carefully check your spelling of the ``SECTION``
variable.
* The main CLI function must be decorated as a ``click.command``.
* (If distributed as a file plugin) It must be possible to ``exec`` it in an
empty namespace (mainly, this can mean no relative imports).
Writing a command plugin
------------------------

To write an OPS command plugin, you simply need to create an instance of
:class:`paths_cli.OPSCommandPlugin` and to install the module in a location
where the CLI knows to look for it. The input parameters to
``OPSCommandPlugin`` are:

* ``command``: This is the main CLI function for the subcommand. It must be
decorated as a ``click.command``.
* ``section``: This is a string to determine where to show it in help (what
kind of command it is). Valid values are ``"Simulation"``,
``"Analysis"``, ``"Miscellaneous"``, or ``"Workflow"``. If ``section``
doesn't have one of these values, it won't show in ``openpathsampling
--help``, but might still be usable. If your command doesn't show in the
help, carefully check your spelling of the ``section`` variable.
* ``requires_ops`` (optional, default ``(1, 0)``): Minimum allowed version
of OpenPathSampling. Note that this is currently informational only, and
has no effect on functionality.
* ``requires_cli`` (optional, default ``(0, 3)``): Minimum allowed version
of the OpenPathSampling CLI. Note that this is currently informational
only, and has no effect on functionality.


If you distribute your plugin as a file-based plugin, be aware that it must
be possible to ``exec`` it in an empty namespace (mainly, this can mean no
relative imports).

As a suggestion, I (DWHS) tend to structure my plugins as follows:

Expand All @@ -41,16 +51,15 @@ As a suggestion, I (DWHS) tend to structure my plugins as follows:
...
return final_status, simulation

CLI = plugin
SECTION = "MySection"
PLUGIN = OPSCommandPlugin(command=plugin, section="MySection")

The basic idea is that there's a ``plugin_main`` function that is based on
pure OPS, using only inputs that OPS can immediately understand (no need to
process the command line). This is easy to develop/test with OPS. Then
there's a wrapper function whose sole purpose is to convert the command line
parameters to something OPS can understand (using the ``get`` method). This
wrapper is the ``CLI`` variable. Give it an allowed ``SECTION``, and the
plugin is ready!
wrapper is the ``command`` in you ``OPSCommandPlugin``. Also provide an
allowed ``section``, and the plugin is ready!

The result is that plugins are astonishingly easy to develop, once you have
the scientific code implemented in a library. This structure also makes it
Expand Down Expand Up @@ -95,4 +104,3 @@ namespace.
.. _native namespace packages:
https://packaging.python.org/guides/packaging-namespace-packages/#native-namespace-packages


10 changes: 10 additions & 0 deletions example_plugins/one_pot_tps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import click
from paths_cli import OPSCommandPlugin
from paths_cli.parameters import (INPUT_FILE, OUTPUT_FILE, ENGINE, STATES,
N_STEPS_MC, INIT_SNAP)
from paths_cli.commands.visit_all import visit_all_main
Expand Down Expand Up @@ -44,6 +45,15 @@ def one_pot_tps_main(output_storage, states, engine, engine_hot,
equil_multiplier, equil_extra)
return pathsampling_main(output_storage, scheme, equil_set, nsteps)

# these lines enable this plugin to support OPS CLI < 0.3
CLI = one_pot_tps
SECTION = "Workflow"
REQUIRES_OPS = (1, 2)

# these lines enable this plugin to support OPS CLI >= 0.3
PLUGIN = OPSCommandPlugin(
command=one_pot_tps,
section="Workflow",
requires_ops=(1, 2),
requires_cli=(0, 3)
)
10 changes: 10 additions & 0 deletions example_plugins/tps.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import click
from paths_cli import OPSCommandPlugin
from paths_cli.parameters import (
INPUT_FILE, OUTPUT_FILE, ENGINE, STATES, INIT_CONDS, N_STEPS_MC
)
Expand Down Expand Up @@ -40,8 +41,17 @@ def tps_main(engine, states, init_traj, output_storage, n_steps):
return simulation.sample_set, simulation


# these lines enable this plugin to support OPS CLI < 0.3
CLI = tps
SECTION = "Simulation"

# these lines enable this plugin to support OPS CLI >= 0.3
PLUGIN = OPSCommandPlugin(
command=tps,
section="Simulation"
)


# this allows you to use this as a script, independently of the OPS CLI
if __name__ == "__main__":
tps()
3 changes: 3 additions & 0 deletions paths_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .plugin_management import (
OPSCommandPlugin,
)
from .cli import OpenPathSamplingCLI
from . import commands
from . import version
11 changes: 6 additions & 5 deletions paths_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
# import click_completion
# click_completion.init()

from .plugin_management import FilePluginLoader, NamespacePluginLoader
from .plugin_management import (FilePluginLoader, NamespacePluginLoader,
OPSCommandPlugin)

CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])

Expand All @@ -31,10 +32,10 @@ def app_dir_plugins(posix):
).resolve() / 'cli-plugins')

self.plugin_loaders = [
FilePluginLoader(commands),
FilePluginLoader(app_dir_plugins(posix=False)),
FilePluginLoader(app_dir_plugins(posix=True)),
NamespacePluginLoader('paths_cli_plugins')
FilePluginLoader(commands, OPSCommandPlugin),
FilePluginLoader(app_dir_plugins(posix=False), OPSCommandPlugin),
FilePluginLoader(app_dir_plugins(posix=True), OPSCommandPlugin),
NamespacePluginLoader('paths_cli_plugins', OPSCommandPlugin)
]

plugins = sum([loader() for loader in self.plugin_loaders], [])
Expand Down
10 changes: 7 additions & 3 deletions paths_cli/commands/append.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import click
from paths_cli import OPSCommandPlugin
from paths_cli.parameters import (
INPUT_FILE, APPEND_FILE, MULTI_CV, MULTI_ENGINE, MULTI_VOLUME,
MULTI_NETWORK, MULTI_SCHEME, MULTI_TAG
Expand Down Expand Up @@ -55,6 +56,9 @@ def append(input_file, append_file, engine, cv, volume, network, scheme,
storage.close()


CLI = append
SECTION = "Miscellaneous"
REQUIRES_OPS = (1, 0)
PLUGIN = OPSCommandPlugin(
command=append,
section="Miscellaneous",
requires_ops=(1, 0),
requires_cli=(0, 3)
)
11 changes: 8 additions & 3 deletions paths_cli/commands/contents.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import click
from paths_cli.parameters import INPUT_FILE
from paths_cli import OPSCommandPlugin

UNNAMED_SECTIONS = ['steps', 'movechanges', 'samplesets', 'trajectories',
'snapshots']
Expand Down Expand Up @@ -112,6 +113,10 @@ def get_section_string_nameable(section, store, get_named):
+ _item_or_items(n_unnamed))
return out_str

CLI = contents
SECTION = "Miscellaneous"
REQUIRES_OPS = (1, 0)

PLUGIN = OPSCommandPlugin(
command=contents,
section="Miscellaneous",
requires_ops=(1, 0),
requires_cli=(0, 3)
)
11 changes: 8 additions & 3 deletions paths_cli/commands/equilibrate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import click
# import openpathsampling as paths

from paths_cli import OPSCommandPlugin
from paths_cli.parameters import (
INPUT_FILE, OUTPUT_FILE, INIT_CONDS, SCHEME
)
Expand Down Expand Up @@ -58,6 +59,10 @@ def equilibrate_main(output_storage, scheme, init_conds, multiplier,
return simulation.sample_set, simulation


CLI = equilibrate
SECTION = "Simulation"
REQUIRES_OPS = (1, 2)
PLUGIN = OPSCommandPlugin(
command=equilibrate,
section="Simulation",
requires_ops=(1, 2),
requires_cli=(0, 3)
)

10 changes: 7 additions & 3 deletions paths_cli/commands/md.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import click

import paths_cli.utils
from paths_cli import OPSCommandPlugin
from paths_cli.parameters import (INPUT_FILE, OUTPUT_FILE, ENGINE,
MULTI_ENSEMBLE, INIT_SNAP)

Expand Down Expand Up @@ -197,7 +198,10 @@ def md_main(output_storage, engine, ensembles, nsteps, initial_frame):
'final_conditions')
return trajectory, None

CLI = md
SECTION = "Simulation"
REQUIRES_OPS = (1, 0)

PLUGIN = OPSCommandPlugin(
command=md,
section="Simulation",
requires_ops=(1, 0),
requires_cli=(0, 3)
)
10 changes: 7 additions & 3 deletions paths_cli/commands/pathsampling.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import click
# import openpathsampling as paths

from paths_cli import OPSCommandPlugin
from paths_cli.parameters import (
INPUT_FILE, OUTPUT_FILE, INIT_CONDS, SCHEME, N_STEPS_MC
)
Expand Down Expand Up @@ -37,6 +38,9 @@ def pathsampling_main(output_storage, scheme, init_conds, n_steps):
return simulation.sample_set, simulation


CLI = pathsampling
SECTION = "Simulation"
REQUIRES_OPS = (1, 0)
PLUGIN = OPSCommandPlugin(
command=pathsampling,
section="Simulation",
requires_ops=(1, 0),
requires_cli=(0, 3)
)
10 changes: 7 additions & 3 deletions paths_cli/commands/visit_all.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import click

import paths_cli.utils
from paths_cli import OPSCommandPlugin
from paths_cli.parameters import (INPUT_FILE, OUTPUT_FILE, ENGINE, STATES,
INIT_SNAP)

Expand Down Expand Up @@ -41,6 +42,9 @@ def visit_all_main(output_storage, states, engine, initial_frame):
return trajectory, None # no simulation object to return here


CLI = visit_all
SECTION = "Simulation"
REQUIRES_OPS = (1, 0)
PLUGIN = OPSCommandPlugin(
command=visit_all,
section="Simulation",
requires_ops=(1, 0),
requires_cli=(0, 3)
)
2 changes: 1 addition & 1 deletion paths_cli/param_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ class OPSStorageLoadSingle(AbstractLoader):
parameter. Each should be a callable taking a storage and the string
input from the CLI, and should return the desired object or None if
it cannot be found.
none_strategies : List[Callable[:class:`openpathsampling.Storage, Any]]
none_strategies : List[Callable[:class:`openpathsampling.Storage`, Any]]
The strategies to be used when the CLI does not provide a value for
this parameter. Each should be a callable taking a storage, and
returning the desired object or None if it cannot be found.
Expand Down
Loading