Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pluggy based Plugins #311

Merged
merged 57 commits into from Nov 18, 2017
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
cfc7d9a
List pluggy as a dependency
justanr Aug 1, 2017
0511196
Define hooks, loader and registry
justanr Aug 12, 2017
113fbbc
Fix plugin registry repr error
justanr Aug 13, 2017
a3f4e1b
Move portal plugin to seperate repo
sh4nks Sep 4, 2017
cae9525
Further work to replace Flask-Plugins with pluggy
sh4nks Sep 4, 2017
d738e70
Provide method to generate a settings form
sh4nks Sep 5, 2017
37bb1fb
Subclass PluginManager to add some flaskbb specific stuff
sh4nks Sep 5, 2017
a0e99ca
Don't ignore the plugins folder
sh4nks Sep 5, 2017
734f062
Use the same logic for modifying flaskbb and plugin settings
sh4nks Sep 5, 2017
9b6ccb7
Make Plugins work on CLI
justanr Sep 7, 2017
63593d3
Merge branch 'master' into Pluggy
justanr Sep 10, 2017
f503559
Make setting value types enum
justanr Sep 10, 2017
c66dcc1
Fix merge issues
justanr Sep 10, 2017
1e680d6
Make fixtures use enum values
justanr Sep 10, 2017
5c3f136
Clean up some stuff
justanr Sep 10, 2017
458d1b4
Remove assocproxy from PluginStore
justanr Sep 10, 2017
f883d51
Add methods to manage settings for plugins
sh4nks Sep 11, 2017
a0bd7a9
Cleanup ManagementSettings view
sh4nks Sep 11, 2017
5430948
Update stylesheet to include a 'nav-header' class
sh4nks Sep 11, 2017
567aff0
Only display plugins with settings
sh4nks Sep 11, 2017
7fb6bab
Merge remote-tracking branch 'upstream/master' into Pluggy
justanr Sep 19, 2017
1f59916
Merge remote-tracking branch 'origin/Pluggy' into Pluggy
justanr Sep 19, 2017
e15146a
Remove plugin manager from extensions
justanr Sep 19, 2017
ecdd1b6
Merge remote-tracking branch 'origin/master' into Pluggy
justanr Sep 24, 2017
0450656
Merge remote-tracking branch 'origin' into Pluggy
justanr Sep 24, 2017
bc9d73e
Point pluggy migration to hidden posts/topic migration
justanr Sep 24, 2017
b8be1f0
Merge plugin migration files
sh4nks Sep 27, 2017
37b5dfe
Move validate_plugin into the plugin utils module
sh4nks Sep 27, 2017
31b962e
Remove zombie plugins from db
sh4nks Sep 27, 2017
2ea0f92
Use PluginRegistry to get all the needed info for a plugin
sh4nks Sep 27, 2017
c130bdb
Only remove the settings from the db
sh4nks Sep 28, 2017
44eab45
Display plugins in admin overview
sh4nks Sep 28, 2017
d120c9a
Add i18n support for plugins
sh4nks Oct 7, 2017
6a1ebc2
Set correct alembic version location for plugins
sh4nks Oct 7, 2017
63dc9fc
Make it optional to remove plugins from db
sh4nks Oct 7, 2017
fabe6c8
Do not remove dead plugins by default
sh4nks Oct 11, 2017
bb28295
'removed' is never initialized if config is set to False
sh4nks Oct 12, 2017
bc8bfe1
Display plugin info for disabled plugins
sh4nks Oct 12, 2017
2bb6b4b
Allow hooks to be called in templates
sh4nks Oct 19, 2017
605b077
Working on some docs
sh4nks Oct 27, 2017
030efb9
Cleanup
sh4nks Oct 27, 2017
4454d09
Remove fixed navbar from docs
sh4nks Oct 31, 2017
ee64c2a
Use singular for enum
sh4nks Oct 31, 2017
75ee7f8
Add missing hooks
sh4nks Oct 31, 2017
987bfc4
Add proper docs about plugins and hooks
sh4nks Oct 31, 2017
69b035b
Include hooks and settings in toc
sh4nks Oct 31, 2017
6a7ccdb
Update plugin documentation
sh4nks Nov 4, 2017
104976d
Add metadata for disabled plugins
sh4nks Nov 5, 2017
f06d02b
Add option to overwrite plugin settings
sh4nks Nov 5, 2017
cc2fb24
Add management commands for plugins
sh4nks Nov 5, 2017
8b234d4
Check if plugin is enabled before trying it to install
sh4nks Nov 11, 2017
b821094
Try METADATA for bdist and PKG-INFO for sdist
sh4nks Nov 11, 2017
d51e23b
Add portal plugin to requirements
sh4nks Nov 11, 2017
8d6155a
Merge branch 'master' into Pluggy
justanr Nov 18, 2017
1d7e983
Redate plugin migration so its less confusing
justanr Nov 18, 2017
311f8bf
Clarify plugin docs
justanr Nov 18, 2017
9d1dca3
Use SettingValueType enum in test fixture
justanr Nov 18, 2017
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitignore
Expand Up @@ -58,9 +58,6 @@ bower_components
node_modules
.directory

# Ignore plugins
flaskbb/plugins/*

# Created by https://www.gitignore.io/api/vim

### Vim ###
Expand Down
63 changes: 55 additions & 8 deletions flaskbb/app.py
Expand Up @@ -15,6 +15,7 @@

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

Expand All @@ -28,9 +29,10 @@
# 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,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit of noise here from when the pluggy instance was created here instead of on the application.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but it's sorted alphabetically now :)

db, debugtoolbar, limiter, login_manager, mail,
redis_store, themes, whooshee)

# various helpers
from flaskbb.utils.helpers import (time_utcnow, format_date, time_since,
crop_title, is_online, mark_online,
Expand All @@ -50,6 +52,10 @@
# app specific configurations
from flaskbb.utils.settings import flaskbb_config

from flaskbb.plugins.models import PluginRegistry
from flaskbb.plugins.manager import FlaskBBPluginManager
from flaskbb.plugins import spec


def create_app(config=None):
"""Creates the app.
Expand All @@ -61,16 +67,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

Expand All @@ -96,6 +103,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 = FlaskBBPluginManager('flaskbb', implprefix='flaskbb_')


def configure_celery_app(app, celery):
Expand Down Expand Up @@ -125,16 +133,15 @@ 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."""

# Flask-WTF CSRF
csrf.init_app(app)

# Flask-Plugins
plugin_manager.init_app(app)

# Flask-SQLAlchemy
db.init_app(app)

Expand Down Expand Up @@ -239,6 +246,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."""
Expand Down Expand Up @@ -273,6 +282,8 @@ def mark_current_user_online():
else:
mark_online(request.remote_addr, guest=True)

app.pluggy.hook.flaskbb_request_processors(app=app)


def configure_errorhandlers(app):
"""Configures the error handlers."""
Expand All @@ -289,6 +300,8 @@ def page_not_found(error):
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."""
Expand Down Expand Up @@ -349,3 +362,37 @@ def after_cursor_execute(conn, cursor, statement,
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:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is thrown if the table isn't created yet, so upgrades won't bomb. It can potentially be removed later, but I'd need to test a fresh install without it. My gut says the install would fail, but it's been wrong before.

return

for plugin in plugins:
if not plugin.enabled:
app.pluggy.set_blocked(plugin.name)

app.pluggy.load_setuptools_entrypoints('flaskbb_plugins')
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()

app.logger.debug(
"Plugins Found: {}".format(app.pluggy.list_name_plugin())
)
app.logger.debug(
"Disabled Plugins: {}".format(app.pluggy.list_disabled_plugins())
)
32 changes: 30 additions & 2 deletions flaskbb/cli/main.py
Expand Up @@ -13,6 +13,7 @@
import time
import requests
import binascii
import traceback
from datetime import datetime

import click
Expand All @@ -37,8 +38,35 @@
from flaskbb.utils.translations import compile_translations



class FlaskBBGroup(FlaskGroup):
def __init__(self, *args, **kwargs):
super(FlaskBBGroup, self).__init__(*args, **kwargs)
self._loaded_flaskbb_plugins = False

def _load_flaskbb_plugins(self, ctx):
if self._loaded_flaskbb_plugins:
return

try:
app = ctx.ensure_object(ScriptInfo).load_app()
app.pluggy.hook.flaskbb_cli(cli=self, app=app)
self._loaded_flaskbb_plugins = True
except Exception as exc:
click.echo(click.style("Error while loading CLI Plugins", fg='red'))
click.echo(click.style(traceback.format_exc(), fg='red'))

def get_command(self, ctx, name):
self._load_flaskbb_plugins(ctx)
return super(FlaskBBGroup, self).get_command(ctx, name)

def list_commands(self, ctx):
self._load_flaskbb_plugins(ctx)
return super(FlaskBBGroup, self).list_commands(ctx)


def make_app(script_info):
config_file = getattr(script_info, "config_file")
config_file = getattr(script_info, "config_file", None)
if config_file is not None:
# check if config file exists
if os.path.exists(os.path.abspath(config_file)):
Expand Down Expand Up @@ -83,7 +111,7 @@ def set_config(ctx, param, value):
ctx.ensure_object(ScriptInfo).config_file = value


@click.group(cls=FlaskGroup, create_app=make_app, add_version_option=False)
@click.group(cls=FlaskBBGroup, create_app=make_app, add_version_option=False)
@click.option("--config", expose_value=False, callback=set_config,
required=False, is_flag=False, is_eager=True, metavar="CONFIG",
help="Specify the config to use in dotted module notation "
Expand Down
106 changes: 13 additions & 93 deletions flaskbb/cli/plugins.py
Expand Up @@ -8,23 +8,11 @@
:copyright: (c) 2016 by the FlaskBB Team.
:license: BSD, see LICENSE for more details.
"""
import sys
import os
import shutil

import pluggy
import click
from flask import current_app
from flask_plugins import (get_all_plugins, get_enabled_plugins,
get_plugin_from_all)

from flaskbb.cli.main import flaskbb
from flaskbb.cli.utils import check_cookiecutter, validate_plugin
from flaskbb.extensions import plugin_manager

try:
from cookiecutter.main import cookiecutter
except ImportError:
pass


@flaskbb.group()
Expand All @@ -33,98 +21,30 @@ def plugins():
pass


@plugins.command("new")
@click.argument("plugin_identifier", callback=check_cookiecutter)
@click.option("--template", "-t", type=click.STRING,
default="https://github.com/sh4nks/cookiecutter-flaskbb-plugin",
help="Path to a cookiecutter template or to a valid git repo.")
def new_plugin(plugin_identifier, template):
"""Creates a new plugin based on the cookiecutter plugin
template. Defaults to this template:
https://github.com/sh4nks/cookiecutter-flaskbb-plugin.
It will either accept a valid path on the filesystem
or a URL to a Git repository which contains the cookiecutter template.
"""
out_dir = os.path.join(current_app.root_path, "plugins")
click.secho("[+] Creating new plugin {}".format(plugin_identifier),
fg="cyan")
cookiecutter(template, output_dir=out_dir)
click.secho("[+] Done. Created in {}".format(out_dir),
fg="green", bold=True)


@plugins.command("install")
@click.argument("plugin_identifier")
def install_plugin(plugin_identifier):
"""Installs a new plugin."""
validate_plugin(plugin_identifier)
plugin = get_plugin_from_all(plugin_identifier)
click.secho("[+] Installing plugin {}...".format(plugin.name), fg="cyan")
try:
plugin_manager.install_plugins([plugin])
except Exception as e:
click.secho("[-] Couldn't install plugin because of following "
"exception: \n{}".format(e), fg="red")


@plugins.command("uninstall")
@click.argument("plugin_identifier")
def uninstall_plugin(plugin_identifier):
"""Uninstalls a plugin from FlaskBB."""
validate_plugin(plugin_identifier)
plugin = get_plugin_from_all(plugin_identifier)
click.secho("[+] Uninstalling plugin {}...".format(plugin.name), fg="cyan")
try:
plugin_manager.uninstall_plugins([plugin])
except AttributeError:
pass


@plugins.command("remove")
@click.argument("plugin_identifier")
@click.option("--force", "-f", default=False, is_flag=True,
help="Removes the plugin without asking for confirmation.")
def remove_plugin(plugin_identifier, force):
"""Removes a plugin from the filesystem."""
validate_plugin(plugin_identifier)
if not force and not \
click.confirm(click.style("Are you sure?", fg="magenta")):
sys.exit(0)

plugin = get_plugin_from_all(plugin_identifier)
click.secho("[+] Uninstalling plugin {}...".format(plugin.name), fg="cyan")
try:
plugin_manager.uninstall_plugins([plugin])
except Exception as e:
click.secho("[-] Couldn't uninstall plugin because of following "
"exception: \n{}".format(e), fg="red")
if not click.confirm(click.style(
"Do you want to continue anyway?", fg="magenta")
):
sys.exit(0)

click.secho("[+] Removing plugin from filesystem...", fg="cyan")
shutil.rmtree(plugin.path, ignore_errors=False, onerror=None)


@plugins.command("list")
def list_plugins():
"""Lists all installed plugins."""
click.secho("[+] Listing all installed plugins...", fg="cyan")

# This is subject to change as I am not happy with the current
# plugin system
enabled_plugins = get_enabled_plugins()
disabled_plugins = set(get_all_plugins()) - set(enabled_plugins)
enabled_plugins = current_app.pluggy.list_plugin_distinfo()
if len(enabled_plugins) > 0:
click.secho("[+] Enabled Plugins:", fg="blue", bold=True)
for plugin in enabled_plugins:
# plugin[0] is the module
plugin = plugin[1]
click.secho(" - {} (version {})".format(
plugin.name, plugin.version), bold=True
plugin.key, plugin.version), bold=True
)

# TODO: is there a better way for doing this?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, wrap this using with_appcontext and load the information off of current_app.pluggy, with a fix I pushed this'll end up loading plugins twice which might cause issues.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, that shouldn't be needed anymore as we keep track of disabled plugins :) pluggy.list_disabled_plugins() should provide all information needed for this.

pm = pluggy.PluginManager('flaskbb', implprefix='flaskbb_')
pm.load_setuptools_entrypoints('flaskbb_plugins')
all_plugins = pm.list_plugin_distinfo()
disabled_plugins = set(all_plugins) - set(enabled_plugins)
if len(disabled_plugins) > 0:
click.secho("[+] Disabled Plugins:", fg="yellow", bold=True)
for plugin in disabled_plugins:
plugin = plugin[1]
click.secho(" - {} (version {})".format(
plugin.name, plugin.version), bold=True
plugin.key, plugin.version), bold=True
)
5 changes: 2 additions & 3 deletions flaskbb/cli/utils.py
Expand Up @@ -15,11 +15,10 @@

import click

from flask import __version__ as flask_version
from flask import current_app, __version__ as flask_version
from flask_themes2 import get_theme

from flaskbb import __version__
from flaskbb.extensions import plugin_manager
from flaskbb.utils.populate import create_user, update_user


Expand Down Expand Up @@ -75,7 +74,7 @@ def validate_plugin(plugin):
the appcontext can't be found and using with_appcontext doesn't
help either.
"""
if plugin not in plugin_manager.all_plugins.keys():
if plugin not in current_app.pluggy.get_plugins():
raise FlaskBBCLIError("Plugin {} not found.".format(plugin), fg="red")
return True

Expand Down
4 changes: 0 additions & 4 deletions flaskbb/extensions.py
Expand Up @@ -19,7 +19,6 @@
from flask_redis import FlaskRedis
from flask_alembic import Alembic
from flask_themes2 import Themes
from flask_plugins import PluginManager
from flask_babelplus import Babel
from flask_wtf.csrf import CSRFProtect
from flask_limiter import Limiter
Expand Down Expand Up @@ -57,9 +56,6 @@
# Themes
themes = Themes()

# PluginManager
plugin_manager = PluginManager()

# Babel
babel = Babel()

Expand Down