Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions construct/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
from __future__ import absolute_import

# Local imports
from . import api
from .constants import DEFAULT_API_NAME
from . import api, constants, utils
from .api import *
from .errors import *


# Package metadata
Expand Down
138 changes: 138 additions & 0 deletions construct/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-

from __future__ import absolute_import

# Standard library imports
import logging
import sys
from collections import OrderedDict

# Local imports
from .compat import reraise
from .errors import ActionError


__all__ = [
'Action',
'ActionManager',
'is_action_type',
'is_action',
]


_log = logging.getLogger(__name__)


class Action(object):
'''An action is an executable tool that can be presented to users in
Constructs UI.
'''

description = ''
icon = ''
identifier = ''
label = ''

def __init__(self, api):
self.api = api

def __call__(self, ctx=None):
try:
self.run(self.api, ctx or self.api.context.copy())
except:
exc_typ, exc_value, exc_tb = sys.exc_info()
reraise(ActionError, ActionError(exc_value), exc_tb)

def run(self, api, ctx):
'''Subclasses should implement run to perform the Action's work.'''
return NotImplemented

def is_available(self, ctx):
'''Return True if the Action is available in the given context'''
return True


ACTION_TYPES = (Action,)


def is_action_type(obj):
'''Check if an obj is an Action type.'''

return (
obj not in ACTION_TYPES and
isinstance(obj, type) and
issubclass(obj, ACTION_TYPES)
)


def is_action(obj):
'''Check if an obj is an Action instance.'''

return isinstance(obj, ACTION_TYPES)


class ActionManager(OrderedDict):

def __init__(self, api):
self.api = api

def load(self):
pass

def unload(self):
'''Unload all Actions'''
self.clear()

def register(self, action):
'''Register an Action'''

if self.loaded(action):
_log.error('Action already loaded: %s' % action)
return

_log.debug('Loading action: %s' % action)
if is_action_type(action):
inst = action(self.api)
elif is_action(action):
inst = action
else:
_log.error('Expected Action type got %s' % action)
return

self[action.identifier] = inst

def unregister(self, action):
'''Unregister an Action'''

identifier = getattr(action, 'identifier', action)
self.pop(identifier, None)
_log.debug('Unloading action: %s' % action)

def loaded(self, action):
'''Check if an Action has been loaded.'''

identifier = getattr(action, 'identifier', action)
return identifier in self

def ls(self, typ=None):
'''Get a list of available actions.

Arguments:
typ (Action, Optional): List only a specific type of Action.

Examples:
ls() # lists all actions
ls(ActionWrapper) # list only ActionWrapper
'''

matches = []
for action in self.values():
if not type or isinstance(action, typ):
matches.append(action)
return matches

def get_available(self, ctx=None, typ=None):
'''Get actions available in the provided contaction.'''

ctx = ctx or self.api.context.copy()
return [action for action in self.values() if action.is_available(ctx)]
12 changes: 10 additions & 2 deletions construct/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,26 @@

# Local imports
from . import schemas
from .actions import Action, ActionManager
from .compat import Mapping, basestring
from .constants import DEFAULT_LOGGING
from .context import Context
from .events import EventManager
from .extensions import ExtensionManager
from .extensions import Extension, ExtensionManager, Host
from .io import IO
from .path import Path
from .settings import Settings
from .ui.manager import UIManager
from .utils import ensure_exists, unipath, yaml_dump


__all__ = ['API']
__all__ = [
'schemas',
'Action',
'Extension',
'Host',
'Context',
]

_log = logging.getLogger(__name__)
_cache = {}
Expand Down Expand Up @@ -82,6 +89,7 @@ def __init__(self, name=None, **kwargs):
self.path = Path(kwargs.pop('path', None))
self.settings = Settings(self.path)
self.extensions = ExtensionManager(self)
self.actions = ActionManager(self)
self.context = Context()
self.schemas = schemas
self.io = IO(self)
Expand Down
4 changes: 4 additions & 0 deletions construct/errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-


class ActionError(Exception):
'''Raised when an Action fails to run.'''


class InvalidSettings(Exception):
'''Raise when your settings are invalid.'''

Expand Down
10 changes: 6 additions & 4 deletions construct/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
# Standard library imports
import inspect
import logging
from collections import OrderedDict

# Third party imports
# Third library imports
import entrypoints

# Local imports
Expand Down Expand Up @@ -200,9 +200,10 @@ def is_extension(obj):
return isinstance(obj, EXTENSION_TYPES)


class ExtensionManager(dict):
class ExtensionManager(OrderedDict):

def __init__(self, api):
super(ExtensionManager, self).__init__()
self.api = api
self.path = self.api.path
self.settings = self.api.settings
Expand All @@ -226,7 +227,8 @@ def register(self, ext):
'''Register an Extension'''

if self.loaded(ext):
_log.debug('Extension already loaded: %s' % ext)
_log.error('Extension already loaded: %s' % ext)
return

_log.debug('Loading extension: %s' % ext)
if is_extension_type(ext):
Expand Down Expand Up @@ -265,7 +267,7 @@ def unregister(self, ext):
def loaded(self, ext):
'''Check if an Extension has been loaded.'''

identifier = getattr(ext, 'identifer', ext)
identifier = getattr(ext, 'identifier', ext)
return identifier in self

def discover(self):
Expand Down
4 changes: 3 additions & 1 deletion construct/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
# Local imports
from ..errors import ValidationError
from .fsfs import FsfsLayer
from .mongo import MongoLayer


# from .mongo import MongoLayer


class IO(object):
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ universal=1
[isort]
from_first = false
import_heading_stdlib = Standard library imports
import_heading_firstparty = Local imports
import_heading_firstparty = Construct imports
import_heading_localfolder = Local imports
import_heading_thirdparty = Third party imports
indent = ' '
Expand Down
74 changes: 74 additions & 0 deletions tests/test_actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-

from __future__ import absolute_import

# Third party imports
from nose.tools import assert_raises

# Local imports
import construct

# Local imports
from . import setup_api, teardown_api


def setup_module():
setup_api(__name__)


def teardown_module():
teardown_api(__name__)


class SimpleAction(construct.Action):

identifier = 'simple_action'
label = 'Simple Action'
icon = 'icons/simple_action.png'
description = 'Performs a simple action.'

def run(self, api, ctx):
ctx['count'] += 1


class BadAction(construct.Action):

identifier = 'bad_action'
label = 'Bad Action'
icon = 'icons/bad_action.png'
description = 'An action that raises an exception.'

def run(self, api, ctx):
raise Exception('A bad thing happened....')


def test_simple_action():
'''Register and call a simple action.'''

api = construct.API(__name__)
api.register_action(SimpleAction)

simple_action = api.actions.get('simple_action')
assert isinstance(simple_action, construct.Action)
assert simple_action.api is api
assert api.actions.loaded(SimpleAction)

ctx = {'count': 0}
simple_action(ctx)
assert ctx['count'] == 1

api.unregister_action(SimpleAction)
assert not api.actions.loaded(SimpleAction)


def test_bad_action():
'''Call an action that raises an exception.'''

api = construct.API(__name__)
api.register_action(BadAction)
bad_action = api.actions.get('bad_action')

with assert_raises(construct.ActionError):
bad_action()

api.unregister_action(BadAction)
2 changes: 1 addition & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import shutil
import sys

# Local imports
# Construct imports
import construct
from construct.settings import restore_default_settings

Expand Down
3 changes: 1 addition & 2 deletions tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
import os

# Third party imports
# Third library imports
from bson.objectid import ObjectId

# Local imports
# Construct imports
from construct.compat import basestring
from construct.context import Context

Expand Down
Loading