Plugin Development¶
browsepy is extensible via a powerful plugin API. A plugin can register its own Flask blueprints, file browser widgets (filtering by file), mimetype detection functions and even its own command line arguments.
A fully functional browsepy.plugin.player
plugin module is provided as
example.
Plugin Namespace¶
Plugins are regular python modules. They are loaded by –plugin console argument.
Aiming to make plugin names shorter, browsepy try to load plugins
using namespaces
and prefixes defined on configuration’s plugin_namespaces
entry on
browsepy.app.config
. Its default value is browsepy’s built-in module namespace
browsepy.plugins, browsepy_ prefix and an empty namespace (so any plugin could
be used with its full module name).
Summarizing, with default configuration:
- Any python module inside browsepy.plugin can be loaded as plugin by its
relative module name, ie.
player
instead ofbrowsepy.plugin.player
. - Any python module prefixed by
browsepy_
can be loaded as plugin by its unprefixed name, ie.myplugin
instead ofbrowsepy_myplugin
. - Any python module can be loaded as plugin by its full module name.
Said that, you can name your own plugin so it could be loaded easily.
Examples¶
Your built-in plugin, placed under browsepy/plugins/ in your own browsepy fork:
browsepy --plugin=my_builtin_module
Your prefixed plugin, a regular python module in python’s library path, named browsepy_prefixed_plugin:
browsepy --plugin=prefixed_plugin
Your plugin, a regular python module in python’s library path, named my_plugin.
browsepy --plugin=my_plugin
Protocol¶
The plugin manager subsystem expects a register_plugin callable at module
level (in your __init__ module globals) which will be called with the
manager itself (type PluginManager
) as first parameter.
Plugin manager exposes several methods to register widgets and mimetype detection functions.
A *sregister_plugin*s function looks like this (taken from player plugin):
def register_plugin(manager):
'''
Register blueprints and actions using given plugin manager.
:param manager: plugin manager
:type manager: browsepy.manager.PluginManager
'''
manager.register_blueprint(player)
manager.register_mimetype_function(detect_playable_mimetype)
# add style tag
manager.register_widget(
place='styles',
type='stylesheet',
endpoint='player.static',
filename='css/browse.css'
)
# register link actions
manager.register_widget(
place='entry-link',
type='link',
endpoint='player.audio',
filter=PlayableFile.detect
)
manager.register_widget(
place='entry-link',
icon='playlist',
type='link',
endpoint='player.playlist',
filter=PlayListFile.detect
)
# register action buttons
manager.register_widget(
place='entry-actions',
css='play',
type='button',
endpoint='player.audio',
filter=PlayableFile.detect
)
manager.register_widget(
place='entry-actions',
css='play',
type='button',
endpoint='player.playlist',
filter=PlayListFile.detect
)
# check argument (see `register_arguments`) before registering
if manager.get_argument('player_directory_play'):
# register header button
manager.register_widget(
place='header',
type='button',
endpoint='player.directory',
text='Play directory',
filter=PlayableDirectory.detect
)
In case you need to add extra command-line-arguments to browsepy command,
register_arguments()
method can be also declared (like register_plugin,
at your plugin’s module level). It will receive a
ArgumentPluginManager
instance, providing an argument-related subset of whole plugin manager’s functionality.
A simple register_arguments example (from player plugin):
def register_arguments(manager):
'''
Register arguments using given plugin manager.
This method is called before `register_plugin`.
:param manager: plugin manager
:type manager: browsepy.manager.PluginManager
'''
# Arguments are forwarded to argparse:ArgumentParser.add_argument,
# https://docs.python.org/3.7/library/argparse.html#the-add-argument-method
manager.register_argument(
'--player-directory-play', action='store_true',
help='enable directories as playlist'
)
Widgets¶
Widget registration is provided by PluginManager.register_widget()
.
You can alternatively pass a widget object, via widget keyword argument, or use a pure functional approach by passing place, type and the widget-specific properties as keyword arguments.
In addition to that, you can also define in which cases widget will be shown passing
a callable to filter argument keyword, which will receive a
browsepy.file.Node
(commonly a browsepy.file.File
or a
browsepy.file.Directory
) instance.
For those wanting the object-oriented approach, and for reference for those
wanting to know widget properties for using the functional way,
WidgetPluginManager.widget_types
dictionary is
available, containing widget namedtuples (see collections.namedtuple()
) definitions.
Here is the “widget_types” for reference.
class WidgetPluginManager(RegistrablePluginManager):
''' ... '''
widget_types = {
'base': defaultsnamedtuple(
'Widget',
('place', 'type')),
'link': defaultsnamedtuple(
'Link',
('place', 'type', 'css', 'icon', 'text', 'endpoint', 'href'),
{
'text': lambda f: f.name,
'icon': lambda f: f.category
}),
'button': defaultsnamedtuple(
'Button',
('place', 'type', 'css', 'text', 'endpoint', 'href')),
'upload': defaultsnamedtuple(
'Upload',
('place', 'type', 'css', 'text', 'endpoint', 'action')),
'stylesheet': defaultsnamedtuple(
'Stylesheet',
('place', 'type', 'endpoint', 'filename', 'href')),
'script': defaultsnamedtuple(
'Script',
('place', 'type', 'endpoint', 'filename', 'src')),
'html': defaultsnamedtuple(
'Html',
('place', 'type', 'html')),
}
Function browsepy.file.defaultsnamedtuple()
is a
collections.namedtuple()
which uses a third argument dictionary
as default attribute values. None is assumed as implicit default.
All attribute values can be either str
or a callable accepting
a browsepy.file.Node
instance as argument and returning said str
,
allowing dynamic widget content and behavior.
Please note place and type are always required (otherwise widget won’t be drawn), and this properties are mutually exclusive:
- link: attribute href supersedes endpoint.
- button: attribute href supersedes endpoint.
- upload: attribute action supersedes endpoint.
- stylesheet: attribute href supersedes endpoint and filename
- script: attribute src supersedes endpoint and filename.
Endpoints are Flask endpoint names, and endpoint handler functions must receive
a “filename” parameter for stylesheet and script widgets (allowing it to point
using with Flask’s statics view) and a “path” argument for other use-cases. In the
former case it is recommended to use
browsepy.file.Node.from_urlpath()
static method to create the
appropriate file/directory object (see browsepy.file
).
Considerations¶
Name your plugin wisely, look at pypi for conflicting module names.
Always choose the less intrusive approach on plugin development, so new
browsepy versions will not likely break it. That’s why stuff like
PluginManager.register_blueprint()
is provided and its usage is
preferred over directly registering blueprints via plugin manager’s app
reference (or even module-level app reference).
A good way to keep your plugin working on future browsepy releases is upstreaming it onto browsepy itself.
Feel free to hack everything you want. Pull requests are definitely welcome.