Browse Source

Define hooks, loader and registry

Alec Nikolas Reiter 7 years ago
parent
commit
051119678c
4 changed files with 202 additions and 5 deletions
  1. 47 5
      flaskbb/app.py
  2. 47 0
      flaskbb/plugins/models.py
  3. 64 0
      flaskbb/plugins/spec.py
  4. 44 0
      migrations/7c3fcf8a3335_add_plugin_tables.py

+ 47 - 5
flaskbb/app.py

@@ -15,6 +15,7 @@ from functools import partial
 
 from sqlalchemy import event
 from sqlalchemy.engine import Engine
+from sqlalchemy.exc import OperationalError
 from flask import Flask, request
 from flask_login import current_user
 
@@ -28,9 +29,10 @@ from flaskbb.forum.views import forum
 # models
 from flaskbb.user.models import User, Guest
 # extensions
-from flaskbb.extensions import (db, login_manager, mail, cache, redis_store,
-                                debugtoolbar, alembic, themes, plugin_manager,
-                                babel, csrf, allows, limiter, celery, whooshee)
+from flaskbb.extensions import (alembic, allows, babel, cache, celery, csrf,
+                                db, debugtoolbar, limiter, login_manager, mail,
+                                plugin_manager, redis_store, themes, whooshee)
+
 # various helpers
 from flaskbb.utils.helpers import (time_utcnow, format_date, time_since,
                                    crop_title, is_online, mark_online,
@@ -50,6 +52,11 @@ from flaskbb.utils.search import (PostWhoosheer, TopicWhoosheer,
 # app specific configurations
 from flaskbb.utils.settings import flaskbb_config
 
+from flaskbb.plugins.models import PluginRegistry
+from flaskbb.plugins import spec
+
+from pluggy import PluginManager
+
 
 def create_app(config=None):
     """Creates the app.
@@ -61,16 +68,17 @@ def create_app(config=None):
                    later overwrite it from the ENVVAR.
     """
     app = Flask("flaskbb")
-
     configure_app(app, config)
     configure_celery_app(app, celery)
-    configure_blueprints(app)
     configure_extensions(app)
+    load_plugins(app)
+    configure_blueprints(app)
     configure_template_filters(app)
     configure_context_processors(app)
     configure_before_handlers(app)
     configure_errorhandlers(app)
     configure_logging(app)
+    app.pluggy.hook.flaskbb_additional_setup(app=app, pluggy=app.pluggy)
 
     return app
 
@@ -96,6 +104,7 @@ def configure_app(app, config):
     # Parse the env for FLASKBB_ prefixed env variables and set
     # them on the config object
     app_config_from_env(app, prefix="FLASKBB_")
+    app.pluggy = PluginManager('flaskbb', implprefix='flaskbb_')
 
 
 def configure_celery_app(app, celery):
@@ -125,6 +134,8 @@ def configure_blueprints(app):
         message, url_prefix=app.config["MESSAGE_URL_PREFIX"]
     )
 
+    app.pluggy.hook.flaskbb_load_blueprints(app=app)
+
 
 def configure_extensions(app):
     """Configures the extensions."""
@@ -239,6 +250,8 @@ def configure_template_filters(app):
 
     app.jinja_env.filters.update(filters)
 
+    app.pluggy.hook.flaskbb_jinja_directives(app=app)
+
 
 def configure_context_processors(app):
     """Configures the context processors."""
@@ -273,6 +286,8 @@ def configure_before_handlers(app):
             else:
                 mark_online(request.remote_addr, guest=True)
 
+    app.pluggy.hook.flaskbb_request_processors(app=app)
+
 
 def configure_errorhandlers(app):
     """Configures the error handlers."""
@@ -289,6 +304,8 @@ def configure_errorhandlers(app):
     def server_error_page(error):
         return render_template("errors/server_error.html"), 500
 
+    app.pluggy.hook.flaskbb_errorhandlers(app=app)
+
 
 def configure_logging(app):
     """Configures logging."""
@@ -349,3 +366,28 @@ def configure_logging(app):
                                  parameters, context, executemany):
             total = time.time() - conn.info['query_start_time'].pop(-1)
             app.logger.debug("Total Time: %f", total)
+
+
+def load_plugins(app):
+    app.pluggy.add_hookspecs(spec)
+    try:
+        with app.app_context():
+            plugins = PluginRegistry.query.all()
+
+    except OperationalError:
+        return
+
+    for plugin in plugins:
+        if not plugin.enabled:
+            app.pluggy.set_blocked(plugin.name)
+
+    app.pluggy.load_setuptools_entrypoints('flaskbb_plugin')
+    app.pluggy.hook.flaskbb_extensions(app=app)
+
+    loaded_names = set([p[0] for p in app.pluggy.list_name_plugin()])
+    registered_names = set([p.name for p in plugins])
+    unregistered = [PluginRegistry(name=name) for name in loaded_names - registered_names]
+
+    with app.app_context():
+        db.session.add_all(unregistered)
+        db.session.commit()

+ 47 - 0
flaskbb/plugins/models.py

@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.plugins.models
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    This module provides registration and a basic DB backed key-value store for plugins.
+
+    :copyright: (c) 2017 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+from sqlalchemy import UniqueConstraint
+from sqlalchemy.ext.associationproxy import association_proxy
+from sqlalchemy.orm.collections import attribute_mapped_collection
+
+from flaskbb.extensions import db
+
+
+class PluginStore(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.Unicode(255), nullable=False)
+    value = db.Column(db.Unicode(255))
+    plugin_id = db.Column(db.Integer, db.ForeignKey('plugin_registry.id'))
+
+    __table_args__ = (UniqueConstraint('name', 'plugin_id', name='plugin_kv_uniq'), )
+
+    def __repr__(self):
+        return '<FlaskBBPluginSetting plugin={} name={} value={}>'.format(
+            self.plugin.name, self.name, self.value
+        )
+
+
+class PluginRegistry(db.Model):
+    id = db.Column(db.Integer, primary_key=True)
+    name = db.Column(db.Unicode(255), unique=True, nullable=False)
+    enabled = db.Column(db.Boolean, default=True)
+    values = db.relationship(
+        'PluginStore',
+        collection_class=attribute_mapped_collection('name'),
+        cascade='all, delete-orphan',
+        backref='plugin'
+    )
+    settings = association_proxy(
+        'values', 'value', creator=lambda k, v: PluginStore(name=k, value=v)
+    )
+
+    def __repr__(self):
+        return '<FlaskBBPlugin name={} enabled={}>'.format(self.name, self.id)

+ 64 - 0
flaskbb/plugins/spec.py

@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+"""
+    flaskbb.plugins.spec
+    ~~~~~~~~~~~~~~~~~~~~~~~
+
+    This module provides the core FlaskBB plugin hook definitions
+
+    :copyright: (c) 2017 by the FlaskBB Team.
+    :license: BSD, see LICENSE for more details.
+"""
+
+from pluggy import HookspecMarker
+
+spec = HookspecMarker('flaskbb')
+
+
+@spec
+def flaskbb_extensions(app):
+    """
+    Hook for initializing any plugin loaded extensions
+    """
+
+
+@spec
+def flaskbb_load_blueprints(app):
+    """
+    Hook for registering blueprints
+    """
+
+
+@spec
+def flaskbb_request_processors(app):
+    """
+    Hook for registering pre/post request processors
+    """
+
+
+@spec
+def flaskbb_errorhandlers(app):
+    """
+    Hook for registering error handlers
+    """
+
+
+@spec
+def flaskbb_jinja_directives(app):
+    """
+    Hook for registering jinja filters, context processors, etc
+    """
+
+
+@spec
+def flaskbb_additional_setup(app, pluggy):
+    """
+    Hook for any additional setup a plugin wants to do after all other application
+    setup has finished
+    """
+
+
+@spec
+def flaskbb_cli(cli):
+    """
+    Hook for registering CLI commands
+    """

+ 44 - 0
migrations/7c3fcf8a3335_add_plugin_tables.py

@@ -0,0 +1,44 @@
+"""Add plugin tables
+
+Revision ID: 7c3fcf8a3335
+Revises: 
+Create Date: 2017-08-12 12:41:04.725309
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '7c3fcf8a3335'
+down_revision = None
+branch_labels = ('default',)
+depends_on = None
+
+
+def upgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table('plugin_registry',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.Unicode(length=255), nullable=False),
+    sa.Column('enabled', sa.Boolean(), nullable=True),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('name')
+    )
+    op.create_table('plugin_store',
+    sa.Column('id', sa.Integer(), nullable=False),
+    sa.Column('name', sa.Unicode(length=255), nullable=False),
+    sa.Column('value', sa.Unicode(length=255), nullable=True),
+    sa.Column('plugin_id', sa.Integer(), nullable=True),
+    sa.ForeignKeyConstraint(['plugin_id'], ['plugin_registry.id'], ),
+    sa.PrimaryKeyConstraint('id'),
+    sa.UniqueConstraint('name', 'plugin_id', name='plugin_kv_uniq')
+    )
+    # ### end Alembic commands ###
+
+
+def downgrade():
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_table('plugin_store')
+    op.drop_table('plugin_registry')
+    # ### end Alembic commands ###