diff --git a/construct/__init__.py b/construct/__init__.py index 3f67a25..4a5baea 100644 --- a/construct/__init__.py +++ b/construct/__init__.py @@ -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 diff --git a/construct/actions.py b/construct/actions.py new file mode 100644 index 0000000..4d3e7b3 --- /dev/null +++ b/construct/actions.py @@ -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)] diff --git a/construct/api.py b/construct/api.py index 423a08f..5635234 100644 --- a/construct/api.py +++ b/construct/api.py @@ -11,11 +11,12 @@ # 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 @@ -23,7 +24,13 @@ from .utils import ensure_exists, unipath, yaml_dump -__all__ = ['API'] +__all__ = [ + 'schemas', + 'Action', + 'Extension', + 'Host', + 'Context', +] _log = logging.getLogger(__name__) _cache = {} @@ -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) diff --git a/construct/errors.py b/construct/errors.py index 7596518..e7cab04 100644 --- a/construct/errors.py +++ b/construct/errors.py @@ -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.''' diff --git a/construct/extensions.py b/construct/extensions.py index 29604b8..c1ed94d 100644 --- a/construct/extensions.py +++ b/construct/extensions.py @@ -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 @@ -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 @@ -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): @@ -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): diff --git a/construct/io/__init__.py b/construct/io/__init__.py index ff43d21..44bca50 100644 --- a/construct/io/__init__.py +++ b/construct/io/__init__.py @@ -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): diff --git a/setup.cfg b/setup.cfg index f61b8cd..053796d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 = ' ' diff --git a/tests/test_actions.py b/tests/test_actions.py new file mode 100644 index 0000000..c22903a --- /dev/null +++ b/tests/test_actions.py @@ -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) diff --git a/tests/test_api.py b/tests/test_api.py index b81daa7..6e4388e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -6,7 +6,7 @@ import shutil import sys -# Local imports +# Construct imports import construct from construct.settings import restore_default_settings diff --git a/tests/test_context.py b/tests/test_context.py index e9d80a2..961fccf 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -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 diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 75c67a4..c75a0e4 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -2,17 +2,12 @@ from __future__ import absolute_import -# Standard library imports -import os -import shutil -import sys - -# Local imports +# Construct imports import construct from construct.extensions import Extension # Local imports -from . import data_dir, setup_api, teardown_api +from . import setup_api, teardown_api def setup_module(): @@ -56,7 +51,7 @@ def test_simple_extension(): '''Register and use a simple extension''' api = construct.API(__name__) - api.extensions.register(Counter) + api.register_extension(Counter) assert hasattr(api, 'increment') assert hasattr(api, 'decrement') @@ -67,7 +62,7 @@ def test_simple_extension(): api.decrement() assert api.count() == 0 - api.extensions.unregister(Counter) + api.unregister_extension(Counter) assert not hasattr(api, 'increment') assert not hasattr(api, 'decrement') assert not hasattr(api, 'count') @@ -79,5 +74,5 @@ def test_builtins_loaded(): from construct import ext, hosts api = construct.API(__name__) - for ext in ext.extensions + hosts.extensions: - assert api.extensions.loaded(ext.identifier) + for e in ext.extensions + hosts.extensions: + assert api.extensions.loaded(e.identifier) diff --git a/tests/test_io.py b/tests/test_io.py index 94746f3..63eb2d2 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -2,7 +2,7 @@ from __future__ import absolute_import -# Local imports +# Construct imports import construct from construct.settings import restore_default_settings diff --git a/tests/test_migration.py b/tests/test_migration.py index 61f7124..ec67077 100644 --- a/tests/test_migration.py +++ b/tests/test_migration.py @@ -5,7 +5,7 @@ # Third party imports import fsfs -# Local imports +# Construct imports import construct from construct import migrations from construct.settings import restore_default_settings diff --git a/tests/test_resources.py b/tests/test_resources.py index cd91dfc..0fd5821 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -5,7 +5,7 @@ # Standard library imports import sys -# Local imports +# Construct imports import construct from construct.ui import resources diff --git a/tests/test_settings.py b/tests/test_settings.py index ff4b3c9..5662e7b 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -7,7 +7,7 @@ import shutil import sys -# Local imports +# Construct imports from construct.constants import DEFAULT_SETTINGS from construct.errors import InvalidSettings, ValidationError from construct.settings import Settings, restore_default_settings diff --git a/tests/test_software.py b/tests/test_software.py index fe1c46e..6e9a431 100644 --- a/tests/test_software.py +++ b/tests/test_software.py @@ -7,7 +7,7 @@ import shutil import sys -# Local imports +# Construct imports import construct from construct.errors import InvalidSettings