]> git.llucax.com Git - software/pymin.git/blob - pymin/dispatcher.py
Split qos handler in submodules (refs #2).
[software/pymin.git] / pymin / dispatcher.py
1 # vim: set et sts=4 sw=4 encoding=utf-8 :
2
3 r"""Command dispatcher.
4
5 This module provides a convenient and extensible command dispatching mechanism.
6 It's based on Zope or Cherrypy dispatching (but implemented from the scratch)
7 and translates commands to functions/objects/methods.
8 """
9
10 import re
11 import inspect
12 import logging ; log = logging.getLogger('pymin.dispatcher')
13
14 __all__ = ('Error', 'HandlerError', 'CommandNotFoundError', 'Handler',
15             'Dispatcher', 'handler', 'is_handler', 'get_help')
16
17 class Error(RuntimeError):
18     r"""Error(command) -> Error instance :: Base dispatching exceptions class.
19
20     All exceptions raised by the Dispatcher inherits from this one, so you can
21     easily catch any dispatching exception.
22
23     command - is the command that raised the exception, expressed as a list of
24               paths (or subcommands).
25     """
26
27     def __init__(self, message):
28         r"Initialize the Error object. See class documentation for more info."
29         self.message = message
30
31     def __unicode__(self):
32         return self.message
33
34     def __str__(self):
35         return unicode(self).encode('utf-8')
36
37 class HandlerError(Error):
38     r"""HandlerError(command) -> HandlerError instance :: Base handlers error.
39
40     All exceptions raised by the handlers should inherit from this one, so
41     dispatching errors could be separated from real programming errors (bugs).
42     """
43     pass
44
45 class CommandError(Error):
46     r"""CommandError(command) -> CommandError instance :: Base command error.
47
48     This exception is raised when there's a problem with the command itself.
49     It's the base class for all command (as a string) related error.
50     """
51
52     def __init__(self, command):
53         r"Initialize the object, see class documentation for more info."
54         self.command = command
55
56     def __unicode__(self):
57         return u'Error in command "%s".' % u' '.join(self.command)
58
59 class WrongArgumentsError(CommandError):
60     r"""WrongArgumentsError(handler, error) -> WrongArgumentsError instance.
61
62     This exception is raised when an empty command string is received.
63     """
64
65     def __init__(self, handler, error):
66         r"Initialize the object, see class documentation for more info."
67         self.handler = handler
68         self.error = error
69
70     args_re = re.compile(r'\w+\(\) takes (.+) (\d+) \w+ \((\d+) given\)')
71
72     extra_kw_re = re.compile(r'\w+\(\) got an unexpected keyword argument (.+)')
73
74     dup_kw_re = re.compile(r'\w+\(\) got multiple values for keyword argument '
75                             r"'(.+)'")
76
77     def format(self):
78         r"format() -> unicode - Format a TypeError to adapt it to a command."
79         m = self.args_re.match(unicode(self.error))
80         if m:
81             (quant, n_ok, n_bad)  = m.groups()
82             n_ok = int(n_ok)
83             n_bad = int(n_bad)
84             n_ok -= 1
85             n_bad -= 1
86             pl = ''
87             if n_ok != 1:
88                 pl = 's'
89             return u'takes %s %s argument%s, %s given' \
90                         % (quant, n_ok, pl, n_bad)
91         m = self.extra_kw_re.match(unicode(self.error))
92         if m:
93             (kw,)  = m.groups()
94             return u'got an unexpected keyword argument %s' % kw
95         m = self.dup_kw_re.match(unicode(self.error))
96         if m:
97             (kw,)  = m.groups()
98             return u'got multiple values for argument %s' % kw
99         return u'got wrong arguments'
100
101     def __unicode__(self):
102         return u'Command "%s" %s.' % (self.handler.__name__, self.format())
103
104 class CommandNotSpecifiedError(CommandError):
105     r"""CommandNotSpecifiedError() -> CommandNotSpecifiedError instance.
106
107     This exception is raised when an empty command string is received.
108     """
109
110     def __init__(self):
111         r"Initialize the object, see class documentation for more info."
112         pass
113
114     def __unicode__(self):
115         return u'Command not specified.'
116
117 class CommandIsAHandlerError(CommandError):
118     r"""CommandIsAHandlerError() -> CommandIsAHandlerError instance.
119
120     This exception is raised when a command is a handler containing commands
121     instead of a command itself.
122     """
123
124     def __unicode__(self):
125         command = ' '.join(self.command)
126         return u'"%s" is a handler, not a command (type "%s help" for help).' \
127                     % (command, command)
128
129 class CommandNotInHandlerError(CommandError):
130     r"""CommandNotInHandlerError() -> CommandNotInHandlerError instance.
131
132     This exception is raised when a command parent is a hanlder containing
133     commands, but the command itself is not found.
134     """
135
136     def __unicode__(self):
137         return u'Command "%(c)s" not found in handler "%(h)s" ' \
138                 u'(type "%(h)s help" for help).' \
139                         % dict(c=u' '.join(self.command[-1:]),
140                                 h=u' '.join(self.command[0:-1]))
141
142 class CommandNotFoundError(CommandError):
143     r"""CommandNotFoundError(command[, handler]) -> CommandNotFoundError object.
144
145     This exception is raised when the command received can't be dispatched
146     because there is no handlers to process it.
147     """
148
149     def __unicode__(self):
150         return u'Command "%s" not found.' % u' '.join(self.command)
151
152 class ParseError(CommandError):
153     r"""ParseError(command[, desc]) -> ParseError instance
154
155     This exception is raised when there is an error parsing a command.
156
157     command - Command that can't be parsed.
158
159     desc - Description of the error.
160     """
161
162     def __init__(self, command, desc="can't parse"):
163         r"""Initialize the object.
164
165         See class documentation for more info.
166         """
167         self.command = command
168         self.desc = desc
169
170     def __unicode__(self):
171         return u'Syntax error, %s: %s' % (self.desc, self.command)
172
173 class HelpNotFoundError(Error):
174     r"""HelpNotFoundError(command) -> HelpNotFoundError instance.
175
176     This exception is raised when a help command can't find the command
177     asked for help.
178     """
179
180     def __init__(self, command):
181         r"""Initialize the object.
182
183         See class documentation for more info.
184         """
185         self.command = command
186
187     def __unicode__(self):
188         return u"Can't get help for '%s', command not found." % self.command
189
190
191 def handler(help):
192     r"""handler(help) -> function wrapper :: Mark a callable as a handler.
193
194     This is a decorator to mark a callable object as a dispatcher handler.
195
196     help - Help string for the handler.
197     """
198     if not help:
199         raise TypeError("'help' should not be empty")
200     def make_wrapper(f):
201         log.debug('handler(): Decorating %s()', f.__name__)
202         # Here comes the tricky part:
203         # We need to make our wrapped function to accept any number of
204         # positional and keyword arguments, but checking for the correct
205         # arguments and raising an exception in case the arguments doesn't
206         # match.
207         # So we create a dummy function, with the same signature as the
208         # wrapped one, so we can check later (at "dispatch-time") if the
209         # real function call will be successful. If the dummy function don't
210         # raise a TypeError, the arguments are just fine.
211         env = dict()
212         argspec = inspect.getargspec(f)
213         signature = inspect.formatargspec(*argspec)
214         # The dummy function
215         exec "def f%s: pass" % signature in env
216         signature_check = env['f']
217         # The wrapper to check the signature at "dispatch-time"
218         def wrapper(*args, **kwargs):
219             # First we check if the arguments passed are OK.
220             try:
221                 signature_check(*args, **kwargs)
222             except TypeError, e:
223                 # If not, we raise an appropriate error.
224                 raise WrongArgumentsError(f, e)
225             # If they are fine, we call the real function
226             return f(*args, **kwargs)
227         # Some flag to mark our handlers for simple checks
228         wrapper._dispatcher_handler = True
229         # The help string we asked for in the first place =)
230         wrapper.handler_help = help
231         # We store the original signature for better help generation
232         wrapper.handler_argspec = argspec
233         # And some makeup, to make our wrapper look like the original function
234         wrapper.__name__ = f.__name__
235         wrapper.__dict__.update(f.__dict__)
236         # We add a hint in the documentation
237         wrapper.__doc__ = "Pymin handler with signature: %s%s" \
238                             % (wrapper.__name__, signature)
239         if f.__doc__ is not None:
240             wrapper.__doc__ += "\n\n" + f.__doc__
241         wrapper.__module__ = f.__module__
242         return wrapper
243     return make_wrapper
244
245 def is_handler(handler):
246     r"is_handler(handler) -> bool :: Tell if a object is a handler."
247     return callable(handler) and hasattr(handler, '_dispatcher_handler') \
248                 and handler._dispatcher_handler
249
250 class Handler:
251     r"""Handler() -> Handler instance :: Base class for all dispatcher handlers.
252
253     All dispatcher handlers should inherit from this class to have some extra
254     commands, like help. You should override the 'handler_help' attribute to a
255     nice help message describing the handler.
256     """
257
258     handler_help = u'Undocumented handler'
259
260     @handler(u'List available commands')
261     def commands(self):
262         r"""commands() -> generator :: List the available commands."""
263         return (a for a in dir(self) if is_handler(getattr(self, a)))
264
265     @handler(u'Show available commands with their help')
266     def help(self, command=None):
267         r"""help([command]) -> unicode/dict :: Show help on available commands.
268
269         If command is specified, it returns the help of that particular command.
270         If not, it returns a dictionary which keys are the available commands
271         and values are the help strings.
272         """
273         if command is None:
274             d = dict()
275             for a in dir(self):
276                 h = getattr(self, a)
277                 if a == 'parent': continue # Skip parents in SubHandlers
278                 if is_handler(h) or isinstance(h, Handler):
279                     d[a] = h.handler_help
280             return d
281         # A command was specified
282         if command == 'parent': # Skip parents in SubHandlers
283             raise HelpNotFoundError(command)
284         if not hasattr(self, command.encode('utf-8')):
285             raise HelpNotFoundError(command)
286         handler = getattr(self, command.encode('utf-8'))
287         if not is_handler(handler) and not hasattr(handler, 'handler_help'):
288             raise HelpNotFoundError(command)
289         return handler.handler_help
290
291     def handle_timer(self):
292         r"""handle_timer() -> None :: Do periodic tasks.
293
294         By default we do nothing but calling handle_timer() on subhandlers.
295         """
296         for a in dir(self):
297             if a == 'parent': continue # Skip parents in SubHandlers
298             h = getattr(self, a)
299             if isinstance(h, Handler):
300                 h.handle_timer()
301
302 def parse_command(command):
303     r"""parse_command(command) -> (args, kwargs) :: Parse a command.
304
305     This function parses a command and split it into a list of parameters. It
306     has a similar to bash commandline parser. Spaces are the basic token
307     separator but you can group several tokens into one by using (single or
308     double) quotes. You can escape the quotes with a backslash (\' and \"),
309     express a backslash literal using a double backslash (\\), use special
310     meaning escaped sequences (like \a, \n, \r, \b, \v) and use unescaped
311     single quotes inside a double quoted token or vice-versa. A special escape
312     sequence is provided to express a NULL/None value: \N and it should appear
313     always as a separated token.
314
315     Additionally it accepts keyword arguments. When an (not-escaped) equal
316     sign (=) is found, the argument is considered a keyword, and the next
317     argument it's interpreted as its value.
318
319     This function returns a tuple containing a list and a dictionary. The
320     first has the positional arguments, the second, the keyword arguments.
321
322     There is no restriction about the order, a keyword argument can be
323     followed by a positional argument and vice-versa. All type of arguments
324     are grouped in the list/dict returned. The order of the positional
325     arguments is preserved and if there are multiple keyword arguments with
326     the same key, the last value is the winner (all other values are lost).
327
328     The command should be a unicode string.
329
330     Examples:
331
332     >>> parse_command('hello world')
333     ([u'hello', u'world'], {})
334     >>> parse_command('hello planet=earth')
335     ([u'hello'], {'planet': u'earth'})
336     >>> parse_command('hello planet="third rock from the sun"')
337     ([u'hello'], {'planet': u'third rock from the sun'})
338     >>> parse_command(u'  planet="third rock from the sun" hello ')
339     ([u'hello'], {'planet': u'third rock from the sun'})
340     >>> parse_command(u'  planet="third rock from the sun" "hi, hello"'
341             '"how are you" ')
342     ([u'hi, hello', u'how are you'], {'planet': u'third rock from the sun'})
343     >>> parse_command(u'one two three "fourth number"=four')
344     ([u'one', u'two', u'three'], {'fourth number': u'four'})
345     >>> parse_command(u'one two three "fourth number=four"')
346     ([u'one', u'two', u'three', u'fourth number=four'], {})
347     >>> parse_command(u'one two three fourth\=four')
348     ([u'one', u'two', u'three', u'fourth=four'], {})
349     >>> parse_command(u'one two three fourth=four=five')
350     ([u'one', u'two', u'three'], {'fourth': u'four=five'})
351     >>> parse_command(ur'nice\nlong\n\ttext')
352     ([u'nice\nlong\n\ttext'], {})
353     >>> parse_command('=hello')
354     ([u'=hello'], {})
355     >>> parse_command(r'\thello')
356     ([u'\thello'], {})
357     >>> parse_command(r'hello \n')
358     ([u'hello', u'\n'], {})
359     >>> parse_command(r'hello \nmundo')
360     ([u'hello', u'\nmundo'], {})
361     >>> parse_command(r'test \N')
362     ([u'test', None], {})
363     >>> parse_command(r'\N')
364     ([None], {})
365     >>> parse_command(r'none=\N')
366     ([], {'none': None})
367     >>> parse_command(r'\N=none')
368     ([], {'\\N': 'none'})
369     >>> parse_command(r'Not\N')
370     ([u'Not\\N'], {})
371     >>> parse_command(r'\None')
372     ([u'\\None'], {})
373
374     This examples are syntax errors:
375     Missing quote: "hello world
376     Missing value: hello=
377     """
378     SEP, TOKEN, DQUOTE, SQUOTE, EQUAL = u' ', None, u'"', u"'", u'=' # states
379     separators = (u' ', u'\t', u'\v', u'\n') # token separators
380     escaped_chars = (u'a', u'n', u'r', u'b', u'v', u't') # escaped sequences
381     seq = []
382     dic = {}
383     buff = u''
384     escape = False
385     keyword = None
386     state = SEP
387     def register_token(buff, keyword, seq, dic):
388         if buff == r'\N':
389             buff = None
390         if keyword is not None:
391             dic[keyword.encode('utf-8')] = buff
392             keyword = None
393         else:
394             seq.append(buff)
395         buff = u''
396         return (buff, keyword)
397     for n, c in enumerate(command):
398         # Escaped character
399         if escape:
400             # Not yet registered the token
401             if state == SEP and buff:
402                 (buff, keyword) = register_token(buff, keyword, seq, dic)
403                 state = TOKEN
404             for e in escaped_chars:
405                 if c == e:
406                     buff += eval(u'"\\' + e + u'"')
407                     break
408             else:
409                 if c == 'N':
410                     buff += r'\N'
411                 else:
412                     buff += c
413             escape = False
414             continue
415         # Escaped sequence start
416         if c == u'\\':
417             escape = True
418             continue
419         # Looking for spaces
420         if state == SEP:
421             if c in separators:
422                 continue
423             if buff and n != 2: # Not the first item (even if was a escape seq)
424                 if c == EQUAL: # Keyword found
425                     keyword = buff
426                     buff = u''
427                     continue
428                 (buff, keyword) = register_token(buff, keyword, seq, dic)
429             state = TOKEN
430         # Getting a token
431         if state == TOKEN:
432             if c == DQUOTE:
433                 state = DQUOTE
434                 continue
435             if c == SQUOTE:
436                 state = SQUOTE
437                 continue
438             # Check if a keyword is added
439             if c == EQUAL and keyword is None and buff:
440                 keyword = buff
441                 buff = u''
442                 state = SEP
443                 continue
444             if c in separators:
445                 state = SEP
446                 continue
447             buff += c
448             continue
449         # Inside a double quote
450         if state == DQUOTE:
451             if c == DQUOTE:
452                 state = TOKEN
453                 continue
454             buff += c
455             continue
456         # Inside a single quote
457         if state == SQUOTE:
458             if c == SQUOTE:
459                 state = TOKEN
460                 continue
461             buff += c
462             continue
463         assert 0, u'Unexpected state'
464     if state == DQUOTE or state == SQUOTE:
465         raise ParseError(command, u'missing closing quote (%s)' % state)
466     if not buff and keyword is not None:
467         raise ParseError(command,
468                         u'keyword argument (%s) without value' % keyword)
469     if buff:
470         register_token(buff, keyword, seq, dic)
471     return (seq, dic)
472
473 class Dispatcher:
474     r"""Dispatcher([root]) -> Dispatcher instance :: Command dispatcher.
475
476     This class provides a modular and extensible dispatching mechanism. You
477     specify a root handler (probably as a pymin.dispatcher.Handler subclass),
478
479     The command can have arguments, separated by (any number of) spaces and
480     keyword arguments (see parse_command for more details).
481
482     The dispatcher tries to route the command as deeply as it can, passing
483     the other "path" components as arguments to the callable. To route the
484     command it inspects the callable attributes to find a suitable callable
485     attribute to handle the command in a more specific way, and so on.
486
487     Example:
488     >>> d = Dispatcher(dict(handler=some_handler))
489     >>> d.dispatch('handler attribute method arg1 "arg 2" arg=3')
490
491     If 'some_handler' is an object with an 'attribute' that is another
492     object which has a method named 'method', then
493     some_handler.attribute.method('arg1', 'arg 2', arg=3) will be called.
494     If some_handler is a function, then some_handler('attribute', 'method',
495     'arg1', 'arg 2', arg=3) will be called. The handler "tree" can be as
496     complex and deep as you want.
497
498     If some command can't be dispatched, a CommandError subclass is raised.
499     """
500
501     def __init__(self, root):
502         r"""Initialize the Dispatcher object.
503
504         See Dispatcher class documentation for more info.
505         """
506         log.debug(u'Dispatcher(%r)', root)
507         self.root = root
508
509     def dispatch(self, route):
510         r"""dispatch(route) -> None :: Dispatch a command string.
511
512         This method searches for a suitable callable object in the routes
513         "tree" and call it, or raises a CommandError subclass if the command
514         can't be dispatched.
515
516         route - *unicode* string with the command route.
517         """
518         log.debug('Dispatcher.dispatch(%r)', route)
519         command = list()
520         (route, kwargs) = parse_command(route)
521         log.debug(u'Dispatcher.dispatch: route=%r, kwargs=%r', route, kwargs)
522         if not route:
523             log.debug(u'Dispatcher.dispatch: command not specified')
524             raise CommandNotSpecifiedError()
525         handler = self.root
526         while not is_handler(handler):
527             log.debug(u'Dispatcher.dispatch: handler=%r, route=%r',
528                         handler, route)
529             if len(route) is 0:
530                 if isinstance(handler, Handler):
531                     log.debug(u'Dispatcher.dispatch: command is a handler')
532                     raise CommandIsAHandlerError(command)
533                 log.debug(u'Dispatcher.dispatch: command not found')
534                 raise CommandNotFoundError(command)
535             command.append(route[0])
536             log.debug(u'Dispatcher.dispatch: command=%r', command)
537             if route[0] == 'parent':
538                 log.debug(u'Dispatcher.dispatch: is parent => not found')
539                 raise CommandNotFoundError(command)
540             if not hasattr(handler, route[0].encode('utf-8')):
541                 if isinstance(handler, Handler) and len(command) > 1:
542                     log.debug(u'Dispatcher.dispatch: command not in handler')
543                     raise CommandNotInHandlerError(command)
544                 log.debug(u'Dispatcher.dispatch: command not found')
545                 raise CommandNotFoundError(command)
546             handler = getattr(handler, route[0].encode('utf-8'))
547             route = route[1:]
548         log.debug(u'Dispatcher.dispatch: %r is a handler, calling it with '
549                     u'route=%r, kwargs=%r', handler, route, kwargs)
550         r = handler(*route, **kwargs)
551         log.debug(u'Dispatcher.dispatch: handler returned %s', r)
552         return r
553
554
555 if __name__ == '__main__':
556
557     logging.basicConfig(
558         level   = logging.DEBUG,
559         format  = '%(asctime)s %(levelname)-8s %(message)s',
560         datefmt = '%H:%M:%S',
561     )
562
563     @handler(u"test: Print all the arguments, return nothing")
564     def test_func(*args):
565         print 'func:', args
566
567     class TestClassSubHandler(Handler):
568         @handler(u"subcmd: Print all the arguments, return nothing")
569         def subcmd(self, *args):
570             print 'class.subclass.subcmd:', args
571
572     class TestClass(Handler):
573         @handler(u"cmd1: Print all the arguments, return nothing")
574         def cmd1(self, *args):
575             print 'class.cmd1:', args
576         @handler(u"cmd2: Print all the arguments, return nothing")
577         def cmd2(self, arg1, arg2):
578             print 'class.cmd2:', arg1, arg2
579         subclass = TestClassSubHandler()
580
581     class RootHandler(Handler):
582         func = staticmethod(test_func)
583         inst = TestClass()
584
585     d = Dispatcher(RootHandler())
586
587     r = d.dispatch(r'''func arg1 arg2 arg3 "fourth 'argument' with \", a\ttab and\n\\n"''')
588     assert r is None
589     r = list(d.dispatch('inst commands'))
590     r.sort()
591     assert r == ['cmd1', 'cmd2', 'commands', 'help']
592     print 'inst commands:', r
593     r = d.dispatch('inst help')
594     assert r == {
595                 'commands': u'List available commands',
596                 'subclass': u'Undocumented handler',
597                 'cmd1': u'cmd1: Print all the arguments, return nothing',
598                 'cmd2': u'cmd2: Print all the arguments, return nothing',
599                 'help': u'Show available commands with their help'
600     }
601     print 'inst help:', r
602     r = d.dispatch('inst cmd1 arg1 arg2 arg3 arg4')
603     assert r is None
604     r = d.dispatch('inst cmd2 arg1 arg2')
605     assert r is None
606     r = d.dispatch('inst subclass help')
607     assert r == {
608                 'subcmd': u'subcmd: Print all the arguments, return nothing',
609                 'commands': u'List available commands',
610                 'help': u'Show available commands with their help'
611     }
612     print 'inst subclass help:', r
613     r = d.dispatch('inst subclass subcmd arg1 arg2 arg3 arg4 arg5')
614     assert r is None
615     try:
616         d.dispatch('')
617         assert False, 'It should raised a CommandNotSpecifiedError'
618     except CommandNotSpecifiedError, e:
619         print 'Not found:', e
620     try:
621         d.dispatch('sucutrule piquete culete')
622         assert False, 'It should raised a CommandNotFoundError'
623     except CommandNotFoundError, e:
624         print 'Not found:', e
625     try:
626         d.dispatch('inst cmd3 arg1 arg2 arg3')
627         assert False, 'It should raised a CommandNotInHandlerError'
628     except CommandNotInHandlerError, e:
629         print 'Not found:', e
630     try:
631         d.dispatch('inst')
632         assert False, 'It should raised a CommandIsAHandlerError'
633     except CommandIsAHandlerError, e:
634         print 'Not found:', e
635     try:
636         d.dispatch('inst cmd2 "just one arg"')
637         assert False, 'It should raised a WrongArgumentsError'
638     except WrongArgumentsError, e:
639         print 'Bad arguments:', e
640     try:
641         d.dispatch('inst cmd2 arg1 arg2 "an extra argument"')
642         assert False, 'It should raised a WrongArgumentsError'
643     except WrongArgumentsError, e:
644         print 'Bad arguments:', e
645     try:
646         d.dispatch('inst cmd2 arg1 arg2 arg3="unexpected keyword arg"')
647         assert False, 'It should raised a WrongArgumentsError'
648     except WrongArgumentsError, e:
649         print 'Bad arguments:', e
650     try:
651         d.dispatch('inst cmd2 arg1 arg2 arg2="duplicated keyword arg"')
652         assert False, 'It should raised a WrongArgumentsError'
653     except WrongArgumentsError, e:
654         print 'Bad arguments:', e
655     print
656     print
657
658     # Parser tests
659     p = parse_command('hello world')
660     assert p == ([u'hello', u'world'], {}), p
661     p = parse_command('hello planet=earth')
662     assert p  == ([u'hello'], {'planet': u'earth'}), p
663     p = parse_command('hello planet="third rock from the sun"')
664     assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
665     p = parse_command(u'  planet="third rock from the sun" hello ')
666     assert p == ([u'hello'], {'planet': u'third rock from the sun'}), p
667     p = parse_command(u'  planet="third rock from the sun" "hi, hello" '
668                             '"how are you" ')
669     assert p == ([u'hi, hello', u'how are you'],
670                 {'planet': u'third rock from the sun'}), p
671     p = parse_command(u'one two three "fourth number"=four')
672     assert p == ([u'one', u'two', u'three'], {'fourth number': u'four'}), p
673     p = parse_command(u'one two three "fourth number=four"')
674     assert p == ([u'one', u'two', u'three', u'fourth number=four'], {}), p
675     p = parse_command(u'one two three fourth\=four')
676     assert p == ([u'one', u'two', u'three', u'fourth=four'], {}), p
677     p = parse_command(u'one two three fourth=four=five')
678     assert p == ([u'one', u'two', u'three'], {'fourth': u'four=five'}), p
679     p = parse_command(ur'nice\nlong\n\ttext')
680     assert p == ([u'nice\nlong\n\ttext'], {}), p
681     p = parse_command('=hello')
682     assert p == ([u'=hello'], {}), p
683     p = parse_command(r'\thello')
684     assert p == ([u'\thello'], {}), p
685     p = parse_command(r'hello \n')
686     assert p == ([u'hello', u'\n'], {}), p
687     p = parse_command(r'hello \nmundo')
688     assert p == ([u'hello', u'\nmundo'], {}), p
689     p = parse_command(r'test \N')
690     assert p == ([u'test', None], {}), p
691     p = parse_command(r'\N')
692     assert p == ([None], {}), p
693     p = parse_command(r'none=\N')
694     assert p == ([], {'none': None}), p
695     p = parse_command(r'\N=none')
696     assert p == ([], {'\\N': 'none'}), p
697     p = parse_command(r'Not\N')
698     assert p == ([u'Not\\N'], {}), p
699     p = parse_command(r'\None')
700     assert p == ([u'\\None'], {}), p
701     try:
702         p = parse_command('hello=')
703         assert False, p + ' should raised a ParseError'
704     except ParseError, e:
705         pass
706     try:
707         p = parse_command('"hello')
708         assert False, p + ' should raised a ParseError'
709     except ParseError, e:
710         pass
711