Repository: morepath/dectate

Issue:
directive directive confusing
When you use the ``directive`` directive, you need to make sure that the module it is in is imported before you scan any other code. This can be surprising, as you might expect it works like any other Dectate directive, where import order is immaterial.

Perhaps this implies the ``directive`` directive shouldn't be a directive at all. Directives are really like methods. Perhap we should install them like methods on the ``App`` class, like this:

```
class App(dectate.App):
     foo = dectate.directive(FooAction)
```

If we make this the way to install directives, we would have no more import issues, as it's impossible to use a new directive without importing its app class first.

What do you think, @href, @taschini?


Discussion:

Patch:
diff --git a/CHANGES.txt b/CHANGES.txt
index 37a51e8..75646cf 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -4,6 +4,38 @@ CHANGES
 0.12 (unreleased)
 =================
 
+- **Breaking changes**: previously you defined new directives using the
+  ``App.directive`` directive. This would lead to import confusion: you
+  *have* to import the modules that define directives before you can actually
+  use them, even though you've already imported your app class.
+
+  In this version of Dectate we've changed the way you define directives.
+  Instead of::
+
+     class MyApp(dectate.App):
+         pass
+
+     @MyApp.directive('foo')
+     class FooAction(dectate.Action):
+        ...
+
+  You now write this::
+
+     class FooAction(directive.Action)
+         ...
+
+     class MyApp(dectate.App):
+         foo = directive(FooAction)
+
+  So, you define the directives directly on the app class that needs
+  them.
+
+  Uses of ``private_action_class`` should be replaced by an underscored
+  directive definition::
+
+     class MyApp(dectate.App):
+        _my_private_thing = directive(PrivateAction)
+
 - Use the same Git ignore file used in other Morepath projects.
 
 - If you set the ``app_class_arg`` class attribute to ``True`` on an
diff --git a/dectate/__init__.py b/dectate/__init__.py
index 153be1a..1ffc9b2 100644
--- a/dectate/__init__.py
+++ b/dectate/__init__.py
@@ -1,5 +1,5 @@
 # flake8: noqa
-from .app import App
+from .app import App, directive
 from .config import commit, Action, Composite, CodeInfo, NOT_FOUND
 from .error import (ConfigError, DirectiveError, TopologicalSortError,
                     DirectiveReportError, ConflictError, QueryError)
diff --git a/dectate/app.py b/dectate/app.py
index de93c83..ceace6e 100644
--- a/dectate/app.py
+++ b/dectate/app.py
@@ -1,4 +1,3 @@
-import logging
 import sys
 from .config import Configurable, Directive, commit, create_code_info
 from .compat import with_metaclass
@@ -65,51 +64,14 @@ class App(with_metaclass(AppMeta)):
     """
 
     @classmethod
-    def directive(cls, name):
-        """Decorator to register a new directive with this application class.
-
-        You use this as a class decorator for a
-        :class:`dectate.Action` or a :class:`dectate.Composite`
-        subclass::
-
-           @MyApp.directive('my_directive')
-           class FooAction(dectate.Action):
-               ...
-
-        This needs to be executed *before* the directive is used and
-        thus might introduce import dependency issues unlike normal
-        Dectate configuration, so beware! An easy way to make sure
-        that all directives are installed before you use them is to
-        make sure you define them in the same module as where you
-        define the :class:`App` subclass that has them.
-
-        :param name: the name of the directive to register.
-        :return: a directive that when called installs the directive
-          method on the class.
-        """
-        return DirectiveDirective(cls, name)
-
-    @classmethod
-    def private_action_class(cls, action_class):
-        """Register a private action class.
-
-        In some cases action classes can be an implementation detail,
-        for instance in the implementation of a Composite action.
-
-        In this case you don't want the action class to be known
-        but not have a directive.
-
-        This function may be used as a decorator like this::
-
-          @App.private_action_class
-          class MyActionClass(dectate.Action):
-              ...
-
-        :param action_class: the :class:`dectate.Action` subclass to register.
-        :return: the :class`dectate.Action` class that was registered.
-        """
-        cls.dectate.register_action_class(action_class)
-        return action_class
+    def get_directive_methods(cls):
+        for name in dir(cls):
+            attr = getattr(cls, name)
+            im_func = getattr(attr, '__func__', None)
+            if im_func is None:
+                continue
+            if hasattr(im_func, 'action_factory'):
+                yield name, attr
 
     @classmethod
     def commit(cls):
@@ -146,42 +108,36 @@ class App(with_metaclass(AppMeta)):
         pass
 
 
-class DirectiveDirective(object):
-    """Implementation of the ``directive`` directive.
+def directive(action_factory):
+    """Create a classmethod to hook action to application class.
 
-    :param cls: the class that this directive is registered on.
-    :param name: the name of the directive.
-    """
-    def __init__(self, cls, name):
-        self.cls = cls
-        self.name = name
+    You pass in a :class:`dectate.Action` or a
+    :class:`dectate.Composite` subclass and can attach the result as a
+    class method to an :class:`dectate.App` subclass::
 
-    def __call__(self, action_factory):
-        """Register the directive with app class.
+      class FooAction(dectate.Action):
+          ...
 
-        Creates a class method on the app class for the directive.
+      class MyApp(dectate.App):
+          my_directive = dectate.directive(MyAction)
 
-        :param action_factory: the :class:`dectate.Action` or
-          :class:`dectate.Composite` subclass to register.
-        :return: the action or composite subclass that was registered.
-        """
-        directive_name = self.name
-
-        def method(cls, *args, **kw):
-            frame = sys._getframe(1)
-            code_info = create_code_info(frame)
-            logger = logging.getLogger('%s.%s' %
-                                       (cls.logger_name, directive_name))
-            return Directive(cls, action_factory, args, kw,
-                             code_info, directive_name, logger)
-        method.action_factory = action_factory  # to help sphinxext
-        setattr(self.cls, self.name, classmethod(method))
-        method.__name__ = self.name
-        # As of Python 3.5, the repr of bound methods uses __qualname__ instead
-        # of __name__.  See http://bugs.python.org/issue21389#msg217566
-        if hasattr(method, '__qualname__'):
-            method.__qualname__ = type(self.cls).__name__ + '.' + self.name
-        method.__doc__ = action_factory.__doc__
-        method.__module__ = action_factory.__module__
-        self.cls.dectate.register_action_class(action_factory)
-        return action_factory
+    Alternatively you can also define the direction inline using
+    this as a decorator::
+
+      class MyApp(dectate.App):
+          @directive
+          class my_directive(dectate.Action):
+              ...
+
+    :param action_factory: an action class to use as the directive.
+    :return: a class method that represents the directive.
+    """
+    def method(cls, *args, **kw):
+        frame = sys._getframe(1)
+        code_info = create_code_info(frame)
+        return Directive(action_factory, code_info, cls, args, kw)
+    # sphinxext and App.get_action_classes need to recognize this
+    method.action_factory = action_factory
+    method.__doc__ = action_factory.__doc__
+    method.__module__ = action_factory.__module__
+    return classmethod(method)
diff --git a/dectate/config.py b/dectate/config.py
index 084e601..f37b0ee 100644
--- a/dectate/config.py
+++ b/dectate/config.py
@@ -1,4 +1,5 @@
 import abc
+import logging
 import sys
 import inspect
 from .error import (
@@ -47,22 +48,12 @@ class Configurable(object):
         self.extends = extends
         self.config = config
         # all action classes known
-        self._action_classes = set()
+        self._action_classes = {}
         # directives used with configurable
         self._directives = []
         # have we ever been committed
         self.committed = False
 
-    def register_action_class(self, action_class):
-        """Register an action class with this configurable.
-
-        Called during import time when the :meth:`App.directive` directive
-        is executed.
-
-        :param action_class: the :class:`dectate.Action` subclass to register.
-        """
-        self._action_classes.add(action_class)
-
     def register_directive(self, directive, obj):
         """Register a directive with this configurable.
 
@@ -75,6 +66,38 @@ class Configurable(object):
         """
         self._directives.append((directive, obj))
 
+    def _fixup_directive_names(self):
+        """Set up correct name for directives.
+        """
+        app_class = self.app_class
+        for name, method in app_class.get_directive_methods():
+            func = method.__func__
+            func.__name__ = name
+            # As of Python 3.5, the repr of bound methods uses
+            # __qualname__ instead of __name__.
+            # See http://bugs.python.org/issue21389#msg217566
+            if hasattr(func, '__qualname__'):
+                func.__qualname__ = type(app_class).__name__ + '.' + name
+
+    def get_action_classes(self):
+        """Get all action classes registered for this app.
+
+        This includes action classes registered for its base class.
+
+        :return: a dict with action class keys and name values.
+        """
+        result = {}
+        app_class = self.app_class
+        for name, method in app_class.get_directive_methods():
+            result[method.__func__.action_factory] = name
+
+        # add any action classes defined by base classes
+        for configurable in self.extends:
+            for action_class, name in configurable._action_classes.items():
+                if action_class not in result:
+                    result[action_class] = name
+        return result
+
     def setup(self):
         """Set up config object and action groups.
 
@@ -82,42 +105,14 @@ class Configurable(object):
 
         Takes inheritance of apps into account.
         """
-        # add any action classes defined by base classes
-        s = self._action_classes
-        for configurable in self.extends:
-            for action_class in configurable._action_classes:
-                if action_class not in s:
-                    s.add(action_class)
-
-        # we want to have use group_class for each true Action class
-        action_classes = set()
-        for action_class in s:
-            if not issubclass(action_class, Action):
-                continue
-            group_class = action_class.group_class
-            if group_class is None:
-                group_class = action_class
-            else:
-                if group_class.group_class is not None:
-                    raise ConfigError(
-                        "Cannot use group_class on another action class "
-                        "that uses group_class: %r" % action_class)
-                if 'config' in action_class.__dict__:
-                    raise ConfigError(
-                        "Cannot use config class attribute when you use "
-                        "group_class: %r" % action_class)
-                if 'before' in action_class.__dict__:
-                    raise ConfigError(
-                        "Cannot define before method when you use "
-                        "group_class: %r" % action_class)
-                if 'after' in action_class.__dict__:
-                    raise ConfigError(
-                        "Cannot define after method when you use "
-                        "group_class: %r" % action_class)
-            action_classes.add(group_class)
+        self._fixup_directive_names()
+        self._action_classes = self.get_action_classes()
+
+        grouped_action_classes = sort_action_classes(
+            group_action_classes(self._action_classes.keys()))
 
         # delete any old configuration in case we run this a second time
-        for action_class in sort_action_classes(action_classes):
+        for action_class in grouped_action_classes:
             self.delete_config(action_class)
 
         # now we create ActionGroup objects for each action class group
@@ -125,7 +120,7 @@ class Configurable(object):
         # and we track what config factories we've seen for consistency
         # checking
         self._factories_seen = {}
-        for action_class in sort_action_classes(action_classes):
+        for action_class in grouped_action_classes:
             self.setup_config(action_class)
             d[action_class] = ActionGroup(action_class,
                                           self.action_extends(action_class))
@@ -678,28 +673,23 @@ class Directive(object):
     When used as a decorator this tracks where in the source code
     the directive was used for the purposes of error reporting.
     """
-    def __init__(self, app_class, action_factory, args, kw,
-                 code_info, directive_name, logger):
+    def __init__(self, action_factory, code_info, app_class, args, kw):
         """
+        :param action_factory: function that constructs an action instance.
+        :code_info: a :class:`CodeInfo` instance describing where this
+          directive was invoked.
         :param app_class: the :class:`dectate.App` subclass that this
           directive is used on.
-        :param action_factory: function that constructs an action instance.
         :args: the positional arguments passed into the directive.
         :kw: the keyword arguments passed into the directive.
-        :code_info: a :class:`CodeInfo` instance describing where this
-          directive was invoked.
-        :directive_name: the name of this directive.
-        :logger: the logger object to use.
         """
+        self.action_factory = action_factory
+        self.code_info = code_info
         self.app_class = app_class
         self.configurable = app_class.dectate
-        self.action_factory = action_factory
         self.args = args
         self.kw = kw
-        self.code_info = code_info
-        self.directive_name = directive_name
         self.argument_info = (args, kw)
-        self.logger = logger
 
     def action(self):
         """Get the :class:`Action` instance represented by this directive.
@@ -739,8 +729,10 @@ class Directive(object):
         :obj: the function or class object to that this directive is used
           on.
         """
-        if self.logger is None:
-            return
+        directive_name = configurable._action_classes[self.action_factory]
+        logger = logging.getLogger('%s.%s' % (
+            configurable.app_class.logger_name,
+            directive_name))
 
         target_dotted_name = dotted_name(configurable.app_class)
         is_same = self.app_class is configurable.app_class
@@ -761,13 +753,13 @@ class Directive(object):
                  sorted(kw.items())])
 
         message = '@%s.%s(%s) on %s' % (
-            target_dotted_name, self.directive_name, arguments,
+            target_dotted_name, directive_name, arguments,
             func_dotted_name)
 
         if not is_same:
             message += ' (from %s)' % dotted_name(self.app_class)
 
-        self.logger.debug(message)
+        logger.debug(message)
 
 
 class DirectiveAbbreviation(object):
@@ -787,13 +779,11 @@ class DirectiveAbbreviation(object):
         combined_kw = directive.kw.copy()
         combined_kw.update(kw)
         return Directive(
-            app_class=directive.app_class,
             action_factory=directive.action_factory,
-            args=combined_args,
-            kw=combined_kw,
             code_info=code_info,
-            directive_name=directive.directive_name,
-            logger=directive.logger)
+            app_class=directive.app_class,
+            args=combined_args,
+            kw=combined_kw)
 
 
 def commit(*apps):
@@ -840,6 +830,41 @@ def sort_action_classes(action_classes):
     return topological_sort(action_classes, lambda c: c.depends)
 
 
+def group_action_classes(action_classes):
+    """Group action classes by ``group_class``.
+
+    :param action_classes: iterable of action classes
+    :return: set of action classes grouped together.
+    """
+    # we want to have use group_class for each true Action class
+    result = set()
+    for action_class in action_classes:
+        if not issubclass(action_class, Action):
+            continue
+        group_class = action_class.group_class
+        if group_class is None:
+            group_class = action_class
+        else:
+            if group_class.group_class is not None:
+                raise ConfigError(
+                    "Cannot use group_class on another action class "
+                    "that uses group_class: %r" % action_class)
+            if 'config' in action_class.__dict__:
+                raise ConfigError(
+                    "Cannot use config class attribute when you use "
+                    "group_class: %r" % action_class)
+            if 'before' in action_class.__dict__:
+                raise ConfigError(
+                    "Cannot define before method when you use "
+                    "group_class: %r" % action_class)
+            if 'after' in action_class.__dict__:
+                raise ConfigError(
+                    "Cannot define after method when you use "
+                    "group_class: %r" % action_class)
+        result.add(group_class)
+    return result
+
+
 def expand_actions(actions):
     """Expand any :class:`Composite` instances into :class:`Action` instances.
 
diff --git a/doc/api.rst b/doc/api.rst
index 510c7bc..ffcb4c4 100644
--- a/doc/api.rst
+++ b/doc/api.rst
@@ -20,6 +20,8 @@ API
   :inherited-members:
   :members:
 
+.. autofunction:: directive
+
 .. autofunction:: query_tool
 
 .. autofunction:: query_app
diff --git a/doc/usage.rst b/doc/usage.rst
index 29ecb71..e1773ca 100644
--- a/doc/usage.rst
+++ b/doc/usage.rst
@@ -100,29 +100,15 @@ Here are some features of Dectate:
   provides the infrastructure to easily construct command-line tools
   for querying configuration.
 
-App classes
------------
+Actions
+-------
 
-Configuration in Dectate is associated with special *classes* which
-derive from :class:`dectate.App`:
+In Dectate, the simple `plugins` example above looks like this:
 
 .. testcode::
 
   import dectate
 
-  class MyApp(dectate.App):
-      pass
-
-Creating a directive
---------------------
-
-We can now use the :meth:`dectate.App.directive` decorator to declare
-a *directive* which executes a special configuration action. Let's
-replicate the simple `plugins` example above using Dectate:
-
-.. testcode::
-
-  @MyApp.directive('plugin')
   class PluginAction(dectate.Action):
       config = {
          'plugins': dict
@@ -136,38 +122,53 @@ replicate the simple `plugins` example above using Dectate:
       def perform(self, obj, plugins):
           plugins[self.name] = obj
 
+We have formulated a configuration action that affects a ``plugins``
+dict.
+
+App classes
+-----------
+
+Configuration in Dectate is associated with special *classes* which
+derive from :class:`dectate.App`. We also associate the action with
+it as a directive:
+
+.. testcode::
+
+  class PluginApp(dectate.App):
+      plugin = dectate.directive(PluginAction)
+
 Let's use it now:
 
 .. testcode::
 
-  @MyApp.plugin('a')
+  @PluginApp.plugin('a')
   def f():
       pass # do something interesting
 
-  @MyApp.plugin('b')
+  @PluginApp.plugin('b')
   def g():
       pass # something else interesting
 
-We have registered the function ``f`` on ``MyApp``. The ``name``
+We have registered the function ``f`` on ``PluginApp``. The ``name``
 argument is ``'a'``. We've registered ``g`` under ``'b'``.
 
-We can now commit the configuration for ``MyApp``:
+We can now commit the configuration for ``PluginApp``:
 
 .. testcode::
 
-  dectate.commit(MyApp)
+  dectate.commit(PluginApp)
 
 Once the commit has successfully completed, we can take a look at the
 configuration:
 
 .. doctest::
 
-  >>> sorted(MyApp.config.plugins.items())
+  >>> sorted(PluginApp.config.plugins.items())
   [('a', <function f at ...>), ('b', <function g at ...>)]
 
 What are the changes between this and the simple plugins example?
 
-The main difference is that ``plugin`` decorator is associated with a
+The main difference is that the ``plugin`` decorator is associated with a
 class and so is the resulting configuration, which gets stored as the
 ``plugins`` attribute of :attr:`dectate.App.config`. The other
 difference is that we provide an ``identifier`` method in the action
@@ -177,20 +178,20 @@ definition. These differences support configuration *reuse*,
 Reuse
 ~~~~~
 
-You can reuse configuration by simply subclassing ``MyApp``:
+You can reuse configuration by simply subclassing ``PluginApp``:
 
 .. testcode::
 
-  class SubApp(MyApp):
+  class SubApp(PluginApp):
      pass
 
 We commit both classes:
 
 .. testcode::
 
-  dectate.commit(MyApp, SubApp)
+  dectate.commit(PluginApp, SubApp)
 
-``SubClass`` now contains all the configuration declared for ``MyApp``:
+``SubClass`` now contains all the configuration declared for ``PluginApp``:
 
   >>> sorted(SubApp.config.plugins.items())
   [('a', <function f at ...>), ('b', <function g at ...>)]
@@ -205,7 +206,7 @@ Consider this example:
 
 .. testcode::
 
-   class ConflictingApp(MyApp):
+   class ConflictingApp(PluginApp):
        pass
 
    @ConflictingApp.plugin('foo')
@@ -256,7 +257,7 @@ additional configuration actions:
   def h():
       pass # do something interesting
 
-  dectate.commit(MyApp, SubApp)
+  dectate.commit(PluginApp, SubApp)
 
 ``SubApp`` now has the additional plugin ``c``:
 
@@ -265,11 +266,11 @@ additional configuration actions:
   >>> sorted(SubApp.config.plugins.items())
   [('a', <function f at ...>), ('b', <function g at ...>), ('c', <function h at ...>)]
 
-But ``MyApp`` is unaffected:
+But ``PluginApp`` is unaffected:
 
 .. doctest::
 
-  >>> sorted(MyApp.config.plugins.items())
+  >>> sorted(PluginApp.config.plugins.items())
   [('a', <function f at ...>), ('b', <function g at ...>)]
 
 Overrides
@@ -284,7 +285,7 @@ this in ``SubApp`` by simply reusing the same ``name``:
   def x():
       pass
 
-  dectate.commit(MyApp, SubApp)
+  dectate.commit(PluginApp, SubApp)
 
 In ``SubApp`` we now have changed the configuration for ``a`` to
 register the function ``x`` instead of ``f``. If we had done this for
@@ -296,16 +297,16 @@ lets you override configuration instead:
   >>> sorted(SubApp.config.plugins.items())
   [('a', <function x at ...>), ('b', <function g at ...>), ('c', <function h at ...>)]
 
-But ``MyApp`` still uses ``f``:
+But ``PluginApp`` still uses ``f``:
 
-  >>> sorted(MyApp.config.plugins.items())
+  >>> sorted(PluginApp.config.plugins.items())
   [('a', <function f at ...>), ('b', <function g at ...>)]
 
 Isolation
 ~~~~~~~~~
 
 We have already seen in the inheritance and override examples that
-``MyApp`` is isolated from configuration extension and overrides done
+``PluginApp`` is isolated from configuration extension and overrides done
 for ``SubApp``. We can in fact entirely isolate configuration from
 each other.
 
@@ -314,10 +315,6 @@ from everything before:
 
 .. testcode::
 
-  class BaseApp(dectate.App):
-      pass
-
-  @BaseApp.directive('plugin')
   class PluginAction2(dectate.Action):
       config = {
          'plugins': dict
@@ -331,6 +328,9 @@ from everything before:
       def perform(self, obj, plugins):
           plugins[self.name] = obj
 
+  class BaseApp(dectate.App):
+      plugin = dectate.directive(PluginAction2)
+
 We don't set up any configuration for ``BaseApp``; it's intended to be
 part of our framework. Now we create two subclasses:
 
@@ -369,12 +369,11 @@ This won't affect ``TwoApp`` in any way:
 ``OneApp`` and ``TwoApp`` are isolated, so configurations are
 independent, and cannot conflict or override.
 
-The Anatomy of a Directive
---------------------------
+The Anatomy of an Action
+------------------------
 
-Let's consider the directive registration again in detail::
+Let's consider the plugin action in detail::
 
-  @MyApp.directive('plugin')
   class PluginAction(dectate.Action):
       config = {
          'plugins': dict
@@ -390,11 +389,8 @@ Let's consider the directive registration again in detail::
 
 What is going on here?
 
-* We create a new directive called ``plugin`` on ``MyApp``. It also
-  exists for its subclasses.
-
-* The directive is implemented with a custom class called
-  ``PluginAction`` that inherits from :class:`dectate.Action`.
+* We implement a custom class called ``PluginAction`` that inherits
+  from :class:`dectate.Action`.
 
 * ``config`` (:attr:`dectate.Action.config`) specifies that this
   directive has a configuration effect on ``plugins``. We declare that
@@ -421,14 +417,20 @@ What is going on here?
   this case we store ``obj`` under the key ``self.name`` in the
   ``plugins`` dict.
 
+We then associate the action with a class as a directive::
+
+  class PluginApp(dectate.App):
+      plugin = dectate.directive(PluginAction)
+
 Once we have declared the directive for our framework we can tell
 programmers to use it.
 
 Directives have absolutely no effect until *commit* is called, which
 we do with ``dectate.commit``. This performs the actions and we can
-then find the result ``MyApp.config`` (:attr:`dectate.App.config`).
+then find the result ``PluginApp.config``
+(:attr:`dectate.App.config`).
 
-The results are in ``MyApp.config.plugins`` as we set this up with
+The results are in ``PluginApp.config.plugins`` as we set this up with
 ``config`` in our ``PluginAction``.
 
 Depends
@@ -440,15 +442,11 @@ of directive depends on the former. You can make sure this happens by
 using the ``depends`` (:attr:`dectate.Action.depends`) class
 attribute.
 
-First we set up a ``foo`` directive that registers into a ``foos``
+First we set up a ``FooAction`` that registers into a ``foos``
 dict:
 
 .. testcode::
 
-  class DependsApp(dectate.App):
-      pass
-
-  @DependsApp.directive('foo')
   class FooAction(dectate.Action):
       config = {
          'foos': dict
@@ -462,13 +460,12 @@ dict:
       def perform(self, obj, foos):
           foos[self.name] = obj
 
-Now we create a ``bar`` directive that depends on ``FooDirective`` and
-uses information in the ``foos`` dict:
+Now we create a ``BarAction`` directive that depends on ``FooAction``
+and uses information in the ``foos`` dict:
 
 .. testcode::
 
-   @DependsApp.directive('bar')
-   class BarAction(dectate.Action):
+  class BarAction(dectate.Action):
       depends = [FooAction]
 
       config = {
@@ -485,8 +482,19 @@ uses information in the ``foos`` dict:
           in_foo = self.name in foos
           bars.append((self.name, obj, in_foo))
 
-We have now ensured that ``BarAction`` actions are performed after
-``FooAction`` action, no matter what order we use them:
+In order to use them we need to hook up the actions as directives
+onto an app class:
+
+.. testcode::
+
+  class DependsApp(dectate.App):
+      foo = dectate.directive(FooAction)
+      bar = dectate.directive(BarAction)
+
+
+Using ``depends`` we have ensured that ``BarAction`` actions are
+performed after ``FooAction`` action, no matter what order we use
+them:
 
 .. testcode::
 
@@ -515,11 +523,12 @@ We expect ``in_foo`` to be ``True`` for ``a`` but to be ``False`` for
 config dependencies
 -------------------
 
-In the example above, the items in ``bars`` depend on the items in ``foos``
-and we've implemented this dependency in the ``perform`` of ``BarDirective``.
+In the example above, the items in ``bars`` depend on the items in
+``foos`` and we've implemented this dependency in the ``perform`` of
+``BarAction``.
 
-We can instead make the configuration object for the ``BarDirective``
-depend on ``foos``. This way ``BarDirective`` does not need to know
+We can instead make the configuration object for the ``BarAction``
+depend on ``foos``. This way ``BarAction`` does not need to know
 about ``foos``. You can declare a dependency between config objects
 with the ``factory_arguments`` attribute of the config factory. Any
 config object that is created in earlier dependencies of this action,
@@ -527,15 +536,11 @@ or in the action itself, can be listed in ``factory_arguments``. The
 key and value in ``factory_arguments`` have to match the key and value
 in ``config`` of that earlier action.
 
-First we create an app with a ``FooAction`` that sets up a ``foos``
-config item as before:
+First we create a ``FooAction`` that sets up a ``foos`` config item as
+before:
 
 .. testcode::
 
-  class ConfigDependsApp(dectate.App):
-      pass
-
-  @ConfigDependsApp.directive('foo')
   class FooAction(dectate.Action):
       config = {
          'foos': dict
@@ -572,7 +577,6 @@ We create a ``BarAction`` that depends on the ``FooAction`` (so that
 
 .. testcode::
 
-   @ConfigDependsApp.directive('bar')
    class BarAction(dectate.Action):
       depends = [FooAction]
 
@@ -589,6 +593,15 @@ We create a ``BarAction`` that depends on the ``FooAction`` (so that
       def perform(self, obj, bar):
           bar.add(self.name, obj)
 
+
+And we set them up as directives:
+
+.. testcode::
+
+  class ConfigDependsApp(dectate.App):
+      foo = dectate.directive(FooAction)
+      bar = dectate.directive(BarAction)
+
 When we use our directives:
 
 .. testcode::
@@ -625,7 +638,6 @@ so on by setting the special ``app_class_arg`` class attribute:
 
 .. testcode::
 
-  @MyApp.directive('plugin_with_app_class')
   class PluginAction(dectate.Action):
       config = {
          'plugins': dict
@@ -642,6 +654,9 @@ so on by setting the special ``app_class_arg`` class attribute:
           plugins[self.name] = obj
           app_class.touched = True
 
+  class MyApp(dectate.App):
+     plugin_with_app_class = dectate.directive(PluginAction)
+
 When we now perform this directive:
 
 .. testcode::
@@ -672,10 +687,6 @@ using ``before`` (:meth:`dectate.Action.before`) and ``after``
 
 .. testcode::
 
-  class BeforeAfterApp(dectate.App):
-      pass
-
-  @BeforeAfterApp.directive('foo')
   class FooAction(dectate.Action):
       config = {
          'foos': list
@@ -697,6 +708,9 @@ using ``before`` (:meth:`dectate.Action.before`) and ``after``
       def perform(self, obj, foos):
           foos.append((self.name, obj))
 
+  class BeforeAfterApp(dectate.App):
+      foo = dectate.directive(FooAction)
+
   @BeforeAfterApp.foo('a')
   def f():
       pass
@@ -725,10 +739,6 @@ share their ``config`` and their ``before`` and ``after`` methods.
 
 .. testcode::
 
-  class GroupApp(dectate.App):
-      pass
-
-  @GroupApp.directive('foo')
   class FooAction(dectate.Action):
       config = {
          'foos': list
@@ -742,49 +752,49 @@ share their ``config`` and their ``before`` and ``after`` methods.
       def perform(self, obj, foos):
           foos.append((self.name, obj))
 
-We now create a ``BarDirective`` that groups with ``FooAction``:
+We now create a ``BarAction`` that groups with ``FooAction``:
 
 .. testcode::
 
-  @GroupApp.directive('bar')
   class BarAction(dectate.Action):
-     group_class = FooAction
+      group_class = FooAction
 
-     def __init__(self, name):
-         self.name = name
+      def __init__(self, name):
+          self.name = name
 
-     def identifier(self, foos):
-         return self.name
+      def identifier(self, foos):
+          return self.name
 
-     def perform(self, obj, foos):
-         foos.append((self.name, obj))
+      def perform(self, obj, foos):
+          foos.append((self.name, obj))
+
+  class GroupApp(dectate.App):
+      foo = dectate.directive(FooAction)
+      bar = dectate.directive(BarAction)
 
 It reuses the ``config`` from ``FooAction``. This means that ``foo``
 and ``bar`` can be in conflict:
 
 .. testcode::
 
-  class GroupConflictApp(GroupApp):
-      pass
-
-  @GroupConflictApp.foo('a')
+  @GroupApp.foo('a')
   def f():
       pass
 
-  @GroupConflictApp.bar('a')
+  @GroupApp.bar('a')
   def g():
       pass
 
 .. doctest::
 
-  >>> dectate.commit(GroupConflictApp)
+  >>> dectate.commit(GroupApp)
   Traceback (most recent call last):
     ...
   ConflictError: Conflict between:
     File "...", line 4
-      @GroupConflictApp.foo('a')
+      @GroupApp.foo('a')
     File "...", line 8
-      @GroupConflictApp.bar('a')
+      @GroupApp.bar('a')
 
 Additional discriminators
 -------------------------
@@ -795,10 +805,6 @@ all at once. You can take care of this with the ``discriminators``
 
 .. testcode::
 
-  class DiscriminatorsApp(dectate.App):
-      pass
-
-  @DiscriminatorsApp.directive('foo')
   class FooAction(dectate.Action):
       config = {
          'foos': dict
@@ -816,6 +822,10 @@ all at once. You can take care of this with the ``discriminators``
       def perform(self, obj, foos):
           foos[self.name] = obj
 
+
+  class DiscriminatorsApp(dectate.App):
+      foo = dectate.directive(FooAction)
+
 An action now conflicts with an action of the same name *and* with
 any action that is in the ``extra`` list:
 
@@ -849,15 +859,11 @@ Composite actions
 When you can define an action entirely in terms of other actions, you
 can subclass :class:`dectate.Composite`.
 
-First we define a normal ``sub`` directive to use in the composite action
+First we define a normal ``SubAction`` to use in the composite action
 later:
 
 .. testcode::
 
-  class CompositeApp(dectate.App):
-      pass
-
-  @CompositeApp.directive('sub')
   class SubAction(dectate.Action):
       config = {
           'my': list
@@ -878,7 +884,6 @@ uses ``SubAction`` in an ``actions``
 
 .. testcode::
 
-  @CompositeApp.directive('composite')
   class CompositeAction(dectate.Composite):
       def __init__(self, names):
           self.names = names
@@ -886,6 +891,14 @@ uses ``SubAction`` in an ``actions``
       def actions(self, obj):
           return [(SubAction(name), obj) for name in self.names]
 
+  class CompositeApp(dectate.App):
+      _sub = dectate.directive(SubAction)
+      composite = dectate.directive(CompositeAction)
+
+Note that even though ``_sub`` is not intended to be a public part of
+the API we still need to include it in our :class:`dectate.App`
+subclass, as Dectate does need to know it exists.
+
 We can now use it:
 
 .. testcode::
@@ -911,11 +924,7 @@ use the ``with`` statement to do so with less repetition:
 
 .. testcode::
 
-  class WithApp(dectate.App):
-      pass
-
-  @WithApp.directive('foo')
-  class SubAction(dectate.Action):
+  class FooAction(dectate.Action):
       config = {
           'my': list
       }
@@ -930,6 +939,10 @@ use the ``with`` statement to do so with less repetition:
       def perform(self, obj, my):
           my.append((self.a, self.b, obj))
 
+
+  class WithApp(dectate.App):
+      foo = dectate.directive(FooAction)
+
 Instead of this:
 
 .. testcode::
@@ -1020,7 +1033,7 @@ querying
 Dectate keeps a database of committed actions that can be queried by
 using :class:`dectate.Query`.
 
-Here is an example of a query for all the plugin actions on ``MyApp``:
+Here is an example of a query for all the plugin actions on ``PluginApp``:
 
 .. testcode::
 
@@ -1031,7 +1044,7 @@ We can now run the query:
 .. doctest::
   :options: +NORMALIZE_WHITESPACE
 
-  >>> list(q(MyApp))
+  >>> list(q(PluginApp))
   [(<PluginAction ...>, <function f ...>),
    (<PluginAction ...>, <function g ...>)]
 
@@ -1039,7 +1052,7 @@ We can also filter the query for attributes of the action:
 
 .. doctest::
 
-  >>> list(q.filter(name='a')(MyApp))
+  >>> list(q.filter(name='a')(PluginApp))
   [(<PluginAction object ...>, <function f ...>)]
 
 Sometimes the attribute on the action is not the same as the name you


Test patch:
diff --git a/dectate/tests/fixtures/anapp.py b/dectate/tests/fixtures/anapp.py
index 5d71b05..30566cc 100644
--- a/dectate/tests/fixtures/anapp.py
+++ b/dectate/tests/fixtures/anapp.py
@@ -1,9 +1,22 @@
 import dectate
 
 
+class FooAction(dectate.Action):
+    def __init__(self, name):
+        self.name = name
+
+    def identifier(self):
+        return self.name
+
+    def perform(self, obj):
+        pass
+
+
 class AnApp(dectate.App):
     known = "definitely not a directive"
 
+    foo = dectate.directive(FooAction)
+
 
 def other():
     pass
@@ -11,15 +24,3 @@ def other():
 
 class OtherClass(object):
     pass
-
-
-@AnApp.directive('foo')
-class FooAction(dectate.Action):
-    def __init__(self, name):
-        self.name = name
-
-    def identifier(self):
-        return self.name
-
-    def perform(self, obj):
-        pass
diff --git a/dectate/tests/test_directive.py b/dectate/tests/test_directive.py
index e538179..9004d2e 100644
--- a/dectate/tests/test_directive.py
+++ b/dectate/tests/test_directive.py
@@ -1,4 +1,4 @@
-from dectate.app import App
+from dectate.app import App, directive
 from dectate.config import commit, Action, Composite
 from dectate.error import ConflictError, ConfigError
 
@@ -6,10 +6,6 @@ import pytest
 
 
 def test_simple():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -24,6 +20,9 @@ def test_simple():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -33,11 +32,34 @@ def test_simple():
     assert MyApp.config.my == [('hello', f)]
 
 
-def test_commit_method():
+def test_decorator():
+
     class MyApp(App):
+        @directive
+        class foo(Action):
+            config = {
+                'my': list
+            }
+
+            def __init__(self, message):
+                self.message = message
+
+            def identifier(self, my):
+                return self.message
+
+            def perform(self, obj, my):
+                my.append((self.message, obj))
+
+    @MyApp.foo('hello')
+    def f():
         pass
 
-    @MyApp.directive('foo')
+    commit(MyApp)
+
+    assert MyApp.config.my == [('hello', f)]
+
+
+def test_commit_method():
     class MyDirective(Action):
         config = {
             'my': list
@@ -52,6 +74,9 @@ def test_commit_method():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -63,10 +88,6 @@ def test_commit_method():
 
 
 def test_conflict_same_directive():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -81,6 +102,9 @@ def test_conflict_same_directive():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -97,13 +121,6 @@ def test_app_inherit():
     class Registry(object):
         pass
 
-    class MyApp(App):
-        pass
-
-    class SubApp(MyApp):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': Registry
@@ -119,6 +136,12 @@ def test_app_inherit():
             my.message = self.message
             my.obj = obj
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
+    class SubApp(MyApp):
+        pass
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -135,13 +158,6 @@ def test_app_override():
     class Registry(object):
         pass
 
-    class MyApp(App):
-        pass
-
-    class SubApp(MyApp):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': Registry
@@ -157,6 +173,12 @@ def test_app_override():
             my.message = self.message
             my.obj = obj
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
+    class SubApp(MyApp):
+        pass
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -174,10 +196,6 @@ def test_app_override():
 
 
 def test_different_group_no_conflict():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -192,7 +210,6 @@ def test_different_group_no_conflict():
         def perform(self, obj, foo):
             foo.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         config = {
             'bar': list
@@ -207,6 +224,10 @@ def test_different_group_no_conflict():
         def perform(self, obj, bar):
             bar.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -222,10 +243,6 @@ def test_different_group_no_conflict():
 
 
 def test_same_group_conflict():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -240,7 +257,6 @@ def test_same_group_conflict():
         def perform(self, obj, foo):
             foo.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         # should now conflict
         group_class = FooDirective
@@ -254,6 +270,10 @@ def test_same_group_conflict():
         def perform(self, obj, foo):
             foo.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -267,10 +287,6 @@ def test_same_group_conflict():
 
 
 def test_discriminator_conflict():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -289,6 +305,9 @@ def test_discriminator_conflict():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     @MyApp.foo('f', ['a'])
     def f():
         pass
@@ -302,10 +321,6 @@ def test_discriminator_conflict():
 
 
 def test_discriminator_same_group_conflict():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -324,10 +339,13 @@ def test_discriminator_same_group_conflict():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(FooDirective):
         group_class = FooDirective
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     @MyApp.foo('f', ['a'])
     def f():
         pass
@@ -341,10 +359,6 @@ def test_discriminator_same_group_conflict():
 
 
 def test_discriminator_no_conflict():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -363,6 +377,9 @@ def test_discriminator_no_conflict():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     @MyApp.foo('f', ['a'])
     def f():
         pass
@@ -377,10 +394,6 @@ def test_discriminator_no_conflict():
 
 
 def test_discriminator_different_group_no_conflict():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -399,11 +412,14 @@ def test_discriminator_different_group_no_conflict():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(FooDirective):
         # will have its own group key so in a different group
         depends = [FooDirective]
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     @MyApp.foo('f', ['a'])
     def f():
         pass
@@ -418,10 +434,6 @@ def test_discriminator_different_group_no_conflict():
 
 
 def test_depends():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -436,7 +448,6 @@ def test_depends():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         depends = [FooDirective]
 
@@ -453,6 +464,10 @@ def test_depends():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     @MyApp.bar('a')
     def g():
         pass
@@ -468,10 +483,6 @@ def test_depends():
 
 
 def test_composite():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('sub')
     class SubDirective(Action):
         config = {
             'my': list
@@ -486,7 +497,6 @@ def test_composite():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('composite')
     class CompositeDirective(Composite):
         def __init__(self, messages):
             self.messages = messages
@@ -494,6 +504,10 @@ def test_composite():
         def actions(self, obj):
             return [(SubDirective(message), obj) for message in self.messages]
 
+    class MyApp(App):
+        _sub = directive(SubDirective)
+        composite = directive(CompositeDirective)
+
     @MyApp.composite(['a', 'b', 'c'])
     def f():
         pass
@@ -504,10 +518,6 @@ def test_composite():
 
 
 def test_composite_change_object():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('sub')
     class SubDirective(Action):
         config = {
             'my': list
@@ -525,7 +535,6 @@ def test_composite_change_object():
     def other():
         pass
 
-    @MyApp.directive('composite')
     class CompositeDirective(Composite):
         def __init__(self, messages):
             self.messages = messages
@@ -534,6 +543,10 @@ def test_composite_change_object():
             return [(SubDirective(message),
                      other) for message in self.messages]
 
+    class MyApp(App):
+        _sub = directive(SubDirective)
+        composite = directive(CompositeDirective)
+
     @MyApp.composite(['a', 'b', 'c'])
     def f():
         pass
@@ -544,10 +557,6 @@ def test_composite_change_object():
 
 
 def test_composite_private_sub():
-    class MyApp(App):
-        pass
-
-    @MyApp.private_action_class
     class SubDirective(Action):
         config = {
             'my': list
@@ -562,7 +571,6 @@ def test_composite_private_sub():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('composite')
     class CompositeDirective(Composite):
         def __init__(self, messages):
             self.messages = messages
@@ -570,6 +578,11 @@ def test_composite_private_sub():
         def actions(self, obj):
             return [(SubDirective(message), obj) for message in self.messages]
 
+    class MyApp(App):
+        # mark sub as private by using the underscore
+        _sub = directive(SubDirective)
+        composite = directive(CompositeDirective)
+
     @MyApp.composite(['a', 'b', 'c'])
     def f():
         pass
@@ -580,10 +593,6 @@ def test_composite_private_sub():
 
 
 def test_composite_private_composite():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('sub')
     class SubDirective(Action):
         config = {
             'my': list
@@ -598,7 +607,6 @@ def test_composite_private_composite():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.private_action_class
     class CompositeDirective(Composite):
         def __init__(self, messages):
             self.messages = messages
@@ -606,6 +614,10 @@ def test_composite_private_composite():
         def actions(self, obj):
             return [(SubDirective(message), obj) for message in self.messages]
 
+    class MyApp(App):
+        sub = directive(SubDirective)
+        _composite = directive(CompositeDirective)
+
     @MyApp.sub('a')
     def f():
         pass
@@ -616,10 +628,6 @@ def test_composite_private_composite():
 
 
 def test_nested_composite():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('sub')
     class SubDirective(Action):
         config = {
             'my': list
@@ -634,7 +642,6 @@ def test_nested_composite():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('subcomposite')
     class SubCompositeDirective(Composite):
         def __init__(self, message):
             self.message = message
@@ -643,7 +650,6 @@ def test_nested_composite():
             yield SubDirective(self.message + '_0'), obj
             yield SubDirective(self.message + '_1'), obj
 
-    @MyApp.directive('composite')
     class CompositeDirective(Composite):
         def __init__(self, messages):
             self.messages = messages
@@ -652,6 +658,11 @@ def test_nested_composite():
             return [(SubCompositeDirective(message), obj)
                     for message in self.messages]
 
+    class MyApp(App):
+        sub = directive(SubDirective)
+        subcomposite = directive(SubCompositeDirective)
+        composite = directive(CompositeDirective)
+
     @MyApp.composite(['a', 'b', 'c'])
     def f():
         pass
@@ -666,10 +677,6 @@ def test_nested_composite():
 
 
 def test_with_statement_kw():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -688,6 +695,9 @@ def test_with_statement_kw():
     class Dummy(object):
         pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     with MyApp.foo(model=Dummy) as foo:
 
         @foo(name='a')
@@ -707,10 +717,6 @@ def test_with_statement_kw():
 
 
 def test_with_statement_args():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -726,6 +732,9 @@ def test_with_statement_args():
         def perform(self, obj, my):
             my.append((self.model, self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     class Dummy(object):
         pass
 
@@ -757,10 +766,6 @@ def test_before():
             assert self.before
             self.l.append((name, obj))
 
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': Registry
@@ -779,6 +784,9 @@ def test_before():
         def before(my):
             my.before = True
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     @MyApp.foo(name='hello')
     def f():
         pass
@@ -801,10 +809,6 @@ def test_before_without_use():
             assert self.before
             self.l.append((name, obj))
 
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': Registry
@@ -823,6 +827,9 @@ def test_before_without_use():
         def before(my):
             my.before = True
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     commit(MyApp)
 
     assert MyApp.config.my.before
@@ -839,10 +846,6 @@ def test_before_group():
             assert self.before
             self.l.append((name, obj))
 
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': Registry
@@ -861,7 +864,6 @@ def test_before_group():
         def before(my):
             my.before = True
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         group_class = FooDirective
 
@@ -874,6 +876,10 @@ def test_before_group():
         def perform(self, obj, my):
             pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     @MyApp.bar(name='bye')
     def f():
         pass
@@ -891,10 +897,6 @@ def test_before_group():
 
 
 def test_config_group():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -909,7 +911,6 @@ def test_config_group():
         def perform(self, obj, my):
             my.append((self.name, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         group_class = FooDirective
 
@@ -922,6 +923,10 @@ def test_config_group():
         def perform(self, obj, my):
             my.append((self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     @MyApp.bar(name='bye')
     def f():
         pass
@@ -947,10 +952,6 @@ def test_before_group_without_use():
             assert self.before
             self.l.append((name, obj))
 
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': Registry
@@ -969,7 +970,6 @@ def test_before_group_without_use():
         def before(my):
             my.before = True
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         group_class = FooDirective
 
@@ -982,6 +982,10 @@ def test_before_group_without_use():
         def perform(self, obj):
             pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     commit(MyApp)
 
     assert MyApp.config.my.before
@@ -998,10 +1002,6 @@ def test_after():
             assert not self.after
             self.l.append((name, obj))
 
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': Registry
@@ -1020,6 +1020,9 @@ def test_after():
         def after(my):
             my.after = True
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     @MyApp.foo(name='hello')
     def f():
         pass
@@ -1042,10 +1045,6 @@ def test_after_without_use():
             assert not self.after
             self.l.append((name, obj))
 
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': Registry
@@ -1064,6 +1063,9 @@ def test_after_without_use():
         def after(my):
             my.after = True
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     commit(MyApp)
 
     assert MyApp.config.my.after
@@ -1071,10 +1073,6 @@ def test_after_without_use():
 
 
 def test_action_loop_should_conflict():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -1089,6 +1087,9 @@ def test_action_loop_should_conflict():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     for i in range(2):
         @MyApp.foo('hello')
         def f():
@@ -1099,12 +1100,8 @@ def test_action_loop_should_conflict():
 
 
 def test_action_init_only_during_commit():
-    class MyApp(App):
-        pass
-
     init_called = []
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -1120,6 +1117,9 @@ def test_action_init_only_during_commit():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1132,10 +1132,6 @@ def test_action_init_only_during_commit():
 
 
 def test_registry_should_exist_even_without_directive_use():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -1150,19 +1146,15 @@ def test_registry_should_exist_even_without_directive_use():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     commit(MyApp)
 
     assert MyApp.config.my == []
 
 
 def test_registry_should_exist_even_without_directive_use_subclass():
-    class MyApp(App):
-        pass
-
-    class SubApp(MyApp):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -1177,6 +1169,12 @@ def test_registry_should_exist_even_without_directive_use_subclass():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
+    class SubApp(MyApp):
+        pass
+
     commit(MyApp, SubApp)
 
     assert MyApp.config.my == []
@@ -1184,10 +1182,6 @@ def test_registry_should_exist_even_without_directive_use_subclass():
 
 
 def test_rerun_commit():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -1202,6 +1196,9 @@ def test_rerun_commit():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1215,10 +1212,6 @@ def test_rerun_commit():
 
 
 def test_rerun_commit_add_directive():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -1233,6 +1226,9 @@ def test_rerun_commit_add_directive():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1250,13 +1246,6 @@ def test_rerun_commit_add_directive():
 
 
 def test_order_subclass():
-    class MyApp(App):
-        pass
-
-    class SubApp(MyApp):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -1271,6 +1260,12 @@ def test_order_subclass():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
+    class SubApp(MyApp):
+        pass
+
     @SubApp.foo('c')
     def h():
         pass
@@ -1289,9 +1284,6 @@ def test_order_subclass():
 
 
 def test_registry_single_factory_argument():
-    class MyApp(App):
-        pass
-
     class Other(object):
         factory_arguments = {
             'my': list
@@ -1300,7 +1292,6 @@ def test_registry_single_factory_argument():
         def __init__(self, my):
             self.my = my
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list,
@@ -1316,6 +1307,9 @@ def test_registry_single_factory_argument():
         def perform(self, obj, my, other):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1326,9 +1320,6 @@ def test_registry_single_factory_argument():
 
 
 def test_registry_factory_argument_introduces_new_registry():
-    class MyApp(App):
-        pass
-
     class Other(object):
         factory_arguments = {
             'my': list
@@ -1337,7 +1328,6 @@ def test_registry_factory_argument_introduces_new_registry():
         def __init__(self, my):
             self.my = my
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'other': Other
@@ -1352,6 +1342,9 @@ def test_registry_factory_argument_introduces_new_registry():
         def perform(self, obj, other):
             other.my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1363,12 +1356,6 @@ def test_registry_factory_argument_introduces_new_registry():
 
 
 def test_registry_factory_argument_introduces_new_registry_subclass():
-    class MyApp(App):
-        pass
-
-    class SubApp(MyApp):
-        pass
-
     class IsUsedElsewhere(object):
         poked = False
 
@@ -1380,7 +1367,6 @@ def test_registry_factory_argument_introduces_new_registry_subclass():
         def __init__(self, my):
             self.my = my
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'other': Other
@@ -1396,6 +1382,12 @@ def test_registry_factory_argument_introduces_new_registry_subclass():
             assert not other.my.poked
             other.my.poked = True
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
+    class SubApp(MyApp):
+        pass
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1409,9 +1401,6 @@ def test_registry_factory_argument_introduces_new_registry_subclass():
 
 
 def test_registry_multiple_factory_arguments():
-    class MyApp(App):
-        pass
-
     class Other(object):
         factory_arguments = {
             'my': list,
@@ -1422,7 +1411,6 @@ def test_registry_multiple_factory_arguments():
             self.my = my
             self.my2 = my2
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list,
@@ -1440,6 +1428,9 @@ def test_registry_multiple_factory_arguments():
             my.append((self.message, obj))
             my2.append('blah')
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1451,9 +1442,6 @@ def test_registry_multiple_factory_arguments():
 
 
 def test_registry_factory_arguments_depends():
-    class MyApp(App):
-        pass
-
     class Other(object):
         factory_arguments = {
             'my': list
@@ -1462,7 +1450,6 @@ def test_registry_factory_arguments_depends():
         def __init__(self, my):
             self.my = my
 
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'my': list
@@ -1477,7 +1464,6 @@ def test_registry_factory_arguments_depends():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         config = {
             'other': Other
@@ -1494,6 +1480,10 @@ def test_registry_factory_arguments_depends():
         def perform(self, obj, other):
             pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1504,9 +1494,6 @@ def test_registry_factory_arguments_depends():
 
 
 def test_registry_factory_arguments_depends_complex():
-    class MyApp(App):
-        pass
-
     class Registry(object):
         pass
 
@@ -1518,22 +1505,24 @@ def test_registry_factory_arguments_depends_complex():
         def __init__(self, registry):
             self.registry = registry
 
-    @MyApp.directive('setting')
     class SettingAction(Action):
         config = {'registry': Registry}
 
-    @MyApp.directive('predicate')
     class PredicateAction(Action):
         config = {'predicate_registry': PredicateRegistry}
 
         depends = [SettingAction]
 
-    @MyApp.directive('view')
     class ViewAction(Action):
         config = {'registry': Registry}
 
         depends = [PredicateAction]
 
+    class MyApp(App):
+        setting = directive(SettingAction)
+        predicate = directive(PredicateAction)
+        view = directive(ViewAction)
+
     commit(MyApp)
 
     assert MyApp.config.registry is MyApp.config.predicate_registry.registry
@@ -1551,11 +1540,7 @@ def test_is_committed():
 
 
 def test_registry_config_inconsistent():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
-    class MyDirective(Action):
+    class FooDirective(Action):
         config = {
             'my': list
         }
@@ -1569,8 +1554,7 @@ def test_registry_config_inconsistent():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('bar')
-    class MyDirective(Action):  # flake8: noqa
+    class BarDirective(Action):
         config = {
             'my': dict
         }
@@ -1584,14 +1568,15 @@ def test_registry_config_inconsistent():
         def perform(self, obj, my):
             my[self.message] = obj
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     with pytest.raises(ConfigError):
         commit(MyApp)
 
 
 def test_registry_factory_argument_inconsistent():
-    class MyApp(App):
-        pass
-
     class Other(object):
         factory_arguments = {
             'my': list
@@ -1608,7 +1593,6 @@ def test_registry_factory_argument_inconsistent():
         def __init__(self, my):
             self.my = my
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'other': Other,
@@ -1624,14 +1608,14 @@ def test_registry_factory_argument_inconsistent():
         def perform(self, obj, other, yetanother):
             pass
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     with pytest.raises(ConfigError):
         commit(MyApp)
 
 
 def test_registry_factory_argument_and_config_inconsistent():
-    class MyApp(App):
-        pass
-
     class Other(object):
         factory_arguments = {
             'my': dict
@@ -1640,7 +1624,6 @@ def test_registry_factory_argument_and_config_inconsistent():
         def __init__(self, my):
             self.my = my
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list,
@@ -1656,33 +1639,37 @@ def test_registry_factory_argument_and_config_inconsistent():
         def perform(self, obj, my, other):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     with pytest.raises(ConfigError):
         commit(MyApp)
 
 
-# Due to PEP 3155, having this class defined at the module top-level ensures
-# that its repr is the same in both Python 2 and 3.
-class MyAppForRepr(App):
-    pass
+# making this global to ensure the repr is the same
+# on Python 3.5 and earlier versions (see PEP 3155)
+class ReprDirective(Action):
+    """Doc"""
+    config = {
+        'my': list
+    }
 
+    def __init__(self, message):
+        self.message = message
 
-def test_directive_repr():
+    def identifier(self, my):
+        return self.message
 
-    @MyAppForRepr.directive('foo')
-    class MyDirective(Action):
-        """Doc"""
-        config = {
-            'my': list
-        }
+    def perform(self, obj, my):
+        my.append((self.message, obj))
 
-        def __init__(self, message):
-            self.message = message
 
-        def identifier(self, my):
-            return self.message
+class MyAppForRepr(App):
+    foo = directive(ReprDirective)
 
-        def perform(self, obj, my):
-            my.append((self.message, obj))
+
+def test_directive_repr():
+    MyAppForRepr.commit()
 
     assert repr(MyAppForRepr.foo) == (
         "<bound method AppMeta.foo of "
@@ -1690,13 +1677,6 @@ def test_directive_repr():
 
 
 def test_app_class_passed_into_action():
-    class MyApp(App):
-        touched = []
-
-    class SubApp(MyApp):
-        touched = []
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -1714,6 +1694,14 @@ def test_app_class_passed_into_action():
             app_class.touched.append(None)
             my.append((self.message, obj))
 
+    class MyApp(App):
+        touched = []
+
+        foo = directive(MyDirective)
+
+    class SubApp(MyApp):
+        touched = []
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -1732,11 +1720,7 @@ def test_app_class_passed_into_action():
     assert SubApp.touched == [None]
 
 
-
 def test_app_class_passed_into_factory():
-    class MyApp(App):
-        touched = False
-
     class Other(object):
         factory_arguments = {
             'my': list
@@ -1751,7 +1735,6 @@ def test_app_class_passed_into_factory():
         def touch(self):
             self.app_class.touched = True
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'other': Other
@@ -1766,6 +1749,11 @@ def test_app_class_passed_into_factory():
         def perform(self, obj, other):
             other.touch()
 
+    class MyApp(App):
+        touched = False
+
+        foo = directive(MyDirective)
+
     @MyApp.foo()
     def f():
         pass
@@ -1778,9 +1766,6 @@ def test_app_class_passed_into_factory():
 
 
 def test_app_class_passed_into_factory_no_factory_arguments():
-    class MyApp(App):
-        touched = False
-
     class Other(object):
         app_class_arg = True
 
@@ -1790,7 +1775,6 @@ def test_app_class_passed_into_factory_no_factory_arguments():
         def touch(self):
             self.app_class.touched = True
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'other': Other
@@ -1805,6 +1789,11 @@ def test_app_class_passed_into_factory_no_factory_arguments():
         def perform(self, obj, other):
             other.touch()
 
+    class MyApp(App):
+        touched = False
+
+        foo = directive(MyDirective)
+
     @MyApp.foo()
     def f():
         pass
@@ -1817,12 +1806,6 @@ def test_app_class_passed_into_factory_no_factory_arguments():
 
 
 def test_app_class_passed_into_factory_separation():
-    class MyApp(App):
-        touched = False
-
-    class SubApp(MyApp):
-        touched = False
-
     class Other(object):
         factory_arguments = {
             'my': list
@@ -1837,7 +1820,6 @@ def test_app_class_passed_into_factory_separation():
         def touch(self):
             self.app_class.touched = True
 
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'other': Other
@@ -1852,6 +1834,13 @@ def test_app_class_passed_into_factory_separation():
         def perform(self, obj, other):
             other.touch()
 
+    class MyApp(App):
+        touched = False
+        foo = directive(MyDirective)
+
+    class SubApp(MyApp):
+        touched = False
+
     @MyApp.foo()
     def f():
         pass
@@ -1869,16 +1858,7 @@ def test_app_class_passed_into_factory_separation():
     assert SubApp.touched
 
 
-
 def test_app_class_cleanup():
-    class MyApp(App):
-        touched = []
-
-        @classmethod
-        def clean(cls):
-            cls.touched = []
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
         }
@@ -1894,6 +1874,15 @@ def test_app_class_cleanup():
         def perform(self, obj, app_class):
             app_class.touched.append(None)
 
+    class MyApp(App):
+        touched = []
+
+        @classmethod
+        def clean(cls):
+            cls.touched = []
+
+        foo = directive(MyDirective)
+
     @MyApp.foo()
     def f():
         pass
diff --git a/dectate/tests/test_error.py b/dectate/tests/test_error.py
index b166fba..7b3ec15 100644
--- a/dectate/tests/test_error.py
+++ b/dectate/tests/test_error.py
@@ -1,4 +1,4 @@
-from dectate.app import App
+from dectate.app import App, directive
 from dectate.config import commit, Action, Composite
 
 from dectate.error import (ConflictError, ConfigError, DirectiveError,
@@ -9,10 +9,6 @@ import pytest
 
 
 def test_directive_error_in_action():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         def __init__(self, name):
             self.name = name
@@ -23,6 +19,9 @@ def test_directive_error_in_action():
         def perform(self, obj):
             raise DirectiveError("A real problem")
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -37,10 +36,6 @@ def test_directive_error_in_action():
 
 
 def test_directive_error_in_composite():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Composite):
         def __init__(self, name):
             self.name = name
@@ -48,6 +43,9 @@ def test_directive_error_in_composite():
         def actions(self, obj):
             raise DirectiveError("Something went wrong")
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -62,10 +60,6 @@ def test_directive_error_in_composite():
 
 
 def test_conflict_error():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         def __init__(self, name):
             self.name = name
@@ -76,6 +70,9 @@ def test_conflict_error():
         def perform(self, obj):
             raise DirectiveError("A real problem")
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -95,10 +92,6 @@ def test_conflict_error():
 
 
 def test_with_statement_error():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         def __init__(self, model, name):
             self.model = model
@@ -110,6 +103,9 @@ def test_with_statement_error():
         def perform(self, obj):
             raise DirectiveError("A real problem")
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     class Dummy(object):
         pass
 
@@ -133,10 +129,6 @@ def test_with_statement_error():
 
 
 def test_composite_codeinfo_propagation():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('sub')
     class SubDirective(Action):
         config = {
             'my': list
@@ -151,7 +143,6 @@ def test_composite_codeinfo_propagation():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('composite')
     class CompositeDirective(Composite):
         def __init__(self, messages):
             self.messages = messages
@@ -159,6 +150,10 @@ def test_composite_codeinfo_propagation():
         def actions(self, obj):
             return [(SubDirective(message), obj) for message in self.messages]
 
+    class MyApp(App):
+        _sub = directive(SubDirective)
+        composite = directive(CompositeDirective)
+
     @MyApp.composite(['a'])
     def f():
         pass
@@ -177,10 +172,6 @@ def test_composite_codeinfo_propagation():
 
 
 def test_type_error_not_enough_arguments():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -195,6 +186,9 @@ def test_type_error_not_enough_arguments():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     # not enough arguments
     @MyApp.foo()
     def f():
@@ -208,10 +202,6 @@ def test_type_error_not_enough_arguments():
 
 
 def test_type_error_too_many_arguments():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -226,7 +216,10 @@ def test_type_error_too_many_arguments():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    # not enough arguments
+    class MyApp(App):
+        foo = directive(MyDirective)
+
+    # too many arguments
     @MyApp.foo('a', 'b')
     def f():
         pass
@@ -239,10 +232,6 @@ def test_type_error_too_many_arguments():
 
 
 def test_cannot_group_class_group_class():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -257,29 +246,28 @@ def test_cannot_group_class_group_class():
         def perform(self, obj, foo):
             foo.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         group_class = FooDirective
 
         def __init__(self, message):
             pass
 
-    @MyApp.directive('qux')
     class QuxDirective(Action):
         group_class = BarDirective  # should go to FooDirective instead
 
         def __init__(self, message):
             pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+        qux = directive(QuxDirective)
+
     with pytest.raises(ConfigError):
         commit(MyApp)
 
 
 def test_cannot_use_config_with_group_class():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -294,7 +282,6 @@ def test_cannot_use_config_with_group_class():
         def perform(self, obj, foo):
             foo.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         config = {
             'bar': list
@@ -305,15 +292,15 @@ def test_cannot_use_config_with_group_class():
         def __init__(self, message):
             pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     with pytest.raises(ConfigError):
         commit(MyApp)
 
 
 def test_cann_inherit_config_with_group_class():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -328,21 +315,20 @@ def test_cann_inherit_config_with_group_class():
         def perform(self, obj, foo):
             foo.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(FooDirective):
         group_class = FooDirective
 
         def __init__(self, message):
             pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     commit(MyApp)
 
 
 def test_cannot_use_before_with_group_class():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -357,7 +343,6 @@ def test_cannot_use_before_with_group_class():
         def perform(self, obj, foo):
             foo.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         group_class = FooDirective
 
@@ -365,15 +350,15 @@ def test_cannot_use_before_with_group_class():
         def before():
             pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     with pytest.raises(ConfigError):
         commit(MyApp)
 
 
 def test_can_inherit_before_with_group_class():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -392,18 +377,17 @@ def test_can_inherit_before_with_group_class():
         def before(foo):
             pass
 
-    @MyApp.directive('bar')
     class BarDirective(FooDirective):
         group_class = FooDirective
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     commit(MyApp)
 
 
 def test_cannot_use_after_with_group_class():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -418,7 +402,6 @@ def test_cannot_use_after_with_group_class():
         def perform(self, obj, foo):
             foo.append((self.message, obj))
 
-    @MyApp.directive('bar')
     class BarDirective(Action):
         group_class = FooDirective
 
@@ -426,15 +409,15 @@ def test_cannot_use_after_with_group_class():
         def after():
             pass
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+        bar = directive(BarDirective)
+
     with pytest.raises(ConfigError):
         commit(MyApp)
 
 
 def test_action_without_init():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooDirective(Action):
         config = {
             'foo': list
@@ -446,6 +429,9 @@ def test_action_without_init():
         def perform(self, obj, foo):
             foo.append(obj)
 
+    class MyApp(App):
+        foo = directive(FooDirective)
+
     @MyApp.foo()
     def f():
         pass
@@ -456,10 +442,6 @@ def test_action_without_init():
 
 
 def test_composite_without_init():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('sub')
     class SubDirective(Action):
         config = {
             'my': list
@@ -474,11 +456,14 @@ def test_composite_without_init():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
-    @MyApp.directive('composite')
     class CompositeDirective(Composite):
         def actions(self, obj):
             return [(SubDirective(message), obj) for message in ['a', 'b']]
 
+    class MyApp(App):
+        _sub = directive(SubDirective)
+        composite = directive(CompositeDirective)
+
     commit(MyApp)
 
     @MyApp.composite()
diff --git a/dectate/tests/test_logging.py b/dectate/tests/test_logging.py
index 9af8d21..c15a14d 100644
--- a/dectate/tests/test_logging.py
+++ b/dectate/tests/test_logging.py
@@ -1,5 +1,5 @@
 import logging
-from dectate.app import App
+from dectate.app import App, directive
 from dectate.config import Action, commit
 
 
@@ -35,10 +35,6 @@ def test_simple_config_logging():
     log.addHandler(test_handler)
     log.setLevel(logging.DEBUG)
 
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -53,6 +49,9 @@ def test_simple_config_logging():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
@@ -76,10 +75,6 @@ def test_subclass_config_logging():
     log.addHandler(test_handler)
     log.setLevel(logging.DEBUG)
 
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -94,6 +89,9 @@ def test_subclass_config_logging():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        foo = directive(MyDirective)
+
     class SubApp(MyApp):
         pass
 
@@ -127,10 +125,6 @@ def test_override_logger_name():
     log.addHandler(test_handler)
     log.setLevel(logging.DEBUG)
 
-    class MyApp(App):
-        logger_name = 'morepath.directive'
-
-    @MyApp.directive('foo')
     class MyDirective(Action):
         config = {
             'my': list
@@ -145,6 +139,11 @@ def test_override_logger_name():
         def perform(self, obj, my):
             my.append((self.message, obj))
 
+    class MyApp(App):
+        logger_name = 'morepath.directive'
+
+        foo = directive(MyDirective)
+
     @MyApp.foo('hello')
     def f():
         pass
diff --git a/dectate/tests/test_query.py b/dectate/tests/test_query.py
index dd6b185..6e97bdd 100644
--- a/dectate/tests/test_query.py
+++ b/dectate/tests/test_query.py
@@ -1,14 +1,10 @@
 import pytest
 
 from dectate import (
-    Query, App, Action, Composite, commit, QueryError, NOT_FOUND)
+    Query, App, Action, Composite, directive, commit, QueryError, NOT_FOUND)
 
 
 def test_query():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -23,6 +19,9 @@ def test_query():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -42,10 +41,6 @@ def test_query():
 
 
 def test_query_directive_name():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -60,6 +55,9 @@ def test_query_directive_name():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -79,10 +77,6 @@ def test_query_directive_name():
 
 
 def test_multi_action_query():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -97,7 +91,6 @@ def test_multi_action_query():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
-    @MyApp.directive('bar')
     class BarAction(Action):
         config = {
             'registry': list
@@ -112,6 +105,10 @@ def test_multi_action_query():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+        bar = directive(BarAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -131,10 +128,6 @@ def test_multi_action_query():
 
 
 def test_filter():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -149,6 +142,9 @@ def test_filter():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -167,10 +163,6 @@ def test_filter():
 
 
 def test_filter_multiple_fields():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -190,6 +182,9 @@ def test_filter_multiple_fields():
         def perform(self, obj, registry):
             registry.append((self.model, self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     class Alpha(object):
         pass
 
@@ -223,10 +218,6 @@ def test_filter_multiple_fields():
 
 
 def test_filter_not_found():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -241,6 +232,9 @@ def test_filter_not_found():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -257,10 +251,6 @@ def test_filter_not_found():
 
 
 def test_filter_different_attribute_name():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -279,6 +269,9 @@ def test_filter_different_attribute_name():
         def perform(self, obj, registry):
             registry.append((self._name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -295,10 +288,6 @@ def test_filter_different_attribute_name():
 
 
 def test_filter_get_value():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         def filter_get_value(self, name):
             return self.kw.get(name, NOT_FOUND)
@@ -312,6 +301,9 @@ def test_filter_get_value():
         def perform(self, obj):
             pass
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo(x='a', y='b')
     def f():
         pass
@@ -333,10 +325,6 @@ def test_filter_get_value():
 
 
 def test_filter_name_and_get_value():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         filter_name = {
             'name': '_name'
@@ -355,6 +343,9 @@ def test_filter_name_and_get_value():
         def perform(self, obj):
             pass
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo(name='hello', x='a', y='b')
     def f():
         pass
@@ -371,10 +362,6 @@ def test_filter_name_and_get_value():
 
 
 def test_filter_get_value_and_default():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         def filter_get_value(self, name):
             return self.kw.get(name, NOT_FOUND)
@@ -389,6 +376,9 @@ def test_filter_get_value_and_default():
         def perform(self, obj):
             pass
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo(name='hello', x='a', y='b')
     def f():
         pass
@@ -405,10 +395,6 @@ def test_filter_get_value_and_default():
 
 
 def test_filter_class():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('view')
     class ViewAction(Action):
         config = {
             'registry': list
@@ -427,6 +413,9 @@ def test_filter_class():
         def perform(self, obj, registry):
             registry.append((self.model, obj))
 
+    class MyApp(App):
+        view = directive(ViewAction)
+
     class Alpha(object):
         pass
 
@@ -467,10 +456,6 @@ def test_filter_class():
 
 
 def test_query_group_class():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -485,10 +470,13 @@ def test_query_group_class():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
-    @MyApp.directive('bar')
     class BarAction(FooAction):
         group_class = FooAction
 
+    class MyApp(App):
+        foo = directive(FooAction)
+        bar = directive(BarAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -508,10 +496,6 @@ def test_query_group_class():
 
 
 def test_query_on_group_class_action():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -526,10 +510,13 @@ def test_query_on_group_class_action():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
-    @MyApp.directive('bar')
     class BarAction(FooAction):
         group_class = FooAction
 
+    class MyApp(App):
+        foo = directive(FooAction)
+        bar = directive(BarAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -549,10 +536,6 @@ def test_query_on_group_class_action():
 
 
 def test_multi_query_on_group_class_action():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -567,10 +550,13 @@ def test_multi_query_on_group_class_action():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
-    @MyApp.directive('bar')
     class BarAction(FooAction):
         group_class = FooAction
 
+    class MyApp(App):
+        foo = directive(FooAction)
+        bar = directive(BarAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -590,13 +576,6 @@ def test_multi_query_on_group_class_action():
 
 
 def test_inheritance():
-    class MyApp(App):
-        pass
-
-    class SubApp(MyApp):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -611,6 +590,12 @@ def test_inheritance():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
+    class SubApp(MyApp):
+        pass
+
     @MyApp.foo('a')
     def f():
         pass
@@ -630,10 +615,6 @@ def test_inheritance():
 
 
 def test_composite_action():
-    class MyApp(App):
-        pass
-
-    @MyApp.private_action_class
     class SubAction(Action):
         config = {
             'registry': list
@@ -648,7 +629,6 @@ def test_composite_action():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
-    @MyApp.directive('composite')
     class CompositeAction(Composite):
         query_classes = [
             SubAction
@@ -660,6 +640,10 @@ def test_composite_action():
         def actions(self, obj):
             return [(SubAction(name), obj) for name in self.names]
 
+    class MyApp(App):
+        _sub = directive(SubAction)
+        composite = directive(CompositeAction)
+
     @MyApp.composite(['a', 'b'])
     def f():
         pass
@@ -675,10 +659,6 @@ def test_composite_action():
 
 
 def test_composite_action_without_query_classes():
-    class MyApp(App):
-        pass
-
-    @MyApp.private_action_class
     class SubAction(Action):
         config = {
             'registry': list
@@ -693,7 +673,6 @@ def test_composite_action_without_query_classes():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
-    @MyApp.directive('composite')
     class CompositeAction(Composite):
         def __init__(self, names):
             self.names = names
@@ -701,6 +680,10 @@ def test_composite_action_without_query_classes():
         def actions(self, obj):
             return [(SubAction(name), obj) for name in self.names]
 
+    class MyApp(App):
+        _sub = directive(SubAction)
+        composite = directive(CompositeAction)
+
     @MyApp.composite(['a', 'b'])
     def f():
         pass
@@ -714,10 +697,6 @@ def test_composite_action_without_query_classes():
 
 
 def test_nested_composite_action():
-    class MyApp(App):
-        pass
-
-    @MyApp.private_action_class
     class SubSubAction(Action):
         config = {
             'registry': list
@@ -732,7 +711,6 @@ def test_nested_composite_action():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
-    @MyApp.private_action_class
     class SubAction(Composite):
         query_classes = [
             SubSubAction
@@ -744,7 +722,6 @@ def test_nested_composite_action():
         def actions(self, obj):
             return [(SubSubAction(name), obj) for name in self.names]
 
-    @MyApp.directive('composite')
     class CompositeAction(Composite):
         query_classes = [
             SubAction
@@ -757,6 +734,11 @@ def test_nested_composite_action():
             for i in range(self.amount):
                 yield SubAction(['a%s' % i, 'b%s' % i]), obj
 
+    class MyApp(App):
+        _subsub = directive(SubSubAction)
+        _sub = directive(SubAction)
+        composite = directive(CompositeAction)
+
     @MyApp.composite(2)
     def f():
         pass
@@ -774,13 +756,6 @@ def test_nested_composite_action():
 
 
 def test_query_action_for_other_app():
-    class MyApp(App):
-        pass
-
-    class OtherApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         config = {
             'registry': list
@@ -795,7 +770,6 @@ def test_query_action_for_other_app():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
-    @OtherApp.directive('foo')
     class BarAction(Action):
         config = {
             'registry': list
@@ -810,6 +784,12 @@ def test_query_action_for_other_app():
         def perform(self, obj, registry):
             registry.append((self.name, obj))
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
+    class OtherApp(App):
+        bar = directive(BarAction)
+
     @MyApp.foo('a')
     def f():
         pass
diff --git a/dectate/tests/test_tool.py b/dectate/tests/test_tool.py
index ed49afe..cf6b361 100644
--- a/dectate/tests/test_tool.py
+++ b/dectate/tests/test_tool.py
@@ -2,7 +2,7 @@ import pytest
 from argparse import ArgumentTypeError
 
 from dectate.config import Action, commit
-from dectate.app import App
+from dectate.app import App, directive
 from dectate.tool import (parse_app_class, parse_directive, parse_filters,
                           convert_filters,
                           convert_dotted_name, convert_bool,
@@ -122,10 +122,6 @@ def test_convert_filters_value_error():
 
 
 def test_query_tool_output():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         def __init__(self, name):
             self.name = name
@@ -136,6 +132,9 @@ def test_query_tool_output():
         def perform(self, obj):
             pass
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -155,8 +154,18 @@ def test_query_tool_output():
 
 
 def test_query_tool_output_multiple_apps():
+    class FooAction(Action):
+        def __init__(self, name):
+            self.name = name
+
+        def identifier(self):
+            return self.name
+
+        def perform(self, obj):
+            pass
+
     class Base(App):
-        pass
+        foo = directive(FooAction)
 
     class AlphaApp(Base):
         pass
@@ -167,17 +176,6 @@ def test_query_tool_output_multiple_apps():
     class GammaApp(Base):
         pass
 
-    @Base.directive('foo')
-    class FooAction(Action):
-        def __init__(self, name):
-            self.name = name
-
-        def identifier(self):
-            return self.name
-
-        def perform(self, obj):
-            pass
-
     @AlphaApp.foo('a')
     def f():
         pass
@@ -194,10 +192,6 @@ def test_query_tool_output_multiple_apps():
 
 
 def test_query_app():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         filter_convert = {
             'count': int
@@ -212,6 +206,9 @@ def test_query_app():
         def perform(self, obj):
             pass
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo(1)
     def f():
         pass
@@ -228,10 +225,6 @@ def test_query_app():
 
 
 def test_query_tool_uncommitted():
-    class MyApp(App):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         def __init__(self, name):
             self.name = name
@@ -242,6 +235,9 @@ def test_query_tool_uncommitted():
         def perform(self, obj):
             pass
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
     @MyApp.foo('a')
     def f():
         pass
@@ -277,13 +273,6 @@ def test_app_without_directive():
 
 
 def test_inheritance():
-    class MyApp(App):
-        pass
-
-    class SubApp(MyApp):
-        pass
-
-    @MyApp.directive('foo')
     class FooAction(Action):
         filter_convert = {
             'count': int
@@ -298,6 +287,12 @@ def test_inheritance():
         def perform(self, obj):
             pass
 
+    class MyApp(App):
+        foo = directive(FooAction)
+
+    class SubApp(MyApp):
+        pass
+
     @MyApp.foo(1)
     def f():
         pass