Commits

Andy Mikhailenko committed f2ac93b Merge

Merged branch re #6.

Comments (0)

Files changed (4)

 
 A very simple application with one command::
 
+    from argh import *
+
+    @dispatch_command
+    @arg('name')
+    def main(args):
+        return 'Hello ' + args.name
+
+An application with multiple commands::
+
     @command
     def echo(text='hello'):
         print text
 
+    @command
+    def another_echo(text='hi there'):
+        print text
+
     parser = ArghParser()
-    parser.add_commands([echo])
+    parser.add_commands([echo, another_echo])
 
     if __name__ == '__main__':
         parser.dispatch()
     ATTR_ALIAS, ATTR_ARGS, ATTR_NO_NAMESPACE, ATTR_WRAPPED_EXCEPTIONS
 )
 
+
 if PY3:
     def raw_input(text):
         return input(text.decode())
 
+
 __all__ = [
-    'ArghParser', 'add_commands', 'autocomplete', 'dispatch', 'confirm',
-    'wrap_errors'
+    'ArghParser', 'add_commands', 'autocomplete', 'confirm', 'dispatch',
+    'dispatch_command', 'set_default_command', 'wrap_errors'
 ]
+
+
+def set_default_command(parser, function):
+    """ Sets default command (i.e. a function) for given parser.
+
+    .. note::
+
+       An attempt to set default command to a parser which already has
+       subparsers (e.g. added with :func:`~argh.helpers.add_commands`)
+       results in a `RuntimeError`.
+
+    """
+    if parser._subparsers:
+        raise RuntimeError('Cannot set default command to a parser with '
+                           'existing subparsers')
+
+    for a_args, a_kwargs in getattr(function, ATTR_ARGS, []):
+        parser.add_argument(*a_args, **a_kwargs)
+    parser.set_defaults(function=function)
+
+
 def add_commands(parser, functions, namespace=None, title=None,
                  description=None, help=None):
     """Adds given functions as commands to given parser.
         stable. If some implementation details would change and break `argh`,
         we'll simply add a workaround a keep it compatibile.
 
+    .. note::
+
+       An attempt to add commands to a parser which already has a default
+       function (e.g. added with :func:`~argh.helpers.set_default_command`)
+       results in a `RuntimeError`.
+
     """
+    if 'function' in parser._defaults:
+        raise RuntimeError('Cannot add commands to a single-command parser')
+
     subparsers = get_subparsers(parser, create=True)
 
     if namespace:
         cmd_name = getattr(func, ATTR_ALIAS, func.__name__.replace('_','-'))
         cmd_help = func.__doc__
         command_parser = subparsers.add_parser(cmd_name, help=cmd_help)
-        for a_args, a_kwargs in getattr(func, ATTR_ARGS, []):
-            command_parser.add_argument(*a_args, **a_kwargs)
-        command_parser.set_defaults(function=func)
+        set_default_command(command_parser, func)
+
+
+def dispatch_command(function, *args, **kwargs):
+    """ A wrapper for :func:`dispatch` that creates a one-command parser.
+
+    This::
+
+        @command
+        def foo():
+            return 1
+
+        dispatch_command(foo)
+
+    ...is a shortcut for::
+
+        @command
+        def foo():
+            return 1
+
+        parser = ArgumentParser()
+        set_default_command(parser, foo)
+        dispatch(parser)
+
+    This function can also be used as a decorator. Here's a more or less
+    sensible example::
+
+        from argh import *
+
+        @dispatch_command
+        @arg('name')
+        def main(args):
+            return args.name
+
+    """
+    parser = argparse.ArgumentParser()
+    set_default_command(parser, function)
+    dispatch(parser, *args, **kwargs)
+
 
 def dispatch(parser, argv=None, add_help_command=True, encoding=None,
              completion=True, pre_call=None, output_file=sys.stdout,
         f.seek(0)
         return f.read()
 
+
 def _encode(line, output_file, encoding=None):
     """Converts given string to given encoding. If no encoding is specified, it
     is determined from terminal settings or, if none, from system settings.
     # Convert string from Unicode to the output encoding
     return line.encode(encoding)
 
+
 def _execute_command(args):
     """Asserts that ``args.function`` is present and callable. Tries different
     approaches to calling the function (with an `argparse.Namespace` object or
     wrappers for stand-alone functions :func:`add_commands` ,
     :func:`autocomplete` and :func:`dispatch`.
     """
+    def set_default_command(self, *args, **kwargs):
+        "Wrapper for :func:`set_default_command`."
+        return set_default_command(self, *args, **kwargs)
+
     def add_commands(self, *args, **kwargs):
         "Wrapper for :func:`add_commands`."
         return add_commands(self, *args, **kwargs)
         return default
     return None
 
+
 def wrap_errors(*exceptions):
     """Decorator. Wraps given exceptions into :class:`CommandError`. Usage::
 
 Defining and running commands is dead simple::
 
     from argh import *
-    
+
+    @dispatch_command
+    def main(args):
+        print 'Hello'
+
+That's it. And it works::
+
+    $ python script.py
+    Hello
+
+Nice for a quick'n'dirty script. A reusable app would look closer to this::
+
+    from argh import *
+
+    def main(args):
+        print 'Hello'
+
+    if __name__ == '__main__':
+        dispatch_command(main)
+
+...and here's a bit more complex example (still pretty readable)::
+
+    from argh import *
+
     @command
     def load(path, format='json'):
         print loaders[format].load(path)
     $ ./prog.py www serve-rest
     $ ./prog.py www serve --port 6060 --noreload
 
+Single-command application
+--------------------------
+
+There are cases when the application performs a single task and it perfectly
+maps to a single command. The method above would require the user to type a
+command like ``check_mail.py check --now`` while ``check_mail.py --now`` would
+suffice. In such cases :func:`~argh.helpers.add_commands` should be replaced with
+:func:`~argh.helpers.set_default_command`::
+
+    def main(args):
+        return 1
+
+    parser = ArghParser()
+    parser.set_default_command(main)
+
+There's also a nice shortcut :func:`~argh.helpers.dispatch_command`.
+Please refer to the API documentation for details.
+
 Subparsers
 ----------
 
 import argparse
 import argh.helpers
 from argh import (
-    alias, ArghParser, arg, command, CommandError,
+    alias, ArghParser, arg, command, CommandError, dispatch_command,
     plain_signature, wrap_errors
 )
 from argh import completion
         self.assert_cmd_returns('', b(self.parser.format_usage()+'\n'))
 
 
+class DefaultCommandTestCase(BaseArghTestCase):
+    def setUp(self):
+        self.parser = DebugArghParser('PROG')
+
+        @arg('--foo', default=1)
+        def main(args):
+            return args.foo
+
+        self.parser.set_default_command(main)
+
+    def test_default_command(self):
+        self.assert_cmd_returns('', b('1\n'))
+        self.assert_cmd_returns('--foo 2', b('2\n'))
+        self.assert_cmd_exits('--help')
+
+    def test_prevent_conflict_with_single_command(self):
+        def one(args): return 1
+        def two(args): return 2
+
+        p = DebugArghParser('PROG')
+        p.set_default_command(one)
+        with self.assertRaisesRegexp(RuntimeError,
+                               'Cannot add commands to a single-command parser'):
+            p.add_commands([two])
+
+    def test_prevent_conflict_with_subparsers(self):
+        def one(args): return 1
+        def two(args): return 2
+
+        p = DebugArghParser('PROG')
+        p.add_commands([one])
+        with self.assertRaisesRegexp(RuntimeError,
+                               'Cannot set default command to a parser with '
+                               'existing subparsers'):
+            p.set_default_command(two)
+
+
+class DispatchCommandTestCase(BaseArghTestCase):
+
+    def _dispatch_and_capture(self, func, command_string, **kwargs):
+        if isinstance(command_string, string_types):
+            args = command_string.split()
+        else:
+            args = command_string
+
+        io = BytesIO()
+        if 'output_file' not in kwargs:
+            kwargs['output_file'] = io
+
+        result = dispatch_command(func, args, **kwargs)
+
+        if kwargs.get('output_file') is None:
+            return result
+        else:
+            io.seek(0)
+            return io.read()
+
+    def assert_cmd_returns(self, func, command_string, expected_result, **kwargs):
+        """Executes given command using given parser and asserts that it prints
+        given value.
+        """
+        try:
+            result = self._dispatch_and_capture(func, command_string, **kwargs)
+        except SystemExit as error:
+            self.fail('Argument parsing failed for {0!r}: {1!r}'.format(
+                command_string, error))
+        self.assertEqual(result, expected_result)
+
+    def test_dispatch_command_shortcut(self):
+
+        @arg('--foo', default=1)
+        def main(args):
+            return args.foo
+
+        self.assert_cmd_returns(main, '', b('1\n'))
+        self.assert_cmd_returns(main, '--foo 2', b('2\n'))
+
+
 class ConfirmTestCase(unittest.TestCase):
     def assert_choice(self, choice, expected, **kwargs):
         argh.helpers.raw_input = lambda prompt: choice