Andy Mikhailenko avatar Andy Mikhailenko committed fb91ba7

Added dispatch argument `output_file` (issue #10), deprecated argument `intercept`. Added tests.

Comments (0)

Files changed (3)

-version = '0.11.0'
+version = '0.12.0'
 """
 import argparse
 import locale
+from StringIO import StringIO
 import sys
 from types import GeneratorType
 
 
 def dispatch(parser, argv=None, add_help_command=True, encoding=None,
              intercept=False, completion=True, pre_call=None,
-             raw_output=False):
+             output_file=sys.stdout, raw_output=False):
     """Parses given list of arguments using given parser, calls the relevant
     function and prints the result.
 
         Encoding for results. If `None`, it is determined automatically.
         Default is `None`.
 
-    :param intercept:
+    :param output_file:
 
-        If `True`, results are returned as strings. If `False`, results are
-        printed to stdout. Default is `False`.
+        A file-like object for output. If `None`, the resulting lines are
+        collected and returned as a string. Default is ``sys.stdout``.
 
     :param raw_output:
 
     exceptions is :class:`CommandError` which is interpreted as an expected
     event so the traceback is hidden.
     """
+    # TODO: can be safely removed at version ~= 0.14
+    if intercept: # PRAGMA: NOCOVER
+        import warnings
+        warnings.warn('dispatch(intercept=True) is deprecated, use '
+                      'dispatch(output_file=None).', DeprecationWarning)
+        output_file = None
+
     if completion:
         autocomplete(parser)
 
     args = parser.parse_args(argv)
 
     if hasattr(args, 'function'):
-        if pre_call:
+        if pre_call:  # XXX undocumented because I'm unsure if it's OK
             pre_call(args)
         lines = _execute_command(args)
     else:
-        # no commands at all; displaying help message
+        # no commands declared, can't dispatch; display help message
         lines = [parser.format_usage()]
 
-    buf = []
+    if output_file is None:
+        # user wants a string; we create an internal temporary file-like object
+        # and will return its contents as a string
+        f = StringIO()
+    else:
+        # normally this is stdout; can be any file
+        f = output_file
 
     for line in lines:
-        if intercept:
-            buf.append(line)
-        else:
-            # print the line as soon as it is generated to ensure that it is
-            # displayed to the user before anything else happens, e.g.
-            # raw_input() is called
-            output = _encode(line, encoding)
-            if raw_output:
-                sys.stdout.write(output)
-            else:
-                print output
-    if buf:
-        return '\n'.join(buf)
+        # print the line as soon as it is generated to ensure that it is
+        # displayed to the user before anything else happens, e.g.
+        # raw_input() is called
+        output = _encode(line, f, encoding)
+        f.write(output)
+        if not raw_output:
+            # in most cases user wants on message per line
+            f.write('\n')
 
-def _encode(line, encoding=None):
+    if output_file is None:
+        # user wanted a string; return contents of out temporary file-like obj
+        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.
     """
     # Choose output encoding
     if not encoding:
         # choose between terminal's and system's preferred encodings
-        if sys.stdout.isatty():
-            encoding = sys.stdout.encoding
-        else:
-            encoding = locale.getpreferredencoding()
+        if output_file.isatty():
+            encoding = getattr(output_file, 'encoding', None)
+        encoding = encoding or locale.getpreferredencoding()
 
     # Convert string from Unicode to the output encoding
     return line.encode(encoding)
 # -*- coding: utf-8 -*-
 
 import sys
+from StringIO import StringIO
 import unittest2 as unittest
 import argparse
 import argh.helpers
 from argh import (
-    alias, ArghParser, arg, add_commands, dispatch, plain_signature
+    alias, ArghParser, arg, add_commands, CommandError, dispatch,
+    plain_signature
 )
 from argh import completion
 
 def foo_bar(args):
     return args.foo, args.bar
 
+def whiner(args):
+    yield 'Hello...'
+    raise CommandError('I feel depressed.')
 
-class ArghTestCase(unittest.TestCase):
+
+class BaseArghTestCase(unittest.TestCase):
+    commands = {}
+
     def setUp(self):
-        #self.parser = build_parser(echo, plain_echo, foo=[hello, howdy])
         self.parser = DebugArghParser('PROG')
-        self.parser.add_commands([echo, plain_echo, foo_bar, do_aliased])
-        self.parser.add_commands([hello, howdy], namespace='greet')
+        for namespace, commands in self.commands.iteritems():
+            self.parser.add_commands(commands, namespace=namespace)
 
-    def _call_cmd(self, command_string):
-        args = command_string.split() if command_string else command_string
-        return self.parser.dispatch(args, intercept=True)
+    def _call_cmd(self, command_string, **kwargs):
+        if isinstance(command_string, basestring):
+            args = command_string.split()
+        else:
+            args = command_string
 
-    def assert_cmd_returns(self, command_string, expected_result):
+        io = StringIO()
+        if 'output_file' not in kwargs:
+            kwargs['output_file'] = io
+
+        result = self.parser.dispatch(args, **kwargs)
+
+        if kwargs.get('output_file') is None:
+            return result
+        else:
+            io.seek(0)
+            return io.read()
+
+    def assert_cmd_returns(self, command_string, expected_result, **kwargs):
         """Executes given command using given parser and asserts that it prints
         given value.
         """
         try:
-            result = self._call_cmd(command_string)
+            result = self._call_cmd(command_string, **kwargs)
         except SystemExit:
-            self.fail('Argument parsing failed for {0}'.format(command_string))
+            self.fail('Argument parsing failed for {0}'.format(repr(command_string)))
         self.assertEqual(result, expected_result)
 
     def assert_cmd_exits(self, command_string, message_regex=None):
         """
         result = self.assert_cmd_exits(command_string)
 
+
+class ArghTestCase(BaseArghTestCase):
+    commands = {
+        None: [echo, plain_echo, foo_bar, do_aliased, whiner],
+        'greet': [hello, howdy]
+    }
+
     def test_argv(self):
         _argv = sys.argv
         sys.argv = sys.argv[:1] + ['echo', 'hi there']
-        self.assert_cmd_returns(None, 'you said hi there')
+        self.assert_cmd_returns(None, 'you said hi there\n')
         sys.argv = _argv
 
+    def test_no_command(self):
+        self.assert_cmd_fails('', 'too few arguments')
+
     def test_invalid_choice(self):
         self.assert_cmd_fails('whatchamacallit', '^invalid choice')
 
     def test_echo(self):
         "A simple command is resolved to a function."
-        self.assert_cmd_returns('echo foo', 'you said foo')
+        self.assert_cmd_returns('echo foo', 'you said foo\n')
 
     def test_bool_action(self):
         "Action `store_true`/`store_false` is inferred from default value."
-        self.assert_cmd_returns('echo --twice foo', 'you said fooyou said foo')
+        self.assert_cmd_returns('echo --twice foo', 'you said fooyou said foo\n')
 
     def test_plain_signature(self):
         "Arguments can be passed to the function without a Namespace instance."
-        self.assert_cmd_returns('plain-echo bar', 'you said bar')
+        self.assert_cmd_returns('plain-echo bar', 'you said bar\n')
 
     def test_bare_namespace(self):
         "A command can be resolved to a function, not a namespace."
 
     def test_namespaced_function(self):
         "A subcommand is resolved to a function."
-        self.assert_cmd_returns('greet hello', u'Hello world!')
-        self.assert_cmd_returns('greet hello --name=John', u'Hello John!')
+        self.assert_cmd_returns('greet hello', u'Hello world!\n')
+        self.assert_cmd_returns('greet hello --name=John', u'Hello John!\n')
         self.assert_cmd_fails('greet hello John', 'unrecognized arguments')
         self.assert_cmd_fails('greet howdy --name=John', 'too few arguments')
-        self.assert_cmd_returns('greet howdy John', u'Howdy John?')
+        self.assert_cmd_returns('greet howdy John', u'Howdy John?\n')
 
     def test_alias(self):
-        self.assert_cmd_returns('aliased', 'ok')
+        self.assert_cmd_returns('aliased', 'ok\n')
 
     def test_help_alias(self):
         self.assert_cmd_doesnt_fail('--help')
         """Positional arguments are resolved in the order in which the @arg
         decorators are defined.
         """
-        self.assert_cmd_returns('foo-bar foo bar', 'foo\nbar')
+        self.assert_cmd_returns('foo-bar foo bar', 'foo\nbar\n')
+
+    def test_raw_output(self):
+        "If the raw_output flag is set, no extra whitespace is added"
+        self.assert_cmd_returns('foo-bar foo bar', 'foo\nbar\n')
+        self.assert_cmd_returns('foo-bar foo bar', 'foobar', raw_output=True)
+
+    def test_output_file(self):
+        self.assert_cmd_returns('greet hello', 'Hello world!\n')
+        self.assert_cmd_returns('greet hello', 'Hello world!\n', output_file=None)
+
+    def test_command_error(self):
+        self.assert_cmd_returns('whiner', 'Hello...\nI feel depressed.\n')
+
+
+class NoCommandsTestCase(BaseArghTestCase):
+    "Edge case: no commands defined"
+    commands = {}
+    def test_no_command(self):
+        self.assert_cmd_returns('', self.parser.format_usage(), raw_output=True)
+        self.assert_cmd_returns('', self.parser.format_usage()+'\n')
 
 
 class ConfirmTestCase(unittest.TestCase):
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.