]> git.llucax.com Git - software/pymin.git/blobdiff - pymin/dispatcher.py
Add host name related validators to pymin.validation (refs #20)
[software/pymin.git] / pymin / dispatcher.py
index c7d260bda6f1910d27b610a3f79f4b579613256e..0d28cb337bf1686d53f46446cdef03ec2272c2bc 100644 (file)
@@ -7,7 +7,11 @@ It's based on Zope or Cherrypy dispatching (but implemented from the scratch)
 and translates commands to functions/objects/methods.
 """
 
 and translates commands to functions/objects/methods.
 """
 
-__ALL__ = ('Error', 'HandlerError', 'CommandNotFoundError', 'Handler',
+import re
+import inspect
+import logging ; log = logging.getLogger('pymin.dispatcher')
+
+__all__ = ('Error', 'HandlerError', 'CommandNotFoundError', 'Handler',
             'Dispatcher', 'handler', 'is_handler', 'get_help')
 
 class Error(RuntimeError):
             'Dispatcher', 'handler', 'is_handler', 'get_help')
 
 class Error(RuntimeError):
@@ -19,7 +23,16 @@ class Error(RuntimeError):
     command - is the command that raised the exception, expressed as a list of
               paths (or subcommands).
     """
     command - is the command that raised the exception, expressed as a list of
               paths (or subcommands).
     """
-    pass
+
+    def __init__(self, message):
+        r"Initialize the Error object. See class documentation for more info."
+        self.message = message
+
+    def __unicode__(self):
+        return self.message
+
+    def __str__(self):
+        return unicode(self).encode('utf-8')
 
 class HandlerError(Error):
     r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
 
 class HandlerError(Error):
     r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
@@ -37,25 +50,104 @@ class CommandError(Error):
     """
 
     def __init__(self, command):
     """
 
     def __init__(self, command):
-        r"""Initialize the object.
-
-        See class documentation for more info.
-        """
+        r"Initialize the object, see class documentation for more info."
         self.command = command
 
         self.command = command
 
-    def __str__(self):
-        return 'Command error: "%s"' % self.command
+    def __unicode__(self):
+        return u'Error in command "%s".' % u' '.join(self.command)
+
+class WrongArgumentsError(CommandError):
+    r"""WrongArgumentsError(handler, error) -> WrongArgumentsError instance.
+
+    This exception is raised when an empty command string is received.
+    """
+
+    def __init__(self, handler, error):
+        r"Initialize the object, see class documentation for more info."
+        self.handler = handler
+        self.error = error
+
+    args_re = re.compile(r'\w+\(\) takes (.+) (\d+) \w+ \((\d+) given\)')
+
+    extra_kw_re = re.compile(r'\w+\(\) got an unexpected keyword argument (.+)')
+
+    dup_kw_re = re.compile(r'\w+\(\) got multiple values for keyword argument '
+                            r"'(.+)'")
+
+    def format(self):
+        r"format() -> unicode - Format a TypeError to adapt it to a command."
+        m = self.args_re.match(unicode(self.error))
+        if m:
+            (quant, n_ok, n_bad)  = m.groups()
+            n_ok = int(n_ok)
+            n_bad = int(n_bad)
+            n_ok -= 1
+            n_bad -= 1
+            pl = ''
+            if n_ok != 1:
+                pl = 's'
+            return u'takes %s %s argument%s, %s given' \
+                        % (quant, n_ok, pl, n_bad)
+        m = self.extra_kw_re.match(unicode(self.error))
+        if m:
+            (kw,)  = m.groups()
+            return u'got an unexpected keyword argument %s' % kw
+        m = self.dup_kw_re.match(unicode(self.error))
+        if m:
+            (kw,)  = m.groups()
+            return u'got multiple values for argument %s' % kw
+        return u'got wrong arguments'
+
+    def __unicode__(self):
+        return u'Command "%s" %s.' % (self.handler.__name__, self.format())
+
+class CommandNotSpecifiedError(CommandError):
+    r"""CommandNotSpecifiedError() -> CommandNotSpecifiedError instance.
+
+    This exception is raised when an empty command string is received.
+    """
+
+    def __init__(self):
+        r"Initialize the object, see class documentation for more info."
+        pass
+
+    def __unicode__(self):
+        return u'Command not specified.'
+
+class CommandIsAHandlerError(CommandError):
+    r"""CommandIsAHandlerError() -> CommandIsAHandlerError instance.
+
+    This exception is raised when a command is a handler containing commands
+    instead of a command itself.
+    """
+
+    def __unicode__(self):
+        command = ' '.join(self.command)
+        return u'"%s" is a handler, not a command (type "%s help" for help).' \
+                    % (command, command)
+
+class CommandNotInHandlerError(CommandError):
+    r"""CommandNotInHandlerError() -> CommandNotInHandlerError instance.
+
+    This exception is raised when a command parent is a hanlder containing
+    commands, but the command itself is not found.
+    """
+
+    def __unicode__(self):
+        return u'Command "%(c)s" not found in handler "%(h)s" ' \
+                u'(type "%(h)s help" for help).' \
+                        % dict(c=u' '.join(self.command[-1:]),
+                                h=u' '.join(self.command[0:-1]))
 
 class CommandNotFoundError(CommandError):
 
 class CommandNotFoundError(CommandError):
-    r"""CommandNotFoundError(command) -> CommandNotFoundError instance.
+    r"""CommandNotFoundError(command[, handler]) -> CommandNotFoundError object.
 
     This exception is raised when the command received can't be dispatched
     because there is no handlers to process it.
     """
 
 
     This exception is raised when the command received can't be dispatched
     because there is no handlers to process it.
     """
 
-    def __str__(self):
-        return 'Command not found: "%s"' % ' '.join(
-                                                repr(c) for c in self.command)
+    def __unicode__(self):
+        return u'Command "%s" not found.' % u' '.join(self.command)
 
 class ParseError(CommandError):
     r"""ParseError(command[, desc]) -> ParseError instance
 
 class ParseError(CommandError):
     r"""ParseError(command[, desc]) -> ParseError instance
@@ -75,8 +167,26 @@ class ParseError(CommandError):
         self.command = command
         self.desc = desc
 
         self.command = command
         self.desc = desc
 
-    def __str__(self):
-        return 'Syntax error, %s: %s' % (self.desc, self.command)
+    def __unicode__(self):
+        return u'Syntax error, %s: %s' % (self.desc, self.command)
+
+class HelpNotFoundError(Error):
+    r"""HelpNotFoundError(command) -> HelpNotFoundError instance.
+
+    This exception is raised when a help command can't find the command
+    asked for help.
+    """
+
+    def __init__(self, command):
+        r"""Initialize the object.
+
+        See class documentation for more info.
+        """
+        self.command = command
+
+    def __unicode__(self):
+        return u"Can't get help for '%s', command not found." % self.command
+
 
 def handler(help):
     r"""handler(help) -> function wrapper :: Mark a callable as a handler.
 
 def handler(help):
     r"""handler(help) -> function wrapper :: Mark a callable as a handler.
@@ -85,36 +195,74 @@ def handler(help):
 
     help - Help string for the handler.
     """
 
     help - Help string for the handler.
     """
-    def wrapper(f):
-        if not help:
-            raise TypeError("'help' should not be empty")
-        f._dispatcher_help = help
-        return f
-    return wrapper
+    if not help:
+        raise TypeError("'help' should not be empty")
+    def make_wrapper(f):
+        log.debug('handler(): Decorating %s()', f.__name__)
+        # Here comes the tricky part:
+        # We need to make our wrapped function to accept any number of
+        # positional and keyword arguments, but checking for the correct
+        # arguments and raising an exception in case the arguments doesn't
+        # match.
+        # So we create a dummy function, with the same signature as the
+        # wrapped one, so we can check later (at "dispatch-time") if the
+        # real function call will be successful. If the dummy function don't
+        # raise a TypeError, the arguments are just fine.
+        env = dict()
+        argspec = inspect.getargspec(f)
+        signature = inspect.formatargspec(*argspec)
+        # The dummy function
+        exec "def f%s: pass" % signature in env
+        signature_check = env['f']
+        # The wrapper to check the signature at "dispatch-time"
+        def wrapper(*args, **kwargs):
+            # First we check if the arguments passed are OK.
+            try:
+                signature_check(*args, **kwargs)
+            except TypeError, e:
+                # If not, we raise an appropriate error.
+                raise WrongArgumentsError(f, e)
+            # If they are fine, we call the real function
+            return f(*args, **kwargs)
+        # Some flag to mark our handlers for simple checks
+        wrapper._dispatcher_handler = True
+        # The help string we asked for in the first place =)
+        wrapper.handler_help = help
+        # We store the original signature for better help generation
+        wrapper.handler_argspec = argspec
+        # And some makeup, to make our wrapper look like the original function
+        wrapper.__name__ = f.__name__
+        wrapper.__dict__.update(f.__dict__)
+        # We add a hint in the documentation
+        wrapper.__doc__ = "Pymin handler with signature: %s%s" \
+                            % (wrapper.__name__, signature)
+        if f.__doc__ is not None:
+            wrapper.__doc__ += "\n\n" + f.__doc__
+        wrapper.__module__ = f.__module__
+        return wrapper
+    return make_wrapper
 
 def is_handler(handler):
     r"is_handler(handler) -> bool :: Tell if a object is a handler."
 
 def is_handler(handler):
     r"is_handler(handler) -> bool :: Tell if a object is a handler."
-    return callable(handler) and hasattr(handler, '_dispatcher_help')
-
-def get_help(handler):
-    r"get_help(handler) -> unicode :: Get a handler's help string."
-    if not is_handler(handler):
-        raise TypeError("'%s' should be a handler" % handler.__name__)
-    return handler._dispatcher_help
+    return callable(handler) and hasattr(handler, '_dispatcher_handler') \
+                and handler._dispatcher_handler
 
 class Handler:
     r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
 
     All dispatcher handlers should inherit from this class to have some extra
 
 class Handler:
     r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
 
     All dispatcher handlers should inherit from this class to have some extra
-    commands, like help.
+    commands, like help. You should override the 'handler_help' attribute to a
+    nice help message describing the handler.
     """
 
     """
 
-    @handler(u'List available commands.')
+    handler_help = u'Undocumented handler'
+
+    @handler(u'List available commands')
     def commands(self):
         r"""commands() -> generator :: List the available commands."""
         return (a for a in dir(self) if is_handler(getattr(self, a)))
 
     def commands(self):
         r"""commands() -> generator :: List the available commands."""
         return (a for a in dir(self) if is_handler(getattr(self, a)))
 
-    @handler(u'Show available commands with their help.')
+    @handler(u'Show available commands with their help')
     def help(self, command=None):
         r"""help([command]) -> unicode/dict :: Show help on available commands.
 
     def help(self, command=None):
         r"""help([command]) -> unicode/dict :: Show help on available commands.
 
@@ -123,14 +271,33 @@ class Handler:
         and values are the help strings.
         """
         if command is None:
         and values are the help strings.
         """
         if command is None:
-            return dict((a, get_help(getattr(self, a)))
-                        for a in dir(self) if is_handler(getattr(self, a)))
-        if not hasattr(self, command):
-            raise CommandNotFoundError(command)
-        handler = getattr(self, command)
-        if not is_handler(handler):
-            raise CommandNotFoundError(command)
-        return get_help(handler)
+            d = dict()
+            for a in dir(self):
+                h = getattr(self, a)
+                if a == 'parent': continue # Skip parents in SubHandlers
+                if is_handler(h) or isinstance(h, Handler):
+                    d[a] = h.handler_help
+            return d
+        # A command was specified
+        if command == 'parent': # Skip parents in SubHandlers
+            raise HelpNotFoundError(command)
+        if not hasattr(self, command.encode('utf-8')):
+            raise HelpNotFoundError(command)
+        handler = getattr(self, command.encode('utf-8'))
+        if not is_handler(handler) and not hasattr(handler, 'handler_help'):
+            raise HelpNotFoundError(command)
+        return handler.handler_help
+
+    def handle_timer(self):
+        r"""handle_timer() -> None :: Do periodic tasks.
+
+        By default we do nothing but calling handle_timer() on subhandlers.
+        """
+        for a in dir(self):
+            if a == 'parent': continue # Skip parents in SubHandlers
+            h = getattr(self, a)
+            if isinstance(h, Handler):
+                h.handle_timer()
 
 def parse_command(command):
     r"""parse_command(command) -> (args, kwargs) :: Parse a command.
 
 def parse_command(command):
     r"""parse_command(command) -> (args, kwargs) :: Parse a command.
@@ -158,6 +325,8 @@ def parse_command(command):
     arguments is preserved and if there are multiple keyword arguments with
     the same key, the last value is the winner (all other values are lost).
 
     arguments is preserved and if there are multiple keyword arguments with
     the same key, the last value is the winner (all other values are lost).
 
+    The command should be a unicode string.
+
     Examples:
 
     >>> parse_command('hello world')
     Examples:
 
     >>> parse_command('hello world')
@@ -185,6 +354,12 @@ def parse_command(command):
     ([u'=hello'], {})
     >>> parse_command(r'\thello')
     ([u'\thello'], {})
     ([u'=hello'], {})
     >>> parse_command(r'\thello')
     ([u'\thello'], {})
+    >>> parse_command(r'hello \n')
+    ([u'hello', u'\n'], {})
+    >>> parse_command(r'hello \nmundo')
+    ([u'hello', u'\nmundo'], {})
+    >>> parse_command(r'test \N')
+    ([u'test', None], {})
     >>> parse_command(r'\N')
     ([None], {})
     >>> parse_command(r'none=\N')
     >>> parse_command(r'\N')
     ([None], {})
     >>> parse_command(r'none=\N')
@@ -209,9 +384,23 @@ def parse_command(command):
     escape = False
     keyword = None
     state = SEP
     escape = False
     keyword = None
     state = SEP
+    def register_token(buff, keyword, seq, dic):
+        if buff == r'\N':
+            buff = None
+        if keyword is not None:
+            dic[keyword.encode('utf-8')] = buff
+            keyword = None
+        else:
+            seq.append(buff)
+        buff = u''
+        return (buff, keyword)
     for n, c in enumerate(command):
         # Escaped character
         if escape:
     for n, c in enumerate(command):
         # Escaped character
         if escape:
+            # Not yet registered the token
+            if state == SEP and buff:
+                (buff, keyword) = register_token(buff, keyword, seq, dic)
+                state = TOKEN
             for e in escaped_chars:
                 if c == e:
                     buff += eval(u'"\\' + e + u'"')
             for e in escaped_chars:
                 if c == e:
                     buff += eval(u'"\\' + e + u'"')
@@ -236,14 +425,7 @@ def parse_command(command):
                     keyword = buff
                     buff = u''
                     continue
                     keyword = buff
                     buff = u''
                     continue
-                if buff == r'\N':
-                    buff = None
-                if keyword is not None: # Value found
-                    dic[str(keyword)] = buff
-                    keyword = None
-                else: # Normal parameter found
-                    seq.append(buff)
-                buff = u''
+                (buff, keyword) = register_token(buff, keyword, seq, dic)
             state = TOKEN
         # Getting a token
         if state == TOKEN:
             state = TOKEN
         # Getting a token
         if state == TOKEN:
@@ -285,24 +467,17 @@ def parse_command(command):
         raise ParseError(command,
                         u'keyword argument (%s) without value' % keyword)
     if buff:
         raise ParseError(command,
                         u'keyword argument (%s) without value' % keyword)
     if buff:
-        if buff == r'\N':
-            buff = None
-        if keyword is not None:
-            dic[str(keyword)] = buff
-        else:
-            seq.append(buff)
+        register_token(buff, keyword, seq, dic)
     return (seq, dic)
 
 class Dispatcher:
     return (seq, dic)
 
 class Dispatcher:
-    r"""Dispatcher([routes]) -> Dispatcher instance :: Command dispatcher
+    r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
 
     This class provides a modular and extensible dispatching mechanism. You
 
     This class provides a modular and extensible dispatching mechanism. You
-    can specify root 'routes' (as a dict where the key is the string of the
-    root command and the value is a callable object to handle that command,
-    or a subcommand if the callable is an instance and the command can be
-    sub-routed).
+    specify a root handler (probably as a pymin.dispatcher.Handler subclass),
 
 
-    The command can have arguments, separated by (any number of) spaces.
+    The command can have arguments, separated by (any number of) spaces and
+    keyword arguments (see parse_command for more details).
 
     The dispatcher tries to route the command as deeply as it can, passing
     the other "path" components as arguments to the callable. To route the
 
     The dispatcher tries to route the command as deeply as it can, passing
     the other "path" components as arguments to the callable. To route the
@@ -311,99 +486,174 @@ class Dispatcher:
 
     Example:
     >>> d = Dispatcher(dict(handler=some_handler))
 
     Example:
     >>> d = Dispatcher(dict(handler=some_handler))
-    >>> d.dispatch('handler attribute method arg1 arg2 "third argument"')
+    >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
 
     If 'some_handler' is an object with an 'attribute' that is another
     object which has a method named 'method', then
 
     If 'some_handler' is an object with an 'attribute' that is another
     object which has a method named 'method', then
-    some_handler.attribute.method('arg1', 'arg2') will be called. If
-    some_handler is a function, then some_handler('attribute', 'method',
-    'arg1', 'arg2') will be called. The handler "tree" can be as complex
-    and deep as you want.
+    some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
+    If some_handler is a function, then some_handler('attribute', 'method',
+    'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
+    complex and deep as you want.
 
 
-    If some command can't be dispatched (because there is no root handler or
-    there is no matching callable attribute), a CommandNotFoundError is raised.
+    If some command can't be dispatched, a CommandError subclass is raised.
     """
 
     """
 
-    def __init__(self, routes=dict()):
+    def __init__(self, root):
         r"""Initialize the Dispatcher object.
 
         See Dispatcher class documentation for more info.
         """
         r"""Initialize the Dispatcher object.
 
         See Dispatcher class documentation for more info.
         """
-        self.routes = routes
+        log.debug(u'Dispatcher(%r)', root)
+        self.root = root
 
     def dispatch(self, route):
         r"""dispatch(route) -> None :: Dispatch a command string.
 
         This method searches for a suitable callable object in the routes
 
     def dispatch(self, route):
         r"""dispatch(route) -> None :: Dispatch a command string.
 
         This method searches for a suitable callable object in the routes
-        "tree" and call it, or raises a CommandNotFoundError if the command
+        "tree" and call it, or raises a CommandError subclass if the command
         can't be dispatched.
         can't be dispatched.
+
+        route - *unicode* string with the command route.
         """
         """
+        log.debug('Dispatcher.dispatch(%r)', route)
         command = list()
         (route, kwargs) = parse_command(route)
         command = list()
         (route, kwargs) = parse_command(route)
+        log.debug(u'Dispatcher.dispatch: route=%r, kwargs=%r', route, kwargs)
         if not route:
         if not route:
-            raise CommandNotFoundError(command)
-        command.append(route[0])
-        handler = self.routes.get(route[0], None)
-        if handler is None:
-            raise CommandNotFoundError(command)
-        route = route[1:]
+            log.debug(u'Dispatcher.dispatch: command not specified')
+            raise CommandNotSpecifiedError()
+        handler = self.root
         while not is_handler(handler):
         while not is_handler(handler):
+            log.debug(u'Dispatcher.dispatch: handler=%r, route=%r',
+                        handler, route)
             if len(route) is 0:
             if len(route) is 0:
+                if isinstance(handler, Handler):
+                    log.debug(u'Dispatcher.dispatch: command is a handler')
+                    raise CommandIsAHandlerError(command)
+                log.debug(u'Dispatcher.dispatch: command not found')
                 raise CommandNotFoundError(command)
             command.append(route[0])
                 raise CommandNotFoundError(command)
             command.append(route[0])
-            if not hasattr(handler, route[0]):
+            log.debug(u'Dispatcher.dispatch: command=%r', command)
+            if route[0] == 'parent':
+                log.debug(u'Dispatcher.dispatch: is parent => not found')
+                raise CommandNotFoundError(command)
+            if not hasattr(handler, route[0].encode('utf-8')):
+                if isinstance(handler, Handler) and len(command) > 1:
+                    log.debug(u'Dispatcher.dispatch: command not in handler')
+                    raise CommandNotInHandlerError(command)
+                log.debug(u'Dispatcher.dispatch: command not found')
                 raise CommandNotFoundError(command)
                 raise CommandNotFoundError(command)
-            handler = getattr(handler, route[0])
+            handler = getattr(handler, route[0].encode('utf-8'))
             route = route[1:]
             route = route[1:]
-        return handler(*route, **kwargs)
+        log.debug(u'Dispatcher.dispatch: %r is a handler, calling it with '
+                    u'route=%r, kwargs=%r', handler, route, kwargs)
+        r = handler(*route, **kwargs)
+        log.debug(u'Dispatcher.dispatch: handler returned %s', r)
+        return r
 
 
 if __name__ == '__main__':
 
 
 
 if __name__ == '__main__':
 
-    @handler(u"test: Print all the arguments, return nothing.")
+    logging.basicConfig(
+        level   = logging.DEBUG,
+        format  = '%(asctime)s %(levelname)-8s %(message)s',
+        datefmt = '%H:%M:%S',
+    )
+
+    @handler(u"test: Print all the arguments, return nothing")
     def test_func(*args):
         print 'func:', args
 
     class TestClassSubHandler(Handler):
     def test_func(*args):
         print 'func:', args
 
     class TestClassSubHandler(Handler):
-        @handler(u"subcmd: Print all the arguments, return nothing.")
+        @handler(u"subcmd: Print all the arguments, return nothing")
         def subcmd(self, *args):
             print 'class.subclass.subcmd:', args
 
     class TestClass(Handler):
         def subcmd(self, *args):
             print 'class.subclass.subcmd:', args
 
     class TestClass(Handler):
-        @handler(u"cmd1: Print all the arguments, return nothing.")
+        @handler(u"cmd1: Print all the arguments, return nothing")
         def cmd1(self, *args):
             print 'class.cmd1:', args
         def cmd1(self, *args):
             print 'class.cmd1:', args
-        @handler(u"cmd2: Print all the arguments, return nothing.")
-        def cmd2(self, *args):
-            print 'class.cmd2:', args
+        @handler(u"cmd2: Print all the arguments, return nothing")
+        def cmd2(self, arg1, arg2):
+            print 'class.cmd2:', arg1, arg2
         subclass = TestClassSubHandler()
 
         subclass = TestClassSubHandler()
 
-    test_class = TestClass()
-
-    d = Dispatcher(dict(
-            func=test_func,
-            inst=test_class,
-    ))
-
-    d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
-    print 'inst commands:', tuple(d.dispatch('inst commands'))
-    print 'inst help:', d.dispatch('inst help')
-    d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
-    d.dispatch('inst cmd2 arg1 arg2')
-    print 'inst subclass help:', d.dispatch('inst subclass help')
-    d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
+    class RootHandler(Handler):
+        func = staticmethod(test_func)
+        inst = TestClass()
+
+    d = Dispatcher(RootHandler())
+
+    r = d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
+    assert r is None
+    r = list(d.dispatch('inst commands'))
+    r.sort()
+    assert r == ['cmd1', 'cmd2', 'commands', 'help']
+    print 'inst commands:', r
+    r = d.dispatch('inst help')
+    assert r == {
+                'commands': u'List available commands',
+                'subclass': u'Undocumented handler',
+                'cmd1': u'cmd1: Print all the arguments, return nothing',
+                'cmd2': u'cmd2: Print all the arguments, return nothing',
+                'help': u'Show available commands with their help'
+    }
+    print 'inst help:', r
+    r = d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
+    assert r is None
+    r = d.dispatch('inst cmd2 arg1 arg2')
+    assert r is None
+    r = d.dispatch('inst subclass help')
+    assert r == {
+                'subcmd': u'subcmd: Print all the arguments, return nothing',
+                'commands': u'List available commands',
+                'help': u'Show available commands with their help'
+    }
+    print 'inst subclass help:', r
+    r = d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
+    assert r is None
     try:
         d.dispatch('')
     try:
         d.dispatch('')
-    except CommandNotFoundError, e:
+        assert False, 'It should raised a CommandNotSpecifiedError'
+    except CommandNotSpecifiedError, e:
         print 'Not found:', e
     try:
         d.dispatch('sucutrule piquete culete')
         print 'Not found:', e
     try:
         d.dispatch('sucutrule piquete culete')
+        assert False, 'It should raised a CommandNotFoundError'
     except CommandNotFoundError, e:
         print 'Not found:', e
     try:
         d.dispatch('inst cmd3 arg1 arg2 arg3')
     except CommandNotFoundError, e:
         print 'Not found:', e
     try:
         d.dispatch('inst cmd3 arg1 arg2 arg3')
-    except CommandNotFoundError, e:
+        assert False, 'It should raised a CommandNotInHandlerError'
+    except CommandNotInHandlerError, e:
+        print 'Not found:', e
+    try:
+        d.dispatch('inst')
+        assert False, 'It should raised a CommandIsAHandlerError'
+    except CommandIsAHandlerError, e:
         print 'Not found:', e
         print 'Not found:', e
+    try:
+        d.dispatch('inst cmd2 "just one arg"')
+        assert False, 'It should raised a WrongArgumentsError'
+    except WrongArgumentsError, e:
+        print 'Bad arguments:', e
+    try:
+        d.dispatch('inst cmd2 arg1 arg2 "an extra argument"')
+        assert False, 'It should raised a WrongArgumentsError'
+    except WrongArgumentsError, e:
+        print 'Bad arguments:', e
+    try:
+        d.dispatch('inst cmd2 arg1 arg2 arg3="unexpected keyword arg"')
+        assert False, 'It should raised a WrongArgumentsError'
+    except WrongArgumentsError, e:
+        print 'Bad arguments:', e
+    try:
+        d.dispatch('inst cmd2 arg1 arg2 arg2="duplicated keyword arg"')
+        assert False, 'It should raised a WrongArgumentsError'
+    except WrongArgumentsError, e:
+        print 'Bad arguments:', e
+    print
+    print
 
     # Parser tests
     p = parse_command('hello world')
 
     # Parser tests
     p = parse_command('hello world')
@@ -432,6 +682,12 @@ if __name__ == '__main__':
     assert p == ([u'=hello'], {}), p
     p = parse_command(r'\thello')
     assert p == ([u'\thello'], {}), p
     assert p == ([u'=hello'], {}), p
     p = parse_command(r'\thello')
     assert p == ([u'\thello'], {}), p
+    p = parse_command(r'hello \n')
+    assert p == ([u'hello', u'\n'], {}), p
+    p = parse_command(r'hello \nmundo')
+    assert p == ([u'hello', u'\nmundo'], {}), p
+    p = parse_command(r'test \N')
+    assert p == ([u'test', None], {}), p
     p = parse_command(r'\N')
     assert p == ([None], {}), p
     p = parse_command(r'none=\N')
     p = parse_command(r'\N')
     assert p == ([None], {}), p
     p = parse_command(r'none=\N')
@@ -444,14 +700,12 @@ if __name__ == '__main__':
     assert p == ([u'\\None'], {}), p
     try:
         p = parse_command('hello=')
     assert p == ([u'\\None'], {}), p
     try:
         p = parse_command('hello=')
+        assert False, p + ' should raised a ParseError'
     except ParseError, e:
         pass
     except ParseError, e:
         pass
-    else:
-        assert False, p + ' should raised a ParseError'
     try:
         p = parse_command('"hello')
     try:
         p = parse_command('"hello')
+        assert False, p + ' should raised a ParseError'
     except ParseError, e:
         pass
     except ParseError, e:
         pass
-    else:
-        assert False, p + ' should raised a ParseError'