Metadata-Version: 1.1
Name: offshoot
Version: 0.1.0
Summary: Modern, elegant, minimalistic but powerful plugin system for Python 3.5+.
Home-page: https://github.com/SerpentAI/offshoot
Author: Nicholas Brochu
Author-email: nicholas@serpent.ai
License: Apache License v2
Description: offshoot
        ========
        
        This one day in the past, you took your first step on your programming
        journey. Some days were tough, some days were great. You made progress.
        You made mistakes. You learned some best practices and design patterns.
        You've come to idolize low coupling and modularity. Eventually, you
        started working on more ambitious projects, always increasing in
        complexity; the possibilities endless. After implementing your 7th
        export format for your latest project, the words of the great Raymond
        Hettinger come to mind: *There's got to be a better way!* After a short
        stint going all out on inheritance and mixins, you turn your attention
        to plugins. You read up on them, get the general idea and start looking
        at what's available for Python. You are happy to find out there are a
        quite a few on the market. You start trying them out and for the most
        part, they work great, but it always feels like something is missing.
        Perhaps they make you go through crazy code gymnastics, lack features or
        are plain just horrible to look at. This is the moment you discover
        *offshoot*.
        
        ***offshoot***:
        
        -  Is a modern, elegant and minimalistic plugin system for Python 3.5+
        -  Is unintrusive; Stays out of our way. No file copying, no symlinks,
           nada!
        -  Provides a clear and simple plugin definition format.
        -  Understands your flow: Provides installation callbacks, can maintain
           a configuration and/or a requirements file for your plugins and has
           an optional plugin validation system on install.
        -  Can discover and import any plugin of any type anywhere in your code
           with a one-liner. No more complex plugin management.
        -  Batteries included. Comes with an executable to install/uninstall
           plugins.
        -  Is fully-tested and is under active development.
        -  Does not aim to please the *PEP 8* gods and the purists. Some dark
           magic is used unapologetically.
        
        Quick Tour
        ----------
        
        **Your Class you'd like to make pluggable**
        
        .. code:: python
        
            class ExportFormat:
                def __init__(self):
                    self.name = "Export Format"
        
                def export(self, data):
                    raise NotImplementedError()
        
                @classmethod
                def is_an_export_format(cls):
                    return True
        
        **Your Class made pluggable with *offshoot***
        
        .. code:: python
        
            import offshoot
        
            class ExportFormat(offshoot.Pluggable):
                def __init__(self):
                    self.name = "Export Format"
        
                @offshoot.expected
                def export(self, data):
                    raise NotImplementedError()
        
                @classmethod
                @offshoot.forbidden
                def is_an_export_format(cls):
                    return True
        
        Yes, that's it! More about those optional decorators later.
        
        **A sample *offshoot* plugin definition**
        
        .. code:: python
        
            import offshoot
        
            class YAMLExportFormatPlugin(offshoot.Plugin):
                name = "YAMLExportFormatPlugin"
                version = "0.1.0"
        
                libraries = ["PyYAML"]
        
                files = [
                    {"path": "export_formats/yaml.py", "pluggable": "ExportFormat"}
                ]
        
                config = {
                    "export_options": {
                        "width": 80
                    }
                }
        
                @classmethod
                def on_install(cls):
                    print("\n\n%s was installed successfully!" % cls.__name__)
        
                @classmethod
                def on_uninstall(cls):
                    print("\n\n%s was uninstalled successfully!" % cls.__name__)
        
            if __name__ == "__main__":
                offshoot.executable_hook(YAMLExportFormatPlugin)
        
        **A sample *offshoot* plugin file**
        
        .. code:: python
        
            import offshoot
            from export_format import ExportFormat
        
            import yaml
        
            class YAMLExportFormat(ExportFormat):
                def export(self, data):
                    return yaml.dump(data)
        
        **Installing an *offshoot* plugin from the command line**
        
        ``offshoot install YAMLExportFormatPlugin``
        
        **Automatic *offshoot* plugin discovery and importing**
        
        .. code:: python
        
            import offshoot
            offshoot.discover("ExportFormat", globals())
        
            YAMLExportFormat # Now in scope!
        
        Requirements
        ------------
        
        -  PyYAML (On the roadmap to make it optional so the project is 100%
           dependency-free!)
        
        Installation
        ------------
        
        ``pip install offshoot``
        
        Configuration
        -------------
        
        Default Configuration Values
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        
        .. code:: python
        
            {
                "modules": [],
                "file_paths": {
                    "plugins": "plugins",
                    "config": "config/config.plugins.yml",
                    "libraries": "requirements.plugins.txt"
                },
                "allow": {
                    "files": True,
                    "config": True,
                    "libraries": True,
                    "callbacks": True
                },
                "sandbox_configuration_keys": True
            }
        
        Initializing offshoot
        ~~~~~~~~~~~~~~~~~~~~~
        
        Initializing *offshoot* will save a YAML copy of the default
        configuration to *offshoot.yml* which you can then modify to suit your
        needs. Just run the following in the command line: ``offshoot init``
        
        Configuration Keys
        ~~~~~~~~~~~~~~~~~~
        
        -  **modules**: Perhaps the most important key to modify since nothing
           will happen without some valid module paths in there. *offshoot*
           needs to discover pluggable classes in the project at import time. It
           will explore the modules listed here to find classes that extend
           *offshoot.Pluggable*
        -  **file\_paths**: Directories and file paths to use when *offshoot*
           needs to hit the file system. *plugins* is where *offshoot* will look
           for plugin files. The defaults should suffice, but do make sure they
           exist.
        -  **allow**: *offshoot* allows you to enable/disable certain part of
           the plugin installation. It is recommended to leave all values to
           True.
        -  **sandbox\_configuration\_keys**: If you chose to let *offshoot*
           merge configuration keys during plugin installation, it can either
           merge them all at the root level (False) or sandbox them under the
           plugin name (True)
        
        Usage
        -----
        
        Initializing Offshoot
        ~~~~~~~~~~~~~~~~~~~~~
        
        The first thing you will want to do after installing *offshoot* is run
        ``offshoot init`` in the command line at the root of your project. This
        will create a configuration file named *offshoot.yml*. You can leave it
        be for now but we will go back to it later.
        
        Making Your Classes Pluggable
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        
        To make a class pluggable with *offshoot* all that technically needs to
        be done is extend it with *offshoot.Pluggable*
        
        So you go from this:
        
        .. code:: python
        
            class Shape:
                pass
        
        To this:
        
        .. code:: python
        
            import offshoot
        
            class Shape(offshoot.Pluggable):
                pass
        
        Then for every class you make pluggable, you append its module path to
        *offshoot.yml* under the modules key. This means that if you make
        ``shape.py`` and ``shapes/rectangle.py`` pluggable, your modules value
        will look like this ``modules: ["shape", "shapes/rectangle"]``
        
        Magic Validation
        ^^^^^^^^^^^^^^^^
        
        *offshoot* comes with an optional validation system for your pluggable
        classes. You can control which class, instance and static methods are
        either *expected*, *accepted* or *forbidden* in a plugin file. The way
        you do this couldn't be any simpler: you wrap them with a decorator. It
        ends up looking like the following:
        
        .. code:: python
        
            import offshoot
        
            class PluggableClass(offshoot.Pluggable):
                @offshoot.expected
                def expected_function(self):
                    raise NotImplementedError()
        
                @classmethod
                @offshoot.accepted
                def accepted_function(cls):
                    raise NotImplementedError()
        
                @staticmethod
                @offshoot.forbidden
                def forbidden_function():
                    raise NotImplementedError()
        
        If a plugin file is missing an *expected* method, or defining a
        *forbidden* method, it will be rejected and the installation will be
        stopped and reverted.
        
        They are called magic decorator because under the hood, they do
        absolutely nothing. They are however found using Python's abstract
        syntax trees (*ast* in the stdlib) during plugin installation and
        validation can be performed.
        
        Installation Callbacks
        ^^^^^^^^^^^^^^^^^^^^^^
        
        In addition to magic validators, you have the option to add callbacks
        that will be executed for each file installed/uninstalled by a plugin.
        
        To leverage these callbacks, simply add these functions to your
        pluggable class:
        
        .. code:: python
        
            @classmethod
            def on_file_install(cls, **kwargs):
                pass
        
            @classmethod
            def on_file_uninstall(cls, **kwargs):
                pass
        
        Contained in ``kwargs`` are the file path and the name of the pluggable
        class.
        
        One common application for these callbacks would be to seed some values
        in a database. If we stick the ExportFormat example, once you install a
        YAMLExportFormat plugin, you may want to add it to a *export\_formats*
        table along with the name of the class. That could then allow list the
        available export format options in a more logical fashion. Similarly,
        you'd want that option to be cleaned up when you uninstall the plugin.
        
        Anatomy of an *offshoot* Plugin
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        
        Expected File Structure
        ^^^^^^^^^^^^^^^^^^^^^^^
        
        .. code:: sh
        
            PLUGINS_DIRECTORY (defined in offshoot.yml)
            ├── ShapesPlugin  # Name of the plugin. Matches the plugin class name in plugin.py
            │   ├── __init__.py
            │   ├── files  # Any file other than the plugin definition goes here
            │   │   ├── __init__.py
            │   │   ├── helpers.py  # Supporting file. Not in plugin definition but can be accessed by plugin files.
            │   │   └── shapes
            │   │       ├── __init__.py
            │   │       ├── rectangle.py  # Variant of the Shape pluggable class. Included in plugin definition file
            │   │       ├── star.py  # Variant of the Shape pluggable class. Included in plugin definition file
            │   │       └── triangle.py  # Variant of the Shape pluggable class. Included in plugin definition file
            │   └── plugin.py  # Plugin definition file
            ├── __init__.py
        
        You are free to structure your file hierarchy exactly the way you want
        inside of the *files* directory. You can also add as many supporting
        files as needed.
        
        ``__init__.py`` files DO need to be peppered everywhere as we want our
        plugin structure to be accessible as a package.
        
        Plugin Definition File (plugin.py)
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        
        The plugin definition file turns out to be a Python file with a class
        that extends ``offshoot.Plugin``. The name of that class needs to be an
        exact match of the name of the directory containing the plugin.
        
        Here's what plugin definition file would look like for a plugin using
        the file structure above. It is annotated to explain what the various
        sections do.
        
        .. code:: python
        
            import offshoot
        
            class ShapesPlugin(offshoot.Plugin):  # We extend offshoot.Plugin
                name = "ShapesPlugin"  # We define a name for the plugin. Matches the class name.
                version = "0.1.0"  # We define a version number for the plugin.
        
                # A list of required PyPI packages for the plugin.
                # Optional. These libraries will be merge to your offshoot requirements.txt during the installation. Set to None if you don't intend to use it.
                libraries = [
                    "requests",
                    "requests-respectful==0.2.0"
                ]
        
                # A list of file objects that target pluggable classes in the project.
                # Required. "path" is the file path relative to the plugin root. "pluggable" is the pluggable class' name.
                files = [
                    {"path": "shapes/rectangle.py", "pluggable": "Shape"},
                    {"path": "shapes/triangle.py", "pluggable": "Shape"},
                    {"path": "shapes/star.py", "pluggable": "Shape"}
                ]
        
                # A Python dict containing configuration keys that can be referenced by your plugin files at runtime.
                # Optional. Any valid Python dict is accepted. Set to None if you don't intend to use it.
                config = {
                    "i_am_a": {
                        "plugin": True,
                        "human": False
                    },
                    "urls": ["http://serpent.ai", "https://github.com/SerpentAI/offshoot"],
                    "count": 42,
                }
        
                # Callbacks to be performed once per install / uninstall
                # Optional.
                @classmethod
                def on_install(cls):
                    print("\n\n%s was installed successfully!" % cls.__name__)
        
                @classmethod
                def on_uninstall(cls):
                    print("\n\n%s was uninstalled successfully!" % cls.__name__)
        
            # This hook always needs to be present in a plugin definition file.
            # It is used by the installation process. Pass it the class you just defined above.
            if __name__ == "__main__":
                offshoot.executable_hook(ShapesPlugin)
        
        Plugin Files Extending the Pluggable Classes
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        
        Each plugin file needs to define a class that extends one of the classes
        that were previously made pluggable. If magic validation decorators were
        used when making the class pluggable, the plugin file needs to validate
        against that protocol successfully to be installed.
        
        Here is a sample plugin file, following our Shapes plugin theme:
        
        .. code:: python
        
            from shape import Shape
        
            class Rectangle(Shape):
                def __init__(self, **kwargs):
                    super().__init__(**kwargs)
        
                    self.name = "Rectangle"
                    self.sides = 4
                    self.is_polygon = True
        
                @property
                def shape_is_a_polygon(self):
                    return "A Rectangle is a polygon!"
        
                def area(self):
                    raise NotImplementedError()
        
                def draw(self):
                    raise NotImplementedError()
        
        You are free to go way beyond the pluggable class' protocol. You can
        require functions from supporting files bundled with the plugin and make
        use of your required PyPI packages and/or configuration keys.
        
        The *offshoot* Manifest
        ~~~~~~~~~~~~~~~~~~~~~~~
        
        The *offshoot* manifest is a critical file that gets created when you
        attempt to install a plugin for the first time. It contains the metadata
        of installed plugins and helps maintain the overall *offshoot* state.
        Look for *offshoot.manifest.json* if you want to take a peek under the
        hood. Be aware that editing or deleting this file will cause issues!
        
        The *offshoot* Executable
        ~~~~~~~~~~~~~~~~~~~~~~~~~
        
        The executable is rather minimalistic at the moment but it used to
        perform two crucial operations: Installing and uninstalling plugins.
        
        Installing Plugins
        ^^^^^^^^^^^^^^^^^^
        
        The first step is making sure the plugin has been copied/cloned into the
        plugin directory defined in *offshoot.yml*
        
        After that, simply run the following in the command line:
        
        ``offshoot install PLUGIN_NAME``
        
        **What happens when a plugin is installed?**
        
        1. The *offshoot* configuration file is consulted to fetch the allow
           flags
        2. If *files* are allowed: Every plugin file in the plugin definition is
           validated against its pluggable class' protocol. If even one
           validation test fails, the installation fails and is reverted. File
           installation callbacks are executed.
        3. If *config* is allowed: The configuration keys contained in the
           plugin definition file are merged in the configuration file defined
           in *offshoot.yml*.
        4. If *libraries* are allowed: Libraries contained in the plugin
           definition file are merged in the libraries file defined in
           *offshoot.yml*.
        5. If *callbacks* are allowed: The *on\_install* callback is executed.
        6. The plugin metadata is appended to the manifest.
        
        The installation process will not automatically install libraries with
        *pip*. It is assumed the user will permorm the pip installation.
        
        Uninstalling Plugins
        ^^^^^^^^^^^^^^^^^^^^
        
        ``offshoot uninstall PLUGIN_NAME``
        
        Discovering & Importing Plugins
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        
        Last but not least, a simple way of getting installed plugins' classes
        into scope has been provided.
        
        Here's how it's done:
        
        .. code:: python
        
            import offshoot
            offshoot.discover("Shape", globals())
        
            # All installed plugin classes that extend the Shape pluggable class are now into scope!
        
        This can be done literally anywhere in your application.
        
        Tips & Tricks
        ~~~~~~~~~~~~~
        
        Listing installed plugins
        ^^^^^^^^^^^^^^^^^^^^^^^^^
        
        A utility method is exposed allowing you to fetch a list of the
        currently installed plugins as per the manifest.
        
        Simply run:
        
        .. code:: python
        
            offshoot.installed_plugins()
        
        Example output:
        
        .. code:: python
        
            ["ShapesPlugin - 0.1.0"]
        
        Merging the *offshoot* configuration keys with your application configuration at runtime.
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        
        Chances are you already have a YAML configuration file for your
        application. In some situations, it may become desirable to merge that
        configuration dict with *offshoot*'s configuration dict.
        
        Here's a code snippet to achieve this:
        
        .. code:: python
        
            import yaml
        
            # Application Configuration
            with open("config/config.yml", "r") as f:
                config = yaml.safe_load(f)
        
            # Offshoot Configuration
            import offshoot
        
            with open(offshoot.config["file_paths"]["config"], "r") as f:
                plugin_config = yaml.safe_load(f)
        
            # Merge Configuration. Application Configuration takes priority in the key space.
            config = {**plugin_config, **config}
        
        You can then import *config* from this file to have the merged
        configurations.
        
        Tests
        -----
        
        Unit tests for the project can be run with the following command:
        
        ``python -m pytest tests --spec``
        
        You can install the test requirements by refering to
        *requirements.test.txt* in the repository.
        
        Examples
        --------
        
        You can find full examples in the ``examples`` directory of the
        repository.
        
        Roadmap / Contribution Ideas
        ----------------------------
        
        -  Make PyYaml optional. Use it if it's there, otherwise default on JSON
           or INI
        -  Explore supporting the extension of 3rd-party modules.
        -  Windows support? Python 2 branch?
        -  Clean up tests. A lot of repetition.
        -  ... Anything else that makes sense really!
        
        *If you like offshoot, feel free to check out
        `requests-respectful <https://github.com/SerpentAI/requests-respectful>`__,
        also by `SerpentAI <http://serpent.ai>`__*
        
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: English
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
