Commits

Andy Mikhailenko committed 95d4a24

Added dispatch_command() shortcut function. Added documentation. This finally fixes #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()
 
 __all__ = [
     'ArghParser', 'add_commands', 'autocomplete', 'confirm', 'dispatch',
-    'set_default_command', 'wrap_errors'
+    '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)
         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:
         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,
              raw_output=False, namespace=None):
     :func:`autocomplete` and :func:`dispatch`.
     """
     def set_default_command(self, *args, **kwargs):
-        "Wrapper for :func:`set_command`."
+        "Wrapper for :func:`set_default_command`."
         return set_default_command(self, *args, **kwargs)
 
     def add_commands(self, *args, **kwargs):

docs/tutorial.rst

 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')
         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):