{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "tags": [
     "remove-input",
     "active-ipynb",
     "remove-output"
    ]
   },
   "outputs": [],
   "source": [
    "try:\n",
    "    from openmdao.utils.notebook_utils import notebook_mode\n",
    "except ImportError:\n",
    "    !python -m pip install openmdao[notebooks]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Writing Plugins\n",
    "\n",
    "OpenMDAO was designed to allow you to code up your own components,\n",
    "groups, etc., and to use them within the framework, but what if you want others to be able to\n",
    "discover and use your creations?  The OpenMDAO plugin system was created to make that easier.\n",
    "\n",
    "Before laying out the steps to follow in order to create your plugin, a brief discussion of\n",
    "entry points is in order.  An entry point is simply a string passed into the `setup()` function\n",
    "in the `setup.py` file for your python package.  The string has the form:\n",
    "\n",
    "```\n",
    "'my_ep_name=my_plugin_module_path:my_module_attribute'\n",
    "```\n",
    "\n",
    "where typically, `my_module_attribute` is a class or a function.\n",
    "\n",
    "The plugin system uses entry points in order to provide local discovery, and in some cases to\n",
    "support adding new functionality to openmdao, e.g., adding new openmdao command line tools.\n",
    "\n",
    "\n",
    "Every entry point is associated with an entry point group, and\n",
    "the entry point groups that openmdao recognizes are shown in the table below:\n",
    "\n",
    "\n",
    "| Entry Point Group        \t| Type              \t| Entry Point Refers To                                       |\n",
    "|--------------------------\t|-------------------\t|-------------------------------------------------------------|\n",
    "| openmdao_component       \t| Component         \t| class or factory funct                                      |\n",
    "| openmdao_group           \t| Group             \t| class or factory funct                                      |\n",
    "| openmdao_driver          \t| Driver            \t| class or factory funct                                      |\n",
    "| openmdao_lin_solver      \t| LinearSolver      \t| class or factory funct                                      |\n",
    "| openmdao_nl_solver       \t| NonlinearSolver   \t| class or factory funct                                      |\n",
    "| openmdao_surrogate_model \t| SurrogateModel    \t| class or factory funct                                      |\n",
    "| openmdao_case_recorder   \t| CaseRecorder      \t| class or factory funct                                      |\n",
    "| openmdao_case_reader     \t| BaseCaseReader    \t| funct returning (file_ext, class or factor funct)           |\n",
    "| openmdao_command         \t| command line tool \t| funct returning (setup_parser_func, exec_func, help_string) |\n",
    "\n",
    "\n",
    "## 'Typical' Plugins\n",
    "\n",
    "Most OpenMDAO plugins are created simply by registering an entry point that refers\n",
    "to the class definition of the plugin or to some factory function that returns an instance of\n",
    "the plugin.  The following entry point types are all handled in this way:\n",
    "\n",
    "- component\n",
    "- group\n",
    "- driver\n",
    "- nl_solver\n",
    "- lin_solver\n",
    "- surrogate_model\n",
    "- case_recorder\n",
    "\n",
    "For these types of plugins, the entry point does nothing other than allow them to be listed using\n",
    "the [openmdao list_installed](list-installed) command.\n",
    "\n",
    "Here's an example of how to specify the *entry_points* arg to the *setup* call in `setup.py`\n",
    "for a component plugin class called `MyComponent` in a package called `my_plugins_package`\n",
    "in a module called `my_comp_plugin.py`:\n",
    "\n",
    "```\n",
    "entry_points={\n",
    "    'openmdao_component': [\n",
    "        'mycompplugin=my_plugins_package.my_comp_plugin:MyComponent'\n",
    "    ]\n",
    "}\n",
    "```\n",
    "\n",
    "Note that the actual entry point name, `mycompplugin` in the example above, isn't used for\n",
    "anything in the case of a 'typical' plugin.\n",
    "\n",
    "## CaseReader Plugins\n",
    "\n",
    "The entry point for a case reader should point to a function that returns a tuple of the form\n",
    "(file_extension, class), where *file_extension* contains the leading dot, for example '.sql',\n",
    "and *class* could either be the class definition of the plugin or a factory function returning\n",
    "an instance of the plugin.  The file extension is used to provide an automatic mapping to the\n",
    "correct case reader based on the file extension of the file being read.\n",
    "\n",
    "## Command Line Tool Plugins\n",
    "\n",
    "An entry point for an OpenMDAO command line tool plugin should point to a function that returns\n",
    "a tuple of the form (setup_parser_func, exec_func, help_string).  For example:\n",
    "\n",
    "```\n",
    "def _hello_setup():\n",
    "    \"\"\"\n",
    "    This command prints a hello message after final setup.\n",
    "    \"\"\"\n",
    "    return (_hello_setup_parser, _hello_exec, 'Print hello message after final setup.')\n",
    "```\n",
    "\n",
    "The *setup_parser_func* is a function taking a single *parser* argument that adds any arguments\n",
    "expected by the plugin to the *parser* object.  The *parser* is an *argparse.ArgumentParser* object.\n",
    "For example, the following code sets up a subparser for a `openmdao hello` command that adds a file\n",
    "argument and a `--repeat` option:\n",
    "\n",
    "```\n",
    "def _hello_setup_parser(parser):\n",
    "    \"\"\"\n",
    "    Set up the openmdao subparser (using argparse) for the 'openmdao hello' command.\n",
    "\n",
    "    Parameters\n",
    "    ----------\n",
    "    parser : argparse subparser\n",
    "        The parser we're adding options to.\n",
    "    \"\"\"\n",
    "    parser.add_argument('-r', '--repeat', action='store', dest='repeats',\n",
    "                        default=1, type=int, help='Number of times to say hello.')\n",
    "    parser.add_argument('file', metavar='file', nargs=1,\n",
    "                        help='Script to execute.')\n",
    "```\n",
    "\n",
    "\n",
    "The *exec_func* is a function that performs whatever action is necessary for the command line\n",
    "tool plugin to operate.  Typically this will involve registering another function that is to\n",
    "execute at some point during the execution of a script file.  For example, the following\n",
    "function registers a function that prints a `hello` message, specifying that it should execute\n",
    "after the `Problem._final_setup` method.\n",
    "\n",
    "```\n",
    "def _hello_exec(options, user_args):\n",
    "    \"\"\"\n",
    "    This registers the hook function and executes the user script.\n",
    "\n",
    "    Parameters\n",
    "    ----------\n",
    "    options : argparse Namespace\n",
    "        Command line options.\n",
    "    user_args : list of str\n",
    "        Args to be passed to the user script.\n",
    "    \"\"\"\n",
    "    script = options.file[0]\n",
    "\n",
    "    def _hello_after_final_setup(prob):\n",
    "        for i in range(options.repeats):\n",
    "            print('*** hello ***')\n",
    "        exit()   # If you want to exit after your command, you must explicitly do that here\n",
    "\n",
    "    # register the hook to execute after Problem.final_setup\n",
    "    _register_hook('final_setup', class_name='Problem', post=_hello_after_final_setup)\n",
    "\n",
    "    # load and execute the given script as __main__\n",
    "    _load_and_exec(script, user_args)\n",
    "```\n",
    "\n",
    "The final entry in the tuple returned by the function referred to by the entry point\n",
    "(in this case *_hello_setup*)\n",
    "is a string containing a high level description of the command.  This description will be displayed\n",
    "along with the name of the command when a user runs `openmdao -h`.\n",
    "\n",
    "Here's an example of how to specify the *entry_points* arg to the *setup* call in `setup.py`\n",
    "for our command line tool described above if it were inside of a package called `my_plugins_package`\n",
    "in a file called `hello_cmd.py`:\n",
    "\n",
    "\n",
    "```\n",
    "entry_points={\n",
    "        'openmdao_command': [\n",
    "            'hello=my_plugins_package.hello_cmd:_hello_setup'\n",
    "        ]\n",
    "}\n",
    "```\n",
    "\n",
    "In this case, the name of our entry point, `hello`, will be the name of the openmdao command line\n",
    "tool, so the user will activate the tool by typing `openmdao hello`.\n",
    "\n",
    "## Local Discovery\n",
    "\n",
    "After a python package containing OpenMDAO plugins has been installed in a user's python\n",
    "environment, they will be able to print a list of installed plugins using the\n",
    "[openmdao list_installed](list-installed) command.\n",
    "For example, if a package called `foobar` is installed, we could list all of the plugins\n",
    "found in that package using the following command:\n",
    "\n",
    "```\n",
    "openmdao list_installed -i foobar\n",
    "```\n",
    "\n",
    "The `list_installed` command simply goes through all of the entry points it finds in any of the\n",
    "openmdao entry point groups described above and displays them.\n",
    "\n",
    "\n",
    "## Global Discovery Using github\n",
    "\n",
    "Entry point groups are also used for global discovery of plugins.  They can be used (in slightly\n",
    "modified form, with underscores replaced with dashes) as *topic* strings in a github repository\n",
    "in order to allow a user to perform a global search over all of github to find any openmdao related\n",
    "plugin packages.\n",
    "\n",
    "\n",
    "## Plugin Creation from Scratch\n",
    "\n",
    "To create an OpenMDAO plugin from scratch, it may be helpful to use the [openmdao scaffold](ref-scaffold) tool.  It will automatically generate the directory structure for a python package and will define the entry point of a type that you specify.  For example, to create a scaffold for a python package called mypackage that contains a component plugin that's an ExplicitComponent called MyComp, do the following:\n",
    "\n",
    "```\n",
    "openmdao scaffold --base=ExplicitComponent --class=MyComp --package=mypackage\n",
    "```\n",
    "\n",
    "To instead create a package containing an openmdao command line tool called `hello` in\n",
    "a package called `myhello`, do the following:\n",
    "\n",
    "```\n",
    "openmdao scaffold --cmd=hello --package=myhello\n",
    "```\n",
    "\n",
    "## Converting Existing Classes to Plugins\n",
    "\n",
    "If you already have a package containing components, groups, etc. that work in the OpenMDAO\n",
    "framework, all you need to do to register them as plugins is to define an entry point in\n",
    "your `setup.py` file for each one.\n",
    "\n",
    "You can use the [openmdao compute_entry_points](compute-entry-points) command\n",
    "line tool to help you do this.  Running the tool with your installed package name will print\n",
    "out a list of all of the openmdao entry points required to register any openmdao compatible\n",
    "classes it finds in your package.  For example, if your package is called `mypackage`, you\n",
    "can list its entry points using\n",
    "\n",
    "```\n",
    "openmdao compute_entry_points mypackage\n",
    "```\n",
    "\n",
    "The entry points will be printed out in a form that can be pasted as a *setup* argument into\n",
    "your `setup.py` file.\n",
    "\n",
    "\n",
    "## Plugin Checklist\n",
    "\n",
    "To recap, to **fully** integrate your plugin into the OpenMDAO plugin infrastructure, you must do all\n",
    "of the following:\n",
    "\n",
    "\n",
    "1. The plugin will be part of a pip-installable python package.\n",
    "2. An entry point will be added to the appropriate entry point group (see above) of the\n",
    "    *entry_points* argument passed to the *setup* call in the *setup.py* file for the python package containing the plugin.\n",
    "3. If the package resides in a public **github** repository, the `openmdao` topic will be added\n",
    "    to the repository, along with topics for each openmdao entry point group (with underscores\n",
    "    converted to dashes, e.g., `openmdao_component` becomes `openmdao-component`) that\n",
    "    contains an openmdao entry point found in the package.\n",
    "4. If the package resides on the Python Package Index (PyPI), the string `openmdao` should be\n",
    "    mentioned in the package summary.\n",
    "5. To support the future ability to query PyPI package keywords, any openmdao entry point\n",
    "    groups used by the package should be added to the `keywords` argument to the *setup*\n",
    "    call in the *setup.py* file for the package.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.5"
  },
  "orphan": true
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
