Skip to content

Commit

Permalink
Merge pull request #133 from nucleic/feature-plugins
Browse files Browse the repository at this point in the history
Feature plugins
  • Loading branch information
sccolbert committed Feb 11, 2014
2 parents 5a9f529 + a226c27 commit 2ab09c6
Show file tree
Hide file tree
Showing 35 changed files with 3,343 additions and 0 deletions.
7 changes: 7 additions & 0 deletions enaml/workbench/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
12 changes: 12 additions & 0 deletions enaml/workbench/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from .extension import Extension
from .extension_point import ExtensionPoint
from .plugin import Plugin
from .plugin_manifest import PluginManifest
from .workbench import Workbench
7 changes: 7 additions & 0 deletions enaml/workbench/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
8 changes: 8 additions & 0 deletions enaml/workbench/core/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from .command import Command
25 changes: 25 additions & 0 deletions enaml/workbench/core/command.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from atom.api import Callable, Unicode

from enaml.core.declarative import Declarative, d_


class Command(Declarative):
""" A declarative class for defining a workbench command.
"""
#: The globally unique identifier for the command.
id = d_(Unicode())

#: An optional description of the command.
description = d_(Unicode())

#: A required callable which handles the command. It must accept a
#: single argument, which is an instance of ExecutionEvent.
handler = d_(Callable())
36 changes: 36 additions & 0 deletions enaml/workbench/core/core_manifest.enaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from enaml.workbench.extension_point import ExtensionPoint
from enaml.workbench.plugin_manifest import PluginManifest


def core_plugin_factory():
""" A factory function which creates a CorePlugin instance.

"""
from .core_plugin import CorePlugin
return CorePlugin()


COMMANDS_DESCRIPTION = \
""" Extensions to this point may contribute `Command` objects which can
be invoked via the `invoke_command` method of the CorePlugin instance.
Commands can be provided by declaring them as children of the Extension
and/or by declaring a factory function which takes the workbench as an
argument and returns a list of Command instances. """


enamldef CoreManifest(PluginManifest):
""" The manifest for the Enaml workbench core plugin.

"""
id = 'enaml.workbench.core'
factory = core_plugin_factory
ExtensionPoint:
id = 'commands'
description = COMMANDS_DESCRIPTION
164 changes: 164 additions & 0 deletions enaml/workbench/core/core_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from collections import defaultdict

from atom.api import Typed

from enaml.workbench.plugin import Plugin

from .command import Command
from .execution_event import ExecutionEvent


COMMANDS_POINT = u'enaml.workbench.core.commands'


class CorePlugin(Plugin):
""" The core plugin for the Enaml workbench.
"""
def start(self):
""" Start the plugin life-cycle.
This method is called by the framework at the appropriate time.
It should never be called by user code.
"""
self._refresh_commands()
self._bind_observers()

def stop(self):
""" Stop the plugin life-cycle.
This method is called by the framework at the appropriate time.
It should never be called by user code.
"""
self._unbind_observers()
self._commands.clear()
self._command_extensions.clear()

def invoke_command(self, command_id, parameters={}, trigger=None):
""" Invoke the command handler for the given command id.
Parameters
----------
command_id : unicode
The unique identifier of the command to invoke.
parameters : dict, optional
The parameters to pass to the command handler.
trigger : object, optional
The object which triggered the command.
"""
if command_id not in self._commands:
msg = "'%s' is not a registered command id"
raise ValueError(msg % command_id)

command = self._commands[command_id]

event = ExecutionEvent()
event.command = command
event.workbench = self.workbench
event.parameters = parameters
event.trigger = trigger

command.handler(event)

#--------------------------------------------------------------------------
# Private API
#--------------------------------------------------------------------------
#: The mapping of command id to Command object.
_commands = Typed(dict, ())

#: The mapping of extension object to list of Command objects.
_command_extensions = Typed(defaultdict, (list,))

def _refresh_commands(self):
""" Refresh the command objects for the plugin.
"""
workbench = self.workbench
point = workbench.get_extension_point(COMMANDS_POINT)
extensions = point.extensions
if not extensions:
self._commands.clear()
self._command_extensions.clear()
return

new_extensions = defaultdict(list)
old_extensions = self._command_extensions
for extension in extensions:
if extension in old_extensions:
commands = old_extensions[extension]
else:
commands = self._load_commands(extension)
new_extensions[extension].extend(commands)

commands = {}
for extension in extensions:
for command in new_extensions[extension]:
if command.id in commands:
msg = "command '%s' is already registered"
raise ValueError(msg % command.id)
if command.handler is None:
msg = "command '%s' does not declare a handler"
raise ValueError(msg % command.id)
commands[command.id] = command

self._commands = commands
self._command_extensions = new_extensions

def _load_commands(self, extension):
""" Load the command objects for the given extension.
Parameters
----------
extension : Extension
The extension object of interest.
Returns
-------
result : list
The list of Command objects declared by the extension.
"""
workbench = self.workbench
commands = extension.get_children(Command)
if extension.factory is not None:
for item in extension.factory(workbench):
if not isinstance(item, Command):
msg = "extension '%s' created non-Command of type '%s'"
args = (extension.qualified_id, type(item).__name__)
raise TypeError(msg % args)
commands.append(item)
return commands

def _on_commands_updated(self, change):
""" The observer for the commands extension point.
"""
self._refresh_commands()

def _bind_observers(self):
""" Setup the observers for the plugin.
"""
workbench = self.workbench
point = workbench.get_extension_point(COMMANDS_POINT)
point.observe('extensions', self._on_commands_updated)

def _unbind_observers(self):
""" Remove the observers for the plugin.
"""
workbench = self.workbench
point = workbench.get_extension_point(COMMANDS_POINT)
point.unobserve('extensions', self._on_commands_updated)
29 changes: 29 additions & 0 deletions enaml/workbench/core/execution_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from atom.api import Atom, Dict, Typed, Value

from enaml.workbench.workbench import Workbench

from .command import Command


class ExecutionEvent(Atom):
""" The object passed to a command handler when it is invoked.
"""
#: The command which is being invoked.
command = Typed(Command)

#: The workbench instance which owns the command.
workbench = Typed(Workbench)

#: The user-supplied parameters for the command.
parameters = Dict()

#: The user-object object which triggered the command.
trigger = Value()
90 changes: 90 additions & 0 deletions enaml/workbench/extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file COPYING.txt, distributed with this software.
#------------------------------------------------------------------------------
from atom.api import Callable, Int, Unicode

from enaml.core.declarative import Declarative, d_


class Extension(Declarative):
""" A declarative class which represents a plugin extension.
An Extension must be declared as a child of a PluginManifest.
"""
#: The globally unique identifier for the extension.
id = d_(Unicode())

#: The fully qualified id of the target extension point.
point = d_(Unicode())

#: An optional rank to use for order the extension among others.
rank = d_(Int())

#: A callable which will create the implementation object for the
#: extension point. The call signature and return type are defined
#: by the extension point plugin which invokes the factory.
factory = d_(Callable())

#: An optional description of the extension.
description = d_(Unicode())

@property
def plugin_id(self):
""" Get the plugin id from the parent plugin manifest.
"""
return self.parent.id

@property
def qualified_id(self):
""" Get the fully qualified extension identifer.
"""
this_id = self.id
if u'.' in this_id:
return this_id
return u'%s.%s' % (self.plugin_id, this_id)

def get_child(self, kind, reverse=False):
""" Find a child by the given type.
Parameters
----------
kind : type
The declartive type of the child of interest.
reverse : bool, optional
Whether to search in reversed order. The default is False.
Returns
-------
result : child or None
The first child found of the requested type.
"""
it = reversed if reverse else iter
for child in it(self.children):
if isinstance(child, kind):
return child
return None

def get_children(self, kind):
""" Get all the children of the given type.
Parameters
----------
kind : type
The declartive type of the children of interest.
Returns
-------
result : list
The list of children of the request type.
"""
return [c for c in self.children if isinstance(c, kind)]
Loading

0 comments on commit 2ab09c6

Please sign in to comment.