You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ariatosca.apache.org by da...@apache.org on 2016/12/06 09:47:37 UTC

incubator-ariatosca git commit: Add registry mechanism for extensions [Forced Update!]

Repository: incubator-ariatosca
Updated Branches:
  refs/heads/ARIA-31-extensions e60e22e15 -> e89f27b4b (forced update)


Add registry mechanism for extensions


Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/e89f27b4
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/e89f27b4
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/e89f27b4

Branch: refs/heads/ARIA-31-extensions
Commit: e89f27b4bb1de8c249e21e9c79c7183c3f79c5c6
Parents: fe974e4
Author: Dan Kilman <da...@gigaspaces.com>
Authored: Mon Dec 5 15:28:29 2016 +0200
Committer: Dan Kilman <da...@gigaspaces.com>
Committed: Tue Dec 6 11:47:32 2016 +0200

----------------------------------------------------------------------
 aria/__init__.py                                |  16 +-
 aria/cli/commands.py                            |   9 +-
 aria/orchestrator/events.py                     |  36 ++++
 aria/orchestrator/events/__init__.py            |  57 ------
 .../events/builtin_event_handler.py             | 123 -------------
 .../events/workflow_engine_event_handler.py     |  74 --------
 aria/orchestrator/workflows/__init__.py         |   3 +
 aria/orchestrator/workflows/core/engine.py      |   2 +
 .../workflows/core/event_handler.py             | 113 ++++++++++++
 aria/orchestrator/workflows/logging.py          |  65 +++++++
 aria/parser/__init__.py                         |   5 +-
 aria/parser/loading/__init__.py                 |   3 +-
 aria/parser/loading/uri.py                      |   5 +-
 aria/parser/presentation/__init__.py            |   3 +-
 aria/parser/presentation/source.py              |   7 +-
 aria/parser/specification.py                    |   6 +-
 aria/registry.py                                | 114 ++++++++++++
 aria/utils/plugin.py                            |  39 ----
 extensions/aria_extension_tosca/__init__.py     |  50 +++---
 tests/orchestrator/conftest.py                  |  23 +++
 tests/orchestrator/events/__init__.py           |  14 --
 .../events/test_builtin_event_handlers.py       |  14 --
 .../test_workflow_enginge_event_handlers.py     |   0
 tests/test_registry.py                          | 179 +++++++++++++++++++
 24 files changed, 583 insertions(+), 377 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/__init__.py
----------------------------------------------------------------------
diff --git a/aria/__init__.py b/aria/__init__.py
index 3f81f98..e106ccd 100644
--- a/aria/__init__.py
+++ b/aria/__init__.py
@@ -17,7 +17,6 @@
 Aria top level package
 """
 
-import sys
 import pkgutil
 
 from .VERSION import version as __version__
@@ -25,6 +24,7 @@ from .VERSION import version as __version__
 from .orchestrator.decorators import workflow, operation
 from .storage import ModelStorage, ResourceStorage, models, ModelDriver, ResourceDriver
 from . import (
+    registry,
     utils,
     parser,
     storage,
@@ -43,19 +43,13 @@ _resource_storage = {}
 
 def install_aria_extensions():
     """
-    Iterates all Python packages with names beginning with :code:`aria_extension_` and calls
-    their :code:`install_aria_extension` function if they have it.
+    Iterates all Python packages with names beginning with :code:`aria_extension_` simply loads it.
+    It then calls all functions registered to the on_init registry
     """
-
     for loader, module_name, _ in pkgutil.iter_modules():
         if module_name.startswith('aria_extension_'):
-            module = loader.find_module(module_name).load_module(module_name)
-
-            if hasattr(module, 'install_aria_extension'):
-                module.install_aria_extension()
-
-            # Loading the module has contaminated sys.modules, so we'll clean it up
-            del sys.modules[module_name]
+            loader.find_module(module_name).load_module(module_name)
+    registry.on_init.init()
 
 
 def application_model_storage(driver):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/cli/commands.py
----------------------------------------------------------------------
diff --git a/aria/cli/commands.py b/aria/cli/commands.py
index 3426bb0..8e73a71 100644
--- a/aria/cli/commands.py
+++ b/aria/cli/commands.py
@@ -28,13 +28,14 @@ from importlib import import_module
 
 from yaml import safe_load, YAMLError
 
+from .. import registry
 from .. import (application_model_storage, application_resource_storage)
 from ..logger import LoggerMixin
 from ..storage import (FileSystemModelDriver, FileSystemResourceDriver)
 from ..orchestrator.context.workflow import WorkflowContext
 from ..orchestrator.workflows.core.engine import Engine
 from ..orchestrator.workflows.executor.thread import ThreadExecutor
-from ..parser import (DSL_SPECIFICATION_PACKAGES, iter_specifications)
+from ..parser import iter_specifications
 from ..parser.consumption import (
     ConsumptionContext,
     ConsumerChain,
@@ -45,7 +46,7 @@ from ..parser.consumption import (
     Inputs,
     Instance
 )
-from ..parser.loading import (LiteralLocation, UriLocation, URI_LOADER_PREFIXES)
+from ..parser.loading import LiteralLocation, UriLocation
 from ..utils.application import StorageManager
 from ..utils.caching import cachedmethod
 from ..utils.console import (puts, Colored, indent)
@@ -315,7 +316,7 @@ class ParseCommand(BaseCommand):
 
         if args_namespace.prefix:
             for prefix in args_namespace.prefix:
-                URI_LOADER_PREFIXES.append(prefix)
+                registry.parser.uri_loader_prefix.registry.append(prefix)
 
         cachedmethod.ENABLED = args_namespace.cached_methods
 
@@ -376,7 +377,7 @@ class SpecCommand(BaseCommand):
         super(SpecCommand, self).__call__(args_namespace, unknown_args)
 
         # Make sure that all @dsl_specification decorators are processed
-        for pkg in DSL_SPECIFICATION_PACKAGES:
+        for pkg in registry.parser.specification_package.registry:
             import_modules(pkg)
 
         # TODO: scan YAML documents as well

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/orchestrator/events.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/events.py b/aria/orchestrator/events.py
new file mode 100644
index 0000000..a1c4922
--- /dev/null
+++ b/aria/orchestrator/events.py
@@ -0,0 +1,36 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+ARIA's events Sub-Package
+Path: aria.events
+
+Events package provides events mechanism for different executions in aria.
+"""
+
+from blinker import signal
+
+# workflow engine task signals:
+sent_task_signal = signal('sent_task_signal')
+start_task_signal = signal('start_task_signal')
+on_success_task_signal = signal('success_task_signal')
+on_failure_task_signal = signal('failure_task_signal')
+
+# workflow engine workflow signals:
+start_workflow_signal = signal('start_workflow_signal')
+on_cancelling_workflow_signal = signal('on_cancelling_workflow_signal')
+on_cancelled_workflow_signal = signal('on_cancelled_workflow_signal')
+on_success_workflow_signal = signal('on_success_workflow_signal')
+on_failure_workflow_signal = signal('on_failure_workflow_signal')

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/orchestrator/events/__init__.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/events/__init__.py b/aria/orchestrator/events/__init__.py
deleted file mode 100644
index fbc0f32..0000000
--- a/aria/orchestrator/events/__init__.py
+++ /dev/null
@@ -1,57 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-ARIA's events Sub-Package
-Path: aria.events
-
-Events package provides events mechanism for different executions in aria.
-
-
-1. storage_event_handler: implementation of storage handlers for workflow and operation events.
-2. logger_event_handler: implementation of logger handlers for workflow and operation events.
-
-API:
-    * start_task_signal
-    * on_success_task_signal
-    * on_failure_task_signal
-    * start_workflow_signal
-    * on_success_workflow_signal
-    * on_failure_workflow_signal
-"""
-
-import os
-
-from blinker import signal
-
-from aria.utils.plugin import plugin_installer
-
-# workflow engine task signals:
-sent_task_signal = signal('sent_task_signal')
-start_task_signal = signal('start_task_signal')
-on_success_task_signal = signal('success_task_signal')
-on_failure_task_signal = signal('failure_task_signal')
-
-# workflow engine workflow signals:
-start_workflow_signal = signal('start_workflow_signal')
-on_cancelling_workflow_signal = signal('on_cancelling_workflow_signal')
-on_cancelled_workflow_signal = signal('on_cancelled_workflow_signal')
-on_success_workflow_signal = signal('on_success_workflow_signal')
-on_failure_workflow_signal = signal('on_failure_workflow_signal')
-
-plugin_installer(
-    path=os.path.dirname(os.path.realpath(__file__)),
-    plugin_suffix='_event_handler',
-    package=__package__)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/orchestrator/events/builtin_event_handler.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/events/builtin_event_handler.py b/aria/orchestrator/events/builtin_event_handler.py
deleted file mode 100644
index c5cccfe..0000000
--- a/aria/orchestrator/events/builtin_event_handler.py
+++ /dev/null
@@ -1,123 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-Aria's events Sub-Package
-Path: aria.events.storage_event_handler
-
-Implementation of storage handlers for workflow and operation events.
-"""
-
-
-from datetime import (
-    datetime,
-    timedelta,
-)
-
-from . import (
-    start_workflow_signal,
-    on_success_workflow_signal,
-    on_failure_workflow_signal,
-    on_cancelled_workflow_signal,
-    on_cancelling_workflow_signal,
-    sent_task_signal,
-    start_task_signal,
-    on_success_task_signal,
-    on_failure_task_signal,
-)
-
-
-@sent_task_signal.connect
-def _task_sent(task, *args, **kwargs):
-    with task._update():
-        task.status = task.SENT
-
-
-@start_task_signal.connect
-def _task_started(task, *args, **kwargs):
-    with task._update():
-        task.started_at = datetime.utcnow()
-        task.status = task.STARTED
-
-
-@on_failure_task_signal.connect
-def _task_failed(task, *args, **kwargs):
-    with task._update():
-        should_retry = (
-            (task.retry_count < task.max_attempts - 1 or
-             task.max_attempts == task.INFINITE_RETRIES) and
-            # ignore_failure check here means the task will not be retries and it will be marked as
-            # failed. The engine will also look at ignore_failure so it won't fail the workflow.
-            not task.ignore_failure)
-        if should_retry:
-            task.status = task.RETRYING
-            task.retry_count += 1
-            task.due_at = datetime.utcnow() + timedelta(seconds=task.retry_interval)
-        else:
-            task.ended_at = datetime.utcnow()
-            task.status = task.FAILED
-
-
-@on_success_task_signal.connect
-def _task_succeeded(task, *args, **kwargs):
-    with task._update():
-        task.ended_at = datetime.utcnow()
-        task.status = task.SUCCESS
-
-
-@start_workflow_signal.connect
-def _workflow_started(workflow_context, *args, **kwargs):
-    execution = workflow_context.execution
-    execution.status = execution.STARTED
-    execution.started_at = datetime.utcnow()
-    workflow_context.execution = execution
-
-
-@on_failure_workflow_signal.connect
-def _workflow_failed(workflow_context, exception, *args, **kwargs):
-    execution = workflow_context.execution
-    execution.error = str(exception)
-    execution.status = execution.FAILED
-    execution.ended_at = datetime.utcnow()
-    workflow_context.execution = execution
-
-
-@on_success_workflow_signal.connect
-def _workflow_succeeded(workflow_context, *args, **kwargs):
-    execution = workflow_context.execution
-    execution.status = execution.TERMINATED
-    execution.ended_at = datetime.utcnow()
-    workflow_context.execution = execution
-
-
-@on_cancelled_workflow_signal.connect
-def _workflow_cancelled(workflow_context, *args, **kwargs):
-    execution = workflow_context.execution
-    # _workflow_cancelling function may have called this function
-    # already
-    if execution.status == execution.CANCELLED:
-        return
-    execution.status = execution.CANCELLED
-    execution.ended_at = datetime.utcnow()
-    workflow_context.execution = execution
-
-
-@on_cancelling_workflow_signal.connect
-def _workflow_cancelling(workflow_context, *args, **kwargs):
-    execution = workflow_context.execution
-    if execution.status == execution.PENDING:
-        return _workflow_cancelled(workflow_context=workflow_context)
-    execution.status = execution.CANCELLING
-    workflow_context.execution = execution

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/orchestrator/events/workflow_engine_event_handler.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/events/workflow_engine_event_handler.py b/aria/orchestrator/events/workflow_engine_event_handler.py
deleted file mode 100644
index 7df11d1..0000000
--- a/aria/orchestrator/events/workflow_engine_event_handler.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-
-"""
-Aria's events Sub-Package
-Path: aria.events.storage_event_handler
-
-Implementation of logger handlers for workflow and operation events.
-"""
-
-from . import (
-    start_task_signal,
-    on_success_task_signal,
-    on_failure_task_signal,
-    start_workflow_signal,
-    on_success_workflow_signal,
-    on_failure_workflow_signal,
-    on_cancelled_workflow_signal,
-    on_cancelling_workflow_signal,
-)
-
-
-@start_task_signal.connect
-def _start_task_handler(task, **kwargs):
-    task.logger.debug('Event: Starting task: {task.name}'.format(task=task))
-
-
-@on_success_task_signal.connect
-def _success_task_handler(task, **kwargs):
-    task.logger.debug('Event: Task success: {task.name}'.format(task=task))
-
-
-@on_failure_task_signal.connect
-def _failure_operation_handler(task, **kwargs):
-    task.logger.error('Event: Task failure: {task.name}'.format(task=task),
-                      exc_info=kwargs.get('exception', True))
-
-
-@start_workflow_signal.connect
-def _start_workflow_handler(context, **kwargs):
-    context.logger.debug('Event: Starting workflow: {context.name}'.format(context=context))
-
-
-@on_failure_workflow_signal.connect
-def _failure_workflow_handler(context, **kwargs):
-    context.logger.debug('Event: Workflow failure: {context.name}'.format(context=context))
-
-
-@on_success_workflow_signal.connect
-def _success_workflow_handler(context, **kwargs):
-    context.logger.debug('Event: Workflow success: {context.name}'.format(context=context))
-
-
-@on_cancelled_workflow_signal.connect
-def _cancel_workflow_handler(context, **kwargs):
-    context.logger.debug('Event: Workflow cancelled: {context.name}'.format(context=context))
-
-
-@on_cancelling_workflow_signal.connect
-def _cancelling_workflow_handler(context, **kwargs):
-    context.logger.debug('Event: Workflow cancelling: {context.name}'.format(context=context))

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/orchestrator/workflows/__init__.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/__init__.py b/aria/orchestrator/workflows/__init__.py
index ae1e83e..4522493 100644
--- a/aria/orchestrator/workflows/__init__.py
+++ b/aria/orchestrator/workflows/__init__.py
@@ -12,3 +12,6 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+
+# Import required so that logging signals are registered
+from . import logging

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/orchestrator/workflows/core/engine.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/core/engine.py b/aria/orchestrator/workflows/core/engine.py
index 87ea8c6..2588f5d 100644
--- a/aria/orchestrator/workflows/core/engine.py
+++ b/aria/orchestrator/workflows/core/engine.py
@@ -29,6 +29,8 @@ from aria.orchestrator import events
 from .. import exceptions
 from . import task as engine_task
 from . import translation
+# Import required so all signals are registered
+from . import event_handler  # pylint: disable=unused-import
 
 
 class Engine(logger.LoggerMixin):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/orchestrator/workflows/core/event_handler.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/core/event_handler.py b/aria/orchestrator/workflows/core/event_handler.py
new file mode 100644
index 0000000..d05cbcb
--- /dev/null
+++ b/aria/orchestrator/workflows/core/event_handler.py
@@ -0,0 +1,113 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Aria's events Sub-Package
+Path: aria.events.storage_event_handler
+
+Implementation of storage handlers for workflow and operation events.
+"""
+
+
+from datetime import (
+    datetime,
+    timedelta,
+)
+
+from ... import events
+
+
+@events.sent_task_signal.connect
+def _task_sent(task, *args, **kwargs):
+    with task._update():
+        task.status = task.SENT
+
+
+@events.start_task_signal.connect
+def _task_started(task, *args, **kwargs):
+    with task._update():
+        task.started_at = datetime.utcnow()
+        task.status = task.STARTED
+
+
+@events.on_failure_task_signal.connect
+def _task_failed(task, *args, **kwargs):
+    with task._update():
+        should_retry = (
+            (task.retry_count < task.max_attempts - 1 or
+             task.max_attempts == task.INFINITE_RETRIES) and
+            # ignore_failure check here means the task will not be retries and it will be marked as
+            # failed. The engine will also look at ignore_failure so it won't fail the workflow.
+            not task.ignore_failure)
+        if should_retry:
+            task.status = task.RETRYING
+            task.retry_count += 1
+            task.due_at = datetime.utcnow() + timedelta(seconds=task.retry_interval)
+        else:
+            task.ended_at = datetime.utcnow()
+            task.status = task.FAILED
+
+
+@events.on_success_task_signal.connect
+def _task_succeeded(task, *args, **kwargs):
+    with task._update():
+        task.ended_at = datetime.utcnow()
+        task.status = task.SUCCESS
+
+
+@events.start_workflow_signal.connect
+def _workflow_started(workflow_context, *args, **kwargs):
+    execution = workflow_context.execution
+    execution.status = execution.STARTED
+    execution.started_at = datetime.utcnow()
+    workflow_context.execution = execution
+
+
+@events.on_failure_workflow_signal.connect
+def _workflow_failed(workflow_context, exception, *args, **kwargs):
+    execution = workflow_context.execution
+    execution.error = str(exception)
+    execution.status = execution.FAILED
+    execution.ended_at = datetime.utcnow()
+    workflow_context.execution = execution
+
+
+@events.on_success_workflow_signal.connect
+def _workflow_succeeded(workflow_context, *args, **kwargs):
+    execution = workflow_context.execution
+    execution.status = execution.TERMINATED
+    execution.ended_at = datetime.utcnow()
+    workflow_context.execution = execution
+
+
+@events.on_cancelled_workflow_signal.connect
+def _workflow_cancelled(workflow_context, *args, **kwargs):
+    execution = workflow_context.execution
+    # _workflow_cancelling function may have called this function
+    # already
+    if execution.status == execution.CANCELLED:
+        return
+    execution.status = execution.CANCELLED
+    execution.ended_at = datetime.utcnow()
+    workflow_context.execution = execution
+
+
+@events.on_cancelling_workflow_signal.connect
+def _workflow_cancelling(workflow_context, *args, **kwargs):
+    execution = workflow_context.execution
+    if execution.status == execution.PENDING:
+        return _workflow_cancelled(workflow_context=workflow_context)
+    execution.status = execution.CANCELLING
+    workflow_context.execution = execution

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/orchestrator/workflows/logging.py
----------------------------------------------------------------------
diff --git a/aria/orchestrator/workflows/logging.py b/aria/orchestrator/workflows/logging.py
new file mode 100644
index 0000000..409ce0a
--- /dev/null
+++ b/aria/orchestrator/workflows/logging.py
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+"""
+Aria's events Sub-Package
+Path: aria.events.storage_event_handler
+
+Implementation of logger handlers for workflow and operation events.
+"""
+
+from .. import events
+
+
+@events.start_task_signal.connect
+def _start_task_handler(task, **kwargs):
+    task.logger.debug('Event: Starting task: {task.name}'.format(task=task))
+
+
+@events.on_success_task_signal.connect
+def _success_task_handler(task, **kwargs):
+    task.logger.debug('Event: Task success: {task.name}'.format(task=task))
+
+
+@events.on_failure_task_signal.connect
+def _failure_operation_handler(task, **kwargs):
+    task.logger.error('Event: Task failure: {task.name}'.format(task=task),
+                      exc_info=kwargs.get('exception', True))
+
+
+@events.start_workflow_signal.connect
+def _start_workflow_handler(context, **kwargs):
+    context.logger.debug('Event: Starting workflow: {context.name}'.format(context=context))
+
+
+@events.on_failure_workflow_signal.connect
+def _failure_workflow_handler(context, **kwargs):
+    context.logger.debug('Event: Workflow failure: {context.name}'.format(context=context))
+
+
+@events.on_success_workflow_signal.connect
+def _success_workflow_handler(context, **kwargs):
+    context.logger.debug('Event: Workflow success: {context.name}'.format(context=context))
+
+
+@events.on_cancelled_workflow_signal.connect
+def _cancel_workflow_handler(context, **kwargs):
+    context.logger.debug('Event: Workflow cancelled: {context.name}'.format(context=context))
+
+
+@events.on_cancelling_workflow_signal.connect
+def _cancelling_workflow_handler(context, **kwargs):
+    context.logger.debug('Event: Workflow cancelling: {context.name}'.format(context=context))

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/parser/__init__.py
----------------------------------------------------------------------
diff --git a/aria/parser/__init__.py b/aria/parser/__init__.py
index 2a83cd4..9ab8785 100644
--- a/aria/parser/__init__.py
+++ b/aria/parser/__init__.py
@@ -13,8 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from .specification import (DSL_SPECIFICATION_PACKAGES, DSL_SPECIFICATION_URLS, dsl_specification,
-                            iter_specifications)
+from .specification import dsl_specification, iter_specifications
 
 
 MODULES = (
@@ -27,7 +26,5 @@ MODULES = (
 
 __all__ = (
     'MODULES',
-    'DSL_SPECIFICATION_PACKAGES',
-    'DSL_SPECIFICATION_URLS',
     'dsl_specification',
     'iter_specifications')

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/parser/loading/__init__.py
----------------------------------------------------------------------
diff --git a/aria/parser/loading/__init__.py b/aria/parser/loading/__init__.py
index f331e39..006f164 100644
--- a/aria/parser/loading/__init__.py
+++ b/aria/parser/loading/__init__.py
@@ -20,7 +20,7 @@ from .loader import Loader
 from .source import LoaderSource, DefaultLoaderSource
 from .location import Location, UriLocation, LiteralLocation
 from .literal import LiteralLoader
-from .uri import URI_LOADER_PREFIXES, UriTextLoader
+from .uri import UriTextLoader
 from .request import SESSION, SESSION_CACHE_PATH, RequestLoader, RequestTextLoader
 from .file import FileTextLoader
 
@@ -37,7 +37,6 @@ __all__ = (
     'UriLocation',
     'LiteralLocation',
     'LiteralLoader',
-    'URI_LOADER_PREFIXES',
     'UriTextLoader',
     'SESSION',
     'SESSION_CACHE_PATH',

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/parser/loading/uri.py
----------------------------------------------------------------------
diff --git a/aria/parser/loading/uri.py b/aria/parser/loading/uri.py
index f94a003..a613e7d 100644
--- a/aria/parser/loading/uri.py
+++ b/aria/parser/loading/uri.py
@@ -16,6 +16,7 @@
 import os
 from urlparse import urljoin
 
+from ...registry import parser
 from ...utils.collections import StrictList
 from ...utils.uris import as_file
 from .loader import Loader
@@ -23,8 +24,6 @@ from .file import FileTextLoader
 from .request import RequestTextLoader
 from .exceptions import DocumentNotFoundException
 
-URI_LOADER_PREFIXES = StrictList(value_class=basestring)
-
 
 class UriTextLoader(Loader):
     """
@@ -58,7 +57,7 @@ class UriTextLoader(Loader):
             add_prefix(origin_location.prefix)
 
         add_prefixes(context.prefixes)
-        add_prefixes(URI_LOADER_PREFIXES)
+        add_prefixes(parser.uri_loader_prefix.registry)
 
     def open(self):
         try:

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/parser/presentation/__init__.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/__init__.py b/aria/parser/presentation/__init__.py
index ba7a163..a681695 100644
--- a/aria/parser/presentation/__init__.py
+++ b/aria/parser/presentation/__init__.py
@@ -18,7 +18,7 @@ from .exceptions import PresenterException, PresenterNotFoundError
 from .context import PresentationContext
 from .presenter import Presenter
 from .presentation import Value, PresentationBase, Presentation, AsIsPresentation
-from .source import PRESENTER_CLASSES, PresenterSource, DefaultPresenterSource
+from .source import PresenterSource, DefaultPresenterSource
 from .null import NULL, none_to_null, null_to_none
 from .fields import (Field, has_fields, short_form_field, allow_unknown_fields, primitive_field,
                      primitive_list_field, primitive_dict_field, primitive_dict_unknown_fields,
@@ -42,7 +42,6 @@ __all__ = (
     'Presentation',
     'AsIsPresentation',
     'PresenterSource',
-    'PRESENTER_CLASSES',
     'DefaultPresenterSource',
     'NULL',
     'none_to_null',

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/parser/presentation/source.py
----------------------------------------------------------------------
diff --git a/aria/parser/presentation/source.py b/aria/parser/presentation/source.py
index 8ff4cab..db0582c 100644
--- a/aria/parser/presentation/source.py
+++ b/aria/parser/presentation/source.py
@@ -13,9 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from .exceptions import PresenterNotFoundError
 
-PRESENTER_CLASSES = []
+from ...registry import parser
+
+from .exceptions import PresenterNotFoundError
 
 
 class PresenterSource(object):
@@ -36,7 +37,7 @@ class DefaultPresenterSource(PresenterSource):
 
     def __init__(self, classes=None):
         if classes is None:
-            classes = PRESENTER_CLASSES
+            classes = parser.presenter_class.registry
         self.classes = classes
 
     def get_presenter(self, raw):

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/parser/specification.py
----------------------------------------------------------------------
diff --git a/aria/parser/specification.py b/aria/parser/specification.py
index 1c7e1f2..c0f5237 100644
--- a/aria/parser/specification.py
+++ b/aria/parser/specification.py
@@ -15,12 +15,10 @@
 
 import re
 
+from ..registry import parser
 from ..utils.collections import OrderedDict
 from ..utils.formatting import full_type_name
 
-
-DSL_SPECIFICATION_PACKAGES = []
-DSL_SPECIFICATION_URLS = {}
 _DSL_SPECIFICATIONS = {}
 
 
@@ -84,7 +82,7 @@ def _section_key(value):
 def _fix_details(details, spec):
     code = details.get('code')
     doc = details.get('doc')
-    url = DSL_SPECIFICATION_URLS.get(spec)
+    url = parser.specification_url.registry.get(spec)
 
     if (url is not None) and (doc is not None):
         # Look for a URL in ReST docstring that begins with our url

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/registry.py
----------------------------------------------------------------------
diff --git a/aria/registry.py b/aria/registry.py
new file mode 100644
index 0000000..85077b6
--- /dev/null
+++ b/aria/registry.py
@@ -0,0 +1,114 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from .utils import collections
+
+_STUB = object()
+
+
+class _Registerer(object):
+
+    def __init__(self, registerer_function):
+        self._registry = None
+        self._registerer_function = registerer_function
+        self._registered_functions = []
+
+    def __call__(self, function):
+        self._registered_functions.append(function)
+        if self._registry:
+            self._register_function(function)
+        return function
+
+    @property
+    def registry(self):
+        if not self._registry:
+            self.init()
+        return self._registry
+
+    def init(self):
+        if self._registry:
+            return
+        self._registry = self._registerer_function()
+        for function in self._registered_functions:
+            self._register_function(function)
+
+    def clear(self):
+        self._registry = None
+        self._registered_functions = []
+
+    def _register_function(self, function):
+        result = function()
+        if isinstance(self._registry, dict):
+            self._registry.update(result)
+        elif isinstance(self._registry, list):
+            if not isinstance(result, (list, tuple, set)):
+                result = [result]
+            self._registry += list(result)
+        elif self._registry == _STUB:
+            pass
+        else:
+            raise RuntimeError('Unsupported registry type')
+_registerer = _Registerer
+
+
+class _ParserRegistration(object):
+
+    @staticmethod
+    @_registerer
+    def presenter_class(*_):
+        """
+        Decorator for parser presentation class registration.
+        Decorated functions can return a single class or a list/tuple of classes
+        """
+        return []
+
+    @staticmethod
+    @_registerer
+    def specification_package(*_):
+        """
+        Decorator for specification package registration.
+        Decorated functions can return a single class or a list/tuple of classes
+        """
+        return []
+
+    @staticmethod
+    @_registerer
+    def specification_url(*_):
+        """
+        Decorator for parser specification URL registration.
+        Decorated functions should return a dictionary from names to URLs
+        a list/tuple of classes
+        """
+        return {}
+
+    @staticmethod
+    @_registerer
+    def uri_loader_prefix(*_):
+        """
+        Decorator for parser uri loader prefix registration.
+        Decorated functions can return a single class or a list/tuple of classes
+        """
+        return collections.StrictList(value_class=basestring)
+
+parser = _ParserRegistration()
+
+
+@_registerer
+def on_init(*_):
+    """
+    Decorator for functions that should perform an initialization process not addressed
+    by the other register functions
+    """
+    return _STUB

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/aria/utils/plugin.py
----------------------------------------------------------------------
diff --git a/aria/utils/plugin.py b/aria/utils/plugin.py
deleted file mode 100644
index bb2b974..0000000
--- a/aria/utils/plugin.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-"""
-Contains utility methods that enable dynamic python code loading
-# TODO: merge with tools.module
-"""
-
-import os
-from importlib import import_module
-
-
-def plugin_installer(path, plugin_suffix, package=None, callback=None):
-    """
-    Load each module under ``path`` that ends with ``plugin_suffix``. If ``callback`` is supplied,
-    call it with each loaded module.
-    """
-    assert callback is None or callable(callback)
-    plugin_suffix = '{0}.py'.format(plugin_suffix)
-
-    for file_name in os.listdir(path):
-        if not file_name.endswith(plugin_suffix):
-            continue
-        module_name = '{0}.{1}'.format(package, file_name[:-3]) if package else file_name[:-3]
-        module = import_module(module_name)
-        if callback:
-            callback(module)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/extensions/aria_extension_tosca/__init__.py
----------------------------------------------------------------------
diff --git a/extensions/aria_extension_tosca/__init__.py b/extensions/aria_extension_tosca/__init__.py
index 54e1c84..8f8577e 100644
--- a/extensions/aria_extension_tosca/__init__.py
+++ b/extensions/aria_extension_tosca/__init__.py
@@ -15,34 +15,38 @@
 
 import os.path
 
-from aria.parser import (DSL_SPECIFICATION_PACKAGES, DSL_SPECIFICATION_URLS)
-from aria.parser.presentation import PRESENTER_CLASSES
-from aria.parser.loading import URI_LOADER_PREFIXES
+from aria import registry
 
 from .simple_v1_0 import ToscaSimplePresenter1_0
 from .simple_nfv_v1_0 import ToscaSimpleNfvPresenter1_0
 
-def install_aria_extension():
-    '''
-    Installs the TOSCA extension to ARIA.
-    '''
-
-    global PRESENTER_CLASSES # pylint: disable=global-statement
-    PRESENTER_CLASSES += (ToscaSimplePresenter1_0, ToscaSimpleNfvPresenter1_0)
-
-    # DSL specification
-    DSL_SPECIFICATION_PACKAGES.append('aria_extension_tosca')
-    DSL_SPECIFICATION_URLS['yaml-1.1'] = \
-        'http://yaml.org'
-    DSL_SPECIFICATION_URLS['tosca-simple-1.0'] = \
-        'http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/cos01' \
-        '/TOSCA-Simple-Profile-YAML-v1.0-cos01.html'
-    DSL_SPECIFICATION_URLS['tosca-simple-nfv-1.0'] = \
-        'http://docs.oasis-open.org/tosca/tosca-nfv/v1.0/tosca-nfv-v1.0.html'
-
-    # Imports
+
+@registry.parser.presenter_class
+def presenter_classes():
+    return ToscaSimplePresenter1_0, ToscaSimpleNfvPresenter1_0
+
+
+@registry.parser.specification_package
+def specification_package():
+    return 'aria_extension_tosca'
+
+
+@registry.parser.specification_url
+def specification_urls():
+    return {
+        'yaml-1.1': 'http://yaml.org',
+        'tosca-simple-1.0': 'http://docs.oasis-open.org/tosca/TOSCA-Simple-Profile-YAML/v1.0/cos01'
+                            '/TOSCA-Simple-Profile-YAML-v1.0-cos01.html',
+        'tosca-simple-nfv-1.0': 'http://docs.oasis-open.org/tosca/tosca-nfv/v1.0/'
+                                'tosca-nfv-v1.0.html'
+    }
+
+
+@registry.parser.uri_loader_prefix
+def uri_loader_prefix():
     the_dir = os.path.dirname(__file__)
-    URI_LOADER_PREFIXES.append(os.path.join(the_dir, 'profiles'))
+    return os.path.join(the_dir, 'profiles')
+
 
 MODULES = (
     'simple_v1_0',

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/tests/orchestrator/conftest.py
----------------------------------------------------------------------
diff --git a/tests/orchestrator/conftest.py b/tests/orchestrator/conftest.py
new file mode 100644
index 0000000..4b24f18
--- /dev/null
+++ b/tests/orchestrator/conftest.py
@@ -0,0 +1,23 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import aria
+
+
+@pytest.fixture(scope='session', autouse=True)
+def install_aria_extensions():
+    aria.install_aria_extensions()

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/tests/orchestrator/events/__init__.py
----------------------------------------------------------------------
diff --git a/tests/orchestrator/events/__init__.py b/tests/orchestrator/events/__init__.py
deleted file mode 100644
index ae1e83e..0000000
--- a/tests/orchestrator/events/__init__.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/tests/orchestrator/events/test_builtin_event_handlers.py
----------------------------------------------------------------------
diff --git a/tests/orchestrator/events/test_builtin_event_handlers.py b/tests/orchestrator/events/test_builtin_event_handlers.py
deleted file mode 100644
index ae1e83e..0000000
--- a/tests/orchestrator/events/test_builtin_event_handlers.py
+++ /dev/null
@@ -1,14 +0,0 @@
-# Licensed to the Apache Software Foundation (ASF) under one or more
-# contributor license agreements.  See the NOTICE file distributed with
-# this work for additional information regarding copyright ownership.
-# The ASF licenses this file to You under the Apache License, Version 2.0
-# (the "License"); you may not use this file except in compliance with
-# the License.  You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/tests/orchestrator/events/test_workflow_enginge_event_handlers.py
----------------------------------------------------------------------
diff --git a/tests/orchestrator/events/test_workflow_enginge_event_handlers.py b/tests/orchestrator/events/test_workflow_enginge_event_handlers.py
deleted file mode 100644
index e69de29..0000000

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/e89f27b4/tests/test_registry.py
----------------------------------------------------------------------
diff --git a/tests/test_registry.py b/tests/test_registry.py
new file mode 100644
index 0000000..b9e7a04
--- /dev/null
+++ b/tests/test_registry.py
@@ -0,0 +1,179 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from aria import registry
+
+
+@pytest.fixture
+def cleanup():
+    registry.on_init.clear()
+
+
+def test_on_init():
+    on_init_called = []
+
+    @registry.on_init
+    def on_init():
+        on_init_called.append(True)
+
+    assert len(on_init_called) == 0
+    registry.on_init.init()
+    assert len(on_init_called) == 1
+    registry.on_init.init()
+    assert len(on_init_called) == 1
+    assert registry.on_init.registry is registry._STUB
+
+
+class TestRegisterer(object):
+
+    def test_invocation_only_once(self):
+        @registry._registerer
+        def test_registerer(*_):
+            return []
+
+        @test_registerer
+        def mock():
+            return True
+
+        assert test_registerer.registry == [True]
+        assert test_registerer.registry == [True]
+        test_registerer.init()
+        assert test_registerer.registry == [True]
+
+    def test_invocation_only_after_registry_access(self):
+        mock_called = []
+
+        @registry._registerer
+        def test_registerer(*_):
+            return []
+
+        @test_registerer
+        def mock():
+            mock_called.append(True)
+            return True
+
+        assert not mock_called
+        assert test_registerer.registry == [True]
+        assert mock_called
+
+    def test_invocation_only_after_init(self):
+        mock_called = []
+
+        @registry._registerer
+        def test_registerer(*_):
+            return []
+
+        @test_registerer
+        def mock():
+            mock_called.append(True)
+            return True
+
+        assert not mock_called
+        assert test_registerer.init()
+        assert mock_called
+
+    def test_invocation_on_runtime_registration(self):
+        @registry._registerer
+        def test_registerer(*_):
+            return []
+
+        assert test_registerer.registry == []
+
+        @test_registerer
+        def mock():
+            return True
+
+        assert test_registerer.registry == [True]
+
+    def test_list_based_registerer_with_single_element_registration(self):
+        @registry._registerer
+        def test_registerer(*_):
+            return []
+
+        @test_registerer
+        def mock():
+            return True
+
+        assert test_registerer.registry == [True]
+
+    def test_list_based_registerer_with_sequence_element_registration(self):
+        @registry._registerer
+        def test_registerer(*_):
+            return []
+
+        @test_registerer
+        def mock1():
+            return [True, True]
+
+        @test_registerer
+        def mock2():
+            return True, True
+
+        @test_registerer
+        def mock3():
+            return set([True])
+
+        assert test_registerer.registry == [True] * 5
+
+    def test_dict_based_registerer(self):
+        @registry._registerer
+        def test_registerer(*_):
+            return {}
+
+        @test_registerer
+        def mock1():
+            return {
+                'a': 'a',
+                'b': 'b'
+            }
+
+        @test_registerer
+        def mock2():
+            return {
+                'c': 'c',
+                'd': 'd'
+            }
+
+        assert test_registerer.registry == {
+            'a': 'a',
+            'b': 'b',
+            'c': 'c',
+            'd': 'd'
+        }
+
+    def test_stub_based_registerer(self):
+        @registry._registerer
+        def test_registerer(*_):
+            return registry._STUB
+
+        @test_registerer
+        def mock():
+            return 123
+
+        assert test_registerer.registry == registry._STUB
+
+    def test_unsupported_registerer(self):
+        with pytest.raises(RuntimeError):
+            @registry._registerer
+            def test_registerer(*_):
+                return 1
+
+            @test_registerer
+            def mock():
+                pass
+
+            test_registerer.init()