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
Pluggy based Plugins #311
Changes from 10 commits
cfc7d9a
0511196
113fbbc
a3f4e1b
cae9525
d738e70
37bb1fb
a0e99ca
734f062
9b6ccb7
63593d3
f503559
c66dcc1
1e680d6
5c3f136
458d1b4
f883d51
a0bd7a9
5430948
567aff0
7fb6bab
1f59916
e15146a
ecdd1b6
0450656
bc9d73e
b8be1f0
37b5dfe
31b962e
2ea0f92
c130bdb
44eab45
d120c9a
6a1ebc2
63dc9fc
fabe6c8
bb28295
bc8bfe1
2bb6b4b
605b077
030efb9
4454d09
ee64c2a
75ee7f8
987bfc4
69b035b
6a7ccdb
104976d
f06d02b
cc2fb24
8b234d4
b821094
d51e23b
8d6155a
1d7e983
311f8bf
9d1dca3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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, | ||
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, | ||
|
@@ -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. | ||
|
@@ -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 | ||
|
||
|
@@ -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): | ||
|
@@ -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) | ||
|
||
|
@@ -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.""" | ||
|
@@ -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.""" | ||
|
@@ -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.""" | ||
|
@@ -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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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() | ||
|
@@ -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? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, wrap this using There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 :) |
||
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 | ||
) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)