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

Pluggy based Plugins #311

merged 57 commits into from Nov 18, 2017

Conversation

justanr
Copy link
Collaborator

@justanr justanr commented Aug 13, 2017

Addresses #183

WIP PR for adding pluggy based plugins to FlaskBB.

Currently this includes:

  • Defining some specs for plugins to implement (by no means exhaustive)
  • Creates manager and loads plugins when application is created -- after extensions
  • Creates a DB interface for toggling plugins enabled
  • Creates KV store in the database for plugins to store settings in so they don't need to populate the settings table -- these appear as a dictionary like interface on the plugin registry instance for the plugin (that's all the collection class and association proxy stuff in the PluginStore/PluginRegistry relationship)

I'm having some issues with loading CLI commands from plugins because the Flask-Click bridge only creates an application when absolutely needed, however plugins are loaded on application creation, so even with overriding the _load_plugin_commands hook in the FlaskGroup doesn't work as the FlaskBB instance hasn't been created yet.

Next steps here are:

  • Documentation for writing pluggy based plugins
  • Converting Portal to pluggy style
  • Add hooks for rendering HTML fragments into templates at certain points
  • Add hooks for pre/post handling on posting, topic, message, etc.
  • Figure out how to load CLI commands.

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 :)

flaskbb/app.py Outdated
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.

@sh4nks
Copy link
Member

sh4nks commented Sep 3, 2017

Looks good so far!

A few more things that need to be addressed (I'll update this list when I find something):

  • Make translations work with pluggy
  • Make migrations work with pluggy
  • Convert AdminCP to use 'pluggy' instead of 'Flask-Plugins'
    • Display Plugins in Overview
    • Enable/Disable Plugins in AdminCP

@justanr justanr added this to the 2.0 milestone Sep 4, 2017
@sh4nks
Copy link
Member

sh4nks commented Sep 4, 2017

I am using the metadata from setup.py to display some info about the plugin in the admin cp. I removed a few cli commands for now (install/enable/etc) except flaskbb plugins list which will display all enabled and disabled plugins using the package name - later I'll probably make the switch to just display the plugin name.

Un/installing the fixtures and enabling/disabling plugins should also work via the admin cp although it's not very user friendly at the moment. changing settings still needs to be done.

id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.Unicode(255), nullable=False)
value = db.Column(db.PickleType, nullable=False)
# Enum?
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sounds good, do we go the Enum34 route or just an older SQLA enum (think it's either a list of strings or a space separated string)

Copy link
Member

Choose a reason for hiding this comment

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

I'd go for Enum34 and change the data type to ChoiceType as we have sqlalchemy-utils already as dependency.

@@ -619,9 +621,20 @@ def __call__(self, environ, start_response):


def real(obj):
"""
Unwraps a werkzeug.local.LocalProxy object if given one, else returns the object
"""Unwraps a werkzeug.local.LocalProxy object if given one,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What's our line length? My yapf file is set to 100, but that's only because I copied it from work. Should we stick to 79/80?

Copy link
Member

Choose a reason for hiding this comment

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

I have always tried to stick to 79/80 but in times with widescreen monitors I think that 100 would make sense :)

@@ -77,7 +77,6 @@ def run_tests(self):
'Flask-Limiter',
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If portal is becoming an external package, should we install it by default?

Copy link
Member

Choose a reason for hiding this comment

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

I think we should install it by default. I have to create a proper package for PyPI first though..

@justanr
Copy link
Collaborator Author

justanr commented Sep 5, 2017

with ctx.ensure_object(ScriptInfo).load_app().app_context():

That might be the secret sauce to getting plugin cli commands loaded. However, we'll end up loading the application twice in many instances.

Actually wrapping the load plugin commands method with with_appcontext looks like it's working.

@sh4nks
Copy link
Member

sh4nks commented Sep 5, 2017

Looks like this is the solution. I stumbled upon this the other day and it seems that they are doing something similar.

@justanr
Copy link
Collaborator Author

justanr commented Sep 5, 2017

I poked around Indico at the behest of ThiefMaster when I was shopping around for plugin solutions, but yes. That looks like the ticket.

Copy link
Collaborator Author

@justanr justanr left a comment

Choose a reason for hiding this comment

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

Some more thoughts.

)

# 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.


plugin.delete()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm wary of this because it only removes the plugin from the blocked list. The next time the application runs, it'll reappear much to the frustration of the user. We should probably include instructions about uninstalling via pip on this page. We should probably also add an uninstall hook for plugins to implement so they can do any decoupling needed.

for ep in iter_entry_points(entrypoint_name):
# is the plugin registered or blocked?
if self.get_plugin(ep.name) or self.is_blocked(ep.name):
self._disabled_plugins.append((ep.name, ep.dist))
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should only append here if the plugin is actually disabled, not merely already loaded.

Copy link
Member

Choose a reason for hiding this comment

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

Oops, yeah you are right. Thanks for pointing it out :)

value = db.Column(db.PickleType, nullable=False)
# Enum?
# Available types: string, integer, float, boolean, select, selectmultiple
value_type = db.Column(db.Unicode(20), nullable=False)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just noticed these extra nullable fields break the association_proxy on PluginRegistry. Should we keep that or remove it? At least it breaks for setting.

Copy link
Member

Choose a reason for hiding this comment

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

mhh thats true.. what do you suggest? We could also revamp the way we create the setting forms but thats probably a different issue :).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Mm, not sure. I like the idea of a simple KV store but if we need more information that what we can get out of that, it's not worth trying to make it work. I'm tempted to say we could infer the value's type, but that could lead to issues.

@@ -38,7 +35,7 @@ def __init__(self, app):
with self.app.app_context():
self.plugin_translations = [
os.path.join(plugin.path, "translations")
for plugin in get_enabled_plugins()
for plugin in self.app.pluggy.get_plugins()
Copy link
Collaborator Author

@justanr justanr Sep 7, 2017

Choose a reason for hiding this comment

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

What if we did self.plugin_translations = self.app.pluggy.load_translations() and have the expectation be the plugin returns a list of translation paths (or maybe a dictionary of language -> path/List[path]). That's all we'd need to complete the translation compilation, right?

Copy link
Member

Choose a reason for hiding this comment

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

Good point! I just blindly changed it to make flaskbb runable again.. I really should stop doing that :P

@djsilcock
Copy link
Contributor

I like the idea - I'm not entirely familiar with the inner workings of click but I think all the subcommands would need the same signature ie can't mix
flaskbb plugins <pluginname> <subcommand>
with
flaskbb plugins <subcommand> <pluginname>

This would leave you with something like

flaskbb plugins db myplugin upgrade

which would at least appear under flaskbb plugins --help

On the other hand, it might be an idea not to bombard the end user with all these options, after all most of the db commands are involved in generating migration files rather than applying them - other than db upgrade and db downgrade which might be served better by incorporating them into some kind of plugin install command

@@ -8,7 +8,7 @@ html {
body {
/* Margin bottom by footer height */
margin-bottom: 60px;
padding-top: 50px;
/*padding-top: 50px; */ /* uncomment if navbar should be fixed */
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Fixed as in "position: fixed"?

Copy link
Member

Choose a reason for hiding this comment

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

if navbar-fixed-top is used

docs/hooks.rst Outdated

.. currentmodule:: flaskbb.plugins.spec

.. autofunction:: flaskbb_extensions
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should probably give more details about when these are called. I'll knock out some docs on it.

Copy link
Member

Choose a reason for hiding this comment

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

Ok, I'll wait for the merge then.

Each hook specification has a corresponding hook implementation. By default,
all hooks that are prefix with ``flaskbb_`` will be marked as a standard
hook implementation. It is possible to modify the behavior of hooks.
For example, default hooks are called in LIFO registered order.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We should clarify that registration order isn't deterministic -- unless I misunderstood something in the pluggy docs or there's something in setuptools that guarantees that.

@justanr
Copy link
Collaborator Author

justanr commented Nov 18, 2017

Wooo, all of this looks good. I'll knock out the changes to the docs above and handle this conflict we have.

@sh4nks sh4nks merged commit ff6798e into flaskbb:master Nov 18, 2017
@sh4nks sh4nks mentioned this pull request Nov 18, 2017
@justanr justanr deleted the Pluggy branch November 19, 2017 00:22
@sh4nks sh4nks added this to Done in 2.0 Getting Hooked Up Mar 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

None yet

3 participants