Giter VIP home page Giter VIP logo

plux's Introduction

Plux

CI badge PyPI Version PyPI License Code style: black

plux is the dynamic code loading framework used in LocalStack.

Overview

Plux builds a higher-level plugin mechanism around Python's entry point mechanism. It provides tools to load plugins from entry points at run time, and to discover entry points from plugins at build time (so you don't have to declare entry points statically in your setup.py).

Core concepts

  • PluginSpec: describes a Plugin. Each plugin has a namespace, a unique name in that namespace, and a PluginFactory (something that creates Plugin the spec is describing. In the simplest case, that can just be the Plugin's class).
  • Plugin: an object that exposes a should_load and load method. Note that it does not function as a domain object (it does not hold the plugins lifecycle state, like initialized, loaded, etc..., or other metadata of the Plugin)
  • PluginFinder: finds plugins, either at build time (by scanning the modules using pkgutil and setuptools) or at run time (reading entrypoints of the distribution using importlib)
  • PluginManager: manages the run time lifecycle of a Plugin, which has three states:
    • resolved: the entrypoint pointing to the PluginSpec was imported and the PluginSpec instance was created
    • init: the PluginFactory of the PluginSpec was successfully invoked
    • loaded: the load method of the Plugin was successfully invoked

architecture

Loading Plugins

At run time, a PluginManager uses a PluginFinder that in turn uses importlib to scan the available entrypoints for things that look like a PluginSpec. With PluginManager.load(name: str) or PluginManager.load_all(), plugins within the namespace that are discoverable in entrypoints can be loaded. If an error occurs at any state of the lifecycle, the PluginManager informs the PluginLifecycleListener about it, but continues operating.

Discovering entrypoints

To build a source distribution and a wheel of your code with your plugins as entrypoints, simply run python setup.py plugins sdist bdist_wheel. If you don't have a setup.py, you can use the plux build frontend and run python -m plux entrypoints.

How it works: For discovering plugins at build time, plux provides a custom setuptools command plugins, invoked via python setup.py plugins. The command uses a special PluginFinder that collects from the codebase anything that can be interpreted as a PluginSpec, and creates from it a plugin index file plux.json, that is placed into the .egg-info distribution metadata directory. When a setuptools command is used to create the distribution (e.g., python setup.py sdist/bdist_wheel/...), plux finds the plux.json plugin index and extends automatically the list of entry points (collected into .egg-info/entry_points.txt). The plux.json file becomes a part of the distribution, s.t., the plugins do not have to be discovered every time your distribution is installed elsewhere. Discovering at build time also works when using python -m build, since it calls registered setuptools scripts.

Examples

To build something using the plugin framework, you will first want to introduce a Plugin that does something when it is loaded. And then, at runtime, you need a component that uses the PluginManager to get those plugins.

One class per plugin

This is the way we went with LocalstackCliPlugin. Every plugin class (e.g., ProCliPlugin) is essentially a singleton. This is easy, as the classes are discoverable as plugins. Simply create a Plugin class with a name and namespace and it will be discovered by the build time PluginFinder.

from plux import Plugin

# abstract case (not discovered at build time, missing name)
class CliPlugin(Plugin):
    namespace = "my.plugins.cli"

    def load(self, cli):
        self.attach(cli)

    def attach(self, cli):
        raise NotImplementedError

# discovered at build time (has a namespace, name, and is a Plugin)
class MyCliPlugin(CliPlugin):
    name = "my"

    def attach(self, cli):
        # ... attach commands to cli object

now we need a PluginManager (which has a generic type) to load the plugins for us:

cli = # ... needs to come from somewhere

manager: PluginManager[CliPlugin] = PluginManager("my.plugins.cli", load_args=(cli,))

plugins: List[CliPlugin] = manager.load_all()

# todo: do stuff with the plugins, if you want/need
#  in this example, we simply use the plugin mechanism to run a one-shot function (attach) on a load argument

Re-usable plugins

When you have lots of plugins that are structured in a similar way, we may not want to create a separate Plugin class for each plugin. Instead we want to use the same Plugin class to do the same thing, but use several instances of it. The PluginFactory, and the fact that PluginSpec instances defined at module level are discoverable (inpired by pluggy), can be used to achieve that.

from plux import Plugin, PluginFactory, PluginSpec
import importlib

class ServicePlugin(Plugin):

    def __init__(self, service_name):
        self.service_name = service_name
        self.service = None

    def should_load(self):
        return self.service_name in config.SERVICES

    def load(self):
        module = importlib.import_module("localstack.services.%s" % self.service_name)
        # suppose we define a convention that each service module has a Service class, like moto's `Backend`
        self.service = module.Service()

def service_plugin_factory(name) -> PluginFactory:
    def create():
        return ServicePlugin(name)

    return create

# discoverable
s3 = PluginSpec("localstack.plugins.services", "s3", service_plugin_factory("s3"))

# discoverable
dynamodb = PluginSpec("localstack.plugins.services", "dynamodb", service_plugin_factory("dynamodb"))

# ... could be simplified with convenience framework code, but the principle will stay the same

Then we could use the PluginManager to build a Supervisor

from plux import PluginManager

class Supervisor:
    manager: PluginManager[ServicePlugin]

    def start(self, service_name):
        plugin = self.manager.load(service_name)
        service = plugin.service
        service.start()

Functions as plugins

with the @plugin decorator, you can expose functions as plugins. They will be wrapped by the framework into FunctionPlugin instances, which satisfy both the contract of a Plugin, and that of the function.

from plugin import plugin

@plugin(namespace="localstack.configurators")
def configure_logging(runtime):
    logging.basicConfig(level=runtime.config.loglevel)

    
@plugin(namespace="localstack.configurators")
def configure_somethingelse(runtime):
    # do other stuff with the runtime object
    pass

With a PluginManager via load_all, you receive the FunctionPlugin instances, that you can call like the functions

runtime = LocalstackRuntime()

for configurator in PluginManager("localstack.configurators").load_all():
    configurator(runtime)

Configuring your distribution

If you are building a python distribution that exposes plugins discovered by plux, you need to configure your projects build system so other dependencies creates the entry_points.txt file when installing your distribution.

For a pyproject.toml template this involves adding the build-system section:

[build-system]
requires = ['setuptools', 'wheel', 'plux>=1.3.1']
build-backend = "setuptools.build_meta"

# ...

Install

pip install plux

Develop

Create the virtual environment, install dependencies, and run tests

make venv
make test

Run the code formatter

make format

Upload the pypi package using twine

make upload

plux's People

Contributors

giograno avatar thrau avatar tjni avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

plux's Issues

Add report log output at end of plugins command

Currently the log output of the plugins command is a bit of a mess. There's no real good way of seeing the warnings that some files weren't importable and scannable for plugins. A nicely formatted report at the end of the command would be useful, containing

  • all discovered plugins and their files
  • all files that had an error while importing (and the related error)

It would also be useful to configure the loglevel and capture all the output created by simply importing code, similar to pytests' --log-cli-level

Provide a way to regenerate plux entrypoints without a `setup.py`

Currently, you need a setup.py file to re-generate plugins and the entry_points.txt file. With modern project configs without a setup.py, this is no longer possible. See the discussion here: pypa/setuptools#4223.

In plux we need a new way to regenerate entrypoints without the setuptools CLI. This will involve:

  • providing a CLI module (that allows, say, python -m plux --entrypoints)
  • providing the necessary bindings to create a setuptools Distribution and Command objects to invoke the plugins command.

Provide command to generate setup.py with entrypoints

Sometimes it can be interesting to not need plux at install time, which can be facilitated by adding the entrypoints directly in the setup.py. It would be great if plux could provide a method similar to setup.py plugins, which just creates or modifies an existing setup.py file by adding the located plugin entrypoints.

[Request] Use tags when creating releases

Trying to package localstack for Nixpkgs. Wanted to pull in tests from github to ensure that plux works, however, it's hard to know which commit was used for a particular release.

plux entrypoints doesn't follow package_dir

when building entrypoints using python -m plux entrypoints, plux doesn't automatically follow package_dir, and also doesn't allow any specification of a directory to look into for plugins.

Plux breaks all other plugins: AttributeError: module 'plugin.setuptools' has no attribute 'load_plux_entrypoints'

We recently adopted localstack in some builds and integrations, and install localstack in environments with other package installations. This has been working great, but recently found this error breaks all package installations and builds, and traced it to this package and recent release.

When plux is installed in a conda environment it overrides the plugin management for all packages - and not just for localstack.

This is an error that occurs in the attrs package, for example when plux is installed:

AttributeError: module 'plugin.setuptools' has no attribute 'load_plux_entrypoints'

ImportError: module 'plugin.setuptools' has no attribute 'load_plux_entrypoints'

Our fix is to explicitly uninstall plux after installing localstack. Is there a way to loosen this plugin management?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.