modflowpy / flopy

A Python package to create, run, and post-process MODFLOW-based models.
https://flopy.readthedocs.io
Other
517 stars 313 forks source link

Plugin Architecture for flopy #1470

Open jlarsen-usgs opened 2 years ago

jlarsen-usgs commented 2 years ago

Hello,

Following the discussion about investigating a plugin architecture for modflowapi packages I've done a little bit of research into how python plugins work and created a simple example to dynamically search for and load plugins in an existing python package. In essence, this would allow user developed modflowapi packages to be installed seperately, but be discoverable and useable by flopy.

The first step is to create a standardized entry point naming convention for the existing python package (i.e. flopy). This could be something like flopy.plugin or flopy.package.plugin. The entry point name will be used to search the python installations to see if they include this "entry point".

After that we can create a module, such as plugins.py in this example, that programatically search entry points and then import them into the existing software package when plugins.py is imported. For this code example I use test_plugins.plugin for the entry point:

import sys

if sys.version_info < (3, 10):
    from importlib_metadata import entry_points
else:
    from importlib.metadata import entry_points

this_module = sys.modules[__name__]
eps = entry_points(group="test_plugins.plugin")
installed_plugins = {}
for ep in eps:
    _x = ep.load()
    setattr(this_module, ep.name, _x())
    installed_plugins[ep.name] = _x()

Once this script exists, a plugin developer can create code as a seperate installable python package for distribution, wrap it with a simple function, add an [options.entry_points] record to their setup.cfg file, and specify that it is a test_plugins.plugin entry point. Example plugin code and setup.cfg entry.

class FancyHelloWord():
    def __init__(self):
        self.str = "@@!! Hello World !!@@"

    def display(self):
        print(self.str)

def plugin():
    return FancyHelloWord
[options.entry_points]
test_plugins.plugin =
    FancyHelloWorld = src.plugin:plugin

Finally, the user will be able to call the new plugin features from the plugins.py module in the existing software. Example:

from main_plugin_arch import plugins

fhw = plugins.FancyHelloWorld()
fhw.display()

print(plugins.installed_plugins)

I put together a full working example with instructions that can be found at https://github.com/jlarsen-usgs/plugin_example.git

Any thoughts or ideas on this @langevin-usgs @jdhughes-usgs @spaulins-usgs? Feel free to mention others that would be interested in this discussion.

langevin-usgs commented 2 years ago

Hey @jlarsen-usgs, this looks very promising to me. Maybe the next step is for you and @spaulins-usgs to start exploring an implementation of the AG package as a flopy plugin?