Commits

Andy Mikhailenko committed a1d2580

Fix issue #23: refactoring.

  • Participants
  • Parent commits 5b2e1da

Comments (0)

Files changed (9)

 .. automodule:: argh.decorators
    :members:
 
+.. automodule:: argh.assembling
+   :members:
+
+.. automodule:: argh.dispatching
+   :members:
+
+.. automodule:: argh.completion
+   :members:
+
 .. automodule:: argh.helpers
    :members:
 
 .. automodule:: argh.exceptions
    :members:
 
+.. automodule:: argh.output
+   :members:
+
+.. automodule:: argh.utils
+   :members:
+
 """
+from .assembling import *
+from .decorators import *
+from .dispatching import *
 from .exceptions import *
+from .interaction import *
 from .helpers import *
-from .decorators import *
 
-__version__ = '0.18.0'
+
+__version__ = '0.19.0'

argh/assembling.py

+# -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2010—2012 Andrey Mikhailenko and contributors
+#
+#  This file is part of Argh.
+#
+#  Argh is free software under terms of the GNU Lesser
+#  General Public License version 3 (LGPLv3) as published by the Free
+#  Software Foundation. See the file README for copying conditions.
+#
+"""
+Assembling
+==========
+
+Functions and classes to properly assemble your commands in a parser.
+"""
+from argh.six import string_types
+from argh.constants import ATTR_ALIAS, ATTR_ARGS
+from argh.utils import get_subparsers
+
+
+__all__ = ['set_default_command', 'add_commands']
+
+
+def set_default_command(parser, function):
+    """ Sets default command (i.e. a function) for given parser.
+
+    If `parser.description` is empty and the function has a docstring, it is
+    used as the description.
+
+    .. 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)
+    if function.__doc__ and not parser.description:
+        parser.description = function.__doc__
+    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.
+
+    :param parser:
+
+        an :class:`argparse.ArgumentParser` instance.
+
+    :param functions:
+
+        a list of functions. A subparser is created for each of them. If the
+        function is decorated with :func:`arg`, the arguments are passed to
+        the :class:`~argparse.ArgumentParser.add_argument` method of the
+        parser. See also :func:`dispatch` for requirements concerning function
+        signatures. The command name is inferred from the function name. Note
+        that the underscores in the name are replaced with hyphens, i.e.
+        function name "foo_bar" becomes command name "foo-bar".
+
+    :param namespace:
+
+        an optional string representing the group of commands. For example, if
+        a command named "hello" is added without the namespace, it will be
+        available as "prog.py hello"; if the namespace if specified as "greet",
+        then the command will be accessible as "prog.py greet hello". The
+        namespace itself is not callable, so "prog.py greet" will fail and only
+        display a help message.
+
+    Help message for a namespace can be also tuned with these params (provided
+    that you specify the `namespace`):
+
+    :param title:
+
+        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as `title`.
+
+    :param description:
+
+        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as
+        `description`.
+
+    :param help:
+
+        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as `help`.
+
+    .. note::
+
+        This function modifies the parser object. Generally side effects are
+        bad practice but we don't seem to have any choice as ArgumentParser is
+        pretty opaque. You may prefer :class:`ArghParser.add_commands` for a
+        bit more predictable API.
+
+    .. admonition:: Design flaw
+
+        This function peeks into the parser object using its internal API.
+        Unfortunately the public API does not allow to *get* the subparsers, it
+        only lets you *add* them, and do that *once*. So you would have to toss
+        the subparsers object around to add something later. That said, I doubt
+        that argparse will change a lot in the future as it's already pretty
+        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:
+        # make a namespace placeholder and register the commands within it
+        assert isinstance(namespace, string_types)
+        subsubparser = subparsers.add_parser(namespace, help=title)
+        subparsers = subsubparser.add_subparsers(title=title,
+                                                 description=description,
+                                                 help=help)
+    else:
+        assert not any([title, description, help]), (
+            'Arguments "title", "description" or "extra_help" only make sense '
+            'if provided along with a namespace.')
+
+    for func in functions:
+        # XXX we could add multiple aliases here but it's a bit of a hack
+        cmd_name = getattr(func, ATTR_ALIAS, func.__name__.replace('_','-'))
+        command_parser = subparsers.add_parser(cmd_name)
+        set_default_command(command_parser, func)

argh/decorators.py

 """
 import inspect
 
-from argh.constants import ATTR_ALIAS, ATTR_ARGS, ATTR_NO_NAMESPACE
+from argh.constants import (ATTR_ALIAS, ATTR_ARGS, ATTR_NO_NAMESPACE,
+                            ATTR_WRAPPED_EXCEPTIONS)
 
 
-__all__ = ['alias', 'arg', 'command', 'plain_signature']
+__all__ = ['alias', 'arg', 'command', 'plain_signature', 'wrap_errors']
 
 
 def alias(name):
         return func
     return wrapper
 
-def generator(func):  # pragma: no cover
-    """
-    .. warning::
-
-        This decorator is deprecated. Argh can detect whether the result is a
-        generator without explicit decorators.
-
-    """
-    import warnings
-    warnings.warn('Decorator @generator is deprecated. The commands can still '
-                  'return generators.', DeprecationWarning)
-    return func
 
 def plain_signature(func):
     """Marks that given function expects ordinary positional and named
     setattr(func, ATTR_NO_NAMESPACE, True)
     return func
 
+
 def arg(*args, **kwargs):
     """Declares an argument for given function. Does not register the function
     anywhere, nor does it modify the function in any way. The signature is
         return func
     return wrapper
 
+
 def command(func):
     """Infers argument specifications from given function. Wraps the function
     in the :func:`plain_signature` decorator and also in an :func:`arg`
             func = arg(a)(func)
 
     return func
+
+
+def wrap_errors(*exceptions):
+    """Decorator. Wraps given exceptions into :class:`CommandError`. Usage::
+
+        @arg('-x')
+        @arg('-y')
+        @wrap_errors(AssertionError)
+        def foo(args):
+            assert args.x or args.y, 'x or y must be specified'
+
+    If the assertion fails, its message will be correctly printed and the
+    stack hidden. This helps to avoid boilerplate code.
+    """
+    def wrapper(func):
+        setattr(func, ATTR_WRAPPED_EXCEPTIONS, exceptions)
+        return func
+    return wrapper

argh/dispatching.py

+# -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2010—2012 Andrey Mikhailenko and contributors
+#
+#  This file is part of Argh.
+#
+#  Argh is free software under terms of the GNU Lesser
+#  General Public License version 3 (LGPLv3) as published by the Free
+#  Software Foundation. See the file README for copying conditions.
+#
+"""
+Dispatching
+==========
+"""
+import argparse
+import sys
+from types import GeneratorType
+
+from argh.six import text_type, BytesIO, StringIO, PY3
+
+from argh.constants import ATTR_NO_NAMESPACE, ATTR_WRAPPED_EXCEPTIONS
+from argh.completion import autocomplete
+from argh.assembling import add_commands, set_default_command
+from argh.exceptions import CommandError
+from argh.output import encode_output
+
+
+__all__ = ['dispatch', 'dispatch_command', 'dispatch_commands']
+
+
+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):
+    """Parses given list of arguments using given parser, calls the relevant
+    function and prints the result.
+
+    The target function should expect one positional argument: the
+    :class:`argparse.Namespace` object. However, if the function is decorated with
+    :func:`plain_signature`, the positional and named arguments from the
+    namespace object are passed to the function instead of the object itself.
+
+    :param parser:
+
+        the ArgumentParser instance.
+
+    :param argv:
+
+        a list of strings representing the arguments. If `None`, ``sys.argv``
+        is used instead. Default is `None`.
+
+    :param add_help_command:
+
+        if `True`, converts first positional argument "help" to a keyword
+        argument so that ``help foo`` becomes ``foo --help`` and displays usage
+        information for "foo". Default is `True`.
+
+    :param encoding:
+
+        Encoding for results. If `None`, it is determined automatically.
+        Default is `None`.
+
+    :param output_file:
+
+        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:
+
+        If `True`, results are written to the output file raw, without adding
+        whitespaces or newlines between yielded strings. Default is `False`.
+
+    :param completion:
+
+        If `True`, shell tab completion is enabled. Default is `True`. (You
+        will also need to install it.)
+
+    By default the exceptions are not wrapped and will propagate. The only
+    exception that is always wrapped is :class:`CommandError` which is
+    interpreted as an expected event so the traceback is hidden. You can also
+    mark arbitrary exceptions as "wrappable" by using the :func:`wrap_errors`
+    decorator.
+    """
+    if completion:
+        autocomplete(parser)
+
+    if argv is None:
+        argv = sys.argv[1:]
+
+    if add_help_command:
+        if argv and argv[0] == 'help':
+            argv.pop(0)
+            argv.append('--help')
+
+    # this will raise SystemExit if parsing fails
+    args = parser.parse_args(argv, namespace=namespace)
+
+    if hasattr(args, 'function'):
+        if pre_call:  # XXX undocumented because I'm unsure if it's OK
+            # Actually used in real projects:
+            # * https://google.com/search?q=argh+dispatch+pre_call
+            # * https://github.com/madjar/aurifere/blob/master/aurifere/cli.py#L92
+            pre_call(args)
+        lines = _execute_command(args)
+    else:
+        # no commands declared, can't dispatch; display help message
+        lines = [parser.format_usage()]
+
+    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() if PY3 else BytesIO()
+    else:
+        # normally this is stdout; can be any file
+        f = output_file
+
+    for line in lines:
+        # 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_output(line, f, encoding) or ''
+        f.write(output)
+        if not raw_output:
+            # in most cases user wants on message per line
+            f.write('\n')
+
+    if output_file is None:
+        # user wanted a string; return contents of our temporary file-like obj
+        f.seek(0)
+        return f.read()
+
+
+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
+    with ordinary signature). Yields the results line by line. If CommandError
+    is raised, its message is appended to the results (i.e. yielded by the
+    generator as a string). All other exceptions propagate unless marked as
+    wrappable by :func:`wrap_errors`.
+    """
+    assert hasattr(args, 'function') and hasattr(args.function, '__call__')
+
+    # the function is nested to catch certain exceptions (see below)
+    def _call():
+        # Actually call the function
+        if getattr(args.function, ATTR_NO_NAMESPACE, False):
+            # filter the namespace variables so that only those expected by the
+            # actual function will pass
+            f = args.function
+            if hasattr(f, 'func_code'):
+                # Python 2
+                expected_args = f.func_code.co_varnames[:f.func_code.co_argcount]
+            else:
+                # Python 3
+                expected_args = f.__code__.co_varnames[:f.__code__.co_argcount]
+            ok_args = [x for x in args._get_args() if x in expected_args]
+            ok_kwargs = dict((k,v) for k,v in args._get_kwargs()
+                             if k in expected_args)
+            result = args.function(*ok_args, **ok_kwargs)
+        else:
+            result = args.function(args)
+
+        # Yield the results
+        if isinstance(result, (GeneratorType, list, tuple)):
+            # yield each line ASAP, convert CommandError message to a line
+            for line in result:
+                yield line
+        else:
+            # yield non-empty non-iterable result as a single line
+            if result is not None:
+                yield result
+
+    wrappable_exceptions = [CommandError]
+    wrappable_exceptions += getattr(args.function, ATTR_WRAPPED_EXCEPTIONS, [])
+
+    try:
+        result = _call()
+        for line in result:
+            yield line
+    except tuple(wrappable_exceptions) as e:
+        yield text_type(e)
+
+
+def dispatch_command(function, *args, **kwargs):
+    """ A wrapper for :func:`dispatch` that creates a one-command parser.
+
+    This::
+
+        dispatch_command(foo)
+
+    ...is a shortcut for::
+
+        parser = ArghParser()
+        parser.set_default_command(foo)
+        parser.dispatch()
+
+    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_commands(functions, *args, **kwargs):
+    """ A wrapper for :func:`dispatch` that creates a parser, adds commands to
+    the parser and dispatches them.
+
+    This::
+
+        dispatch_commands([foo, bar])
+
+    ...is a shortcut for::
+
+        parser = ArgumentParser()
+        parser.add_commands(parser, [foo, bar])
+        parser.dispatch(parser)
+
+    """
+    parser = argparse.ArgumentParser()
+    add_commands(parser, functions)
+    dispatch(parser, *args, **kwargs)
 =======
 """
 import argparse
-import locale
-import sys
-from types import GeneratorType
 
-from argh.six import (b, u, string_types, binary_type, text_type,
-                      BytesIO, StringIO, PY3)
-from argh.exceptions import CommandError
-from argh.utils import get_subparsers
 from argh.completion import autocomplete
-from argh.constants import (
-    ATTR_ALIAS, ATTR_ARGS, ATTR_NO_NAMESPACE, ATTR_WRAPPED_EXCEPTIONS
-)
+from argh.assembling import add_commands, set_default_command
+from argh.dispatching import dispatch
 
 
-if PY3:
-    def raw_input(text):
-        return input(text.decode())
-
-
-__all__ = [
-    'ArghParser', 'add_commands', 'autocomplete', 'confirm', 'dispatch',
-    'dispatch_command', 'dispatch_commands', 'set_default_command',
-    'wrap_errors'
-]
-
-
-def set_default_command(parser, function):
-    """ Sets default command (i.e. a function) for given parser.
-
-    If `parser.description` is empty and the function has a docstring, it is
-    used as the description.
-
-    .. 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)
-    if function.__doc__ and not parser.description:
-        parser.description = function.__doc__
-    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.
-
-    :param parser:
-
-        an :class:`argparse.ArgumentParser` instance.
-
-    :param functions:
-
-        a list of functions. A subparser is created for each of them. If the
-        function is decorated with :func:`arg`, the arguments are passed to
-        the :class:`~argparse.ArgumentParser.add_argument` method of the
-        parser. See also :func:`dispatch` for requirements concerning function
-        signatures. The command name is inferred from the function name. Note
-        that the underscores in the name are replaced with hyphens, i.e.
-        function name "foo_bar" becomes command name "foo-bar".
-
-    :param namespace:
-
-        an optional string representing the group of commands. For example, if
-        a command named "hello" is added without the namespace, it will be
-        available as "prog.py hello"; if the namespace if specified as "greet",
-        then the command will be accessible as "prog.py greet hello". The
-        namespace itself is not callable, so "prog.py greet" will fail and only
-        display a help message.
-
-    Help message for a namespace can be also tuned with these params (provided
-    that you specify the `namespace`):
-
-    :param title:
-
-        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as `title`.
-
-    :param description:
-
-        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as
-        `description`.
-
-    :param help:
-
-        passed to :meth:`argsparse.ArgumentParser.add_subparsers` as `help`.
-
-    .. note::
-
-        This function modifies the parser object. Generally side effects are
-        bad practice but we don't seem to have any choice as ArgumentParser is
-        pretty opaque. You may prefer :class:`ArghParser.add_commands` for a
-        bit more predictable API.
-
-    .. admonition:: Design flaw
-
-        This function peeks into the parser object using its internal API.
-        Unfortunately the public API does not allow to *get* the subparsers, it
-        only lets you *add* them, and do that *once*. So you would have to toss
-        the subparsers object around to add something later. That said, I doubt
-        that argparse will change a lot in the future as it's already pretty
-        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:
-        # make a namespace placeholder and register the commands within it
-        assert isinstance(namespace, string_types)
-        subsubparser = subparsers.add_parser(namespace, help=title)
-        subparsers = subsubparser.add_subparsers(title=title,
-                                                 description=description,
-                                                 help=help)
-    else:
-        assert not any([title, description, help]), (
-            'Arguments "title", "description" or "extra_help" only make sense '
-            'if provided along with a namespace.')
-
-    for func in functions:
-        # XXX we could add multiple aliases here but it's a bit of a hack
-        cmd_name = getattr(func, ATTR_ALIAS, func.__name__.replace('_','-'))
-        command_parser = subparsers.add_parser(cmd_name)
-        set_default_command(command_parser, func)
-
-
-def dispatch_command(function, *args, **kwargs):
-    """ A wrapper for :func:`dispatch` that creates a one-command parser.
-
-    This::
-
-        dispatch_command(foo)
-
-    ...is a shortcut for::
-
-        parser = ArghParser()
-        parser.set_default_command(foo)
-        parser.dispatch()
-
-    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 = ArghParser()
-    parser.set_default_command(function)
-    parser.dispatch(*args, **kwargs)
-
-
-def dispatch_commands(functions, *args, **kwargs):
-    """ A wrapper for :func:`dispatch` that creates a parser, adds commands to
-    the parser and dispatches them.
-
-    This::
-
-        dispatch_commands([foo, bar])
-
-    ...is a shortcut for::
-
-        parser = ArgumentParser()
-        parser.add_commands(parser, [foo, bar])
-        parser.dispatch(parser)
-
-    """
-    parser = ArghParser()
-    parser.add_commands(functions)
-    parser.dispatch(*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):
-    """Parses given list of arguments using given parser, calls the relevant
-    function and prints the result.
-
-    The target function should expect one positional argument: the
-    :class:`argparse.Namespace` object. However, if the function is decorated with
-    :func:`plain_signature`, the positional and named arguments from the
-    namespace object are passed to the function instead of the object itself.
-
-    :param parser:
-
-        the ArgumentParser instance.
-
-    :param argv:
-
-        a list of strings representing the arguments. If `None`, ``sys.argv``
-        is used instead. Default is `None`.
-
-    :param add_help_command:
-
-        if `True`, converts first positional argument "help" to a keyword
-        argument so that ``help foo`` becomes ``foo --help`` and displays usage
-        information for "foo". Default is `True`.
-
-    :param encoding:
-
-        Encoding for results. If `None`, it is determined automatically.
-        Default is `None`.
-
-    :param output_file:
-
-        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:
-
-        If `True`, results are written to the output file raw, without adding
-        whitespaces or newlines between yielded strings. Default is `False`.
-
-    :param completion:
-
-        If `True`, shell tab completion is enabled. Default is `True`. (You
-        will also need to install it.)
-
-    By default the exceptions are not wrapped and will propagate. The only
-    exception that is always wrapped is :class:`CommandError` which is
-    interpreted as an expected event so the traceback is hidden. You can also
-    mark arbitrary exceptions as "wrappable" by using the :func:`wrap_errors`
-    decorator.
-    """
-    if completion:
-        autocomplete(parser)
-
-    if argv is None:
-        argv = sys.argv[1:]
-
-    if add_help_command:
-        if argv and argv[0] == 'help':
-            argv.pop(0)
-            argv.append('--help')
-
-    # this will raise SystemExit if parsing fails
-    args = parser.parse_args(argv, namespace=namespace)
-
-    if hasattr(args, 'function'):
-        if pre_call:  # XXX undocumented because I'm unsure if it's OK
-            pre_call(args)
-        lines = _execute_command(args)
-    else:
-        # no commands declared, can't dispatch; display help message
-        lines = [parser.format_usage()]
-
-    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() if PY3 else BytesIO()
-    else:
-        # normally this is stdout; can be any file
-        f = output_file
-
-    for line in lines:
-        # 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) or ''
-        f.write(output)
-        if not raw_output:
-            # in most cases user wants on message per line
-            f.write('\n')
-
-    if output_file is None:
-        # user wanted a string; return contents of our 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.
-
-    .. note:: Compatibility
-
-       :Python 2.x:
-           `sys.stdout` is a file-like object that accepts `str` (bytes)
-           and breaks when `unicode` is passed to `sys.stdout.write()`.
-       :Python 3.x:
-           `sys.stdout` is a `_io.TextIOWrapper` instance that accepts `str`
-           (unicode) and breaks on `bytes`.
-
-       In Python 2.x arbitrary types are coerced to `unicode` and then to `str`.
-
-       In Python 3.x all types are coerced to `str` with the exception
-       for `bytes` which is **not allowed** to avoid confusion.
-
-    """
-    if not isinstance(line, text_type):
-        if PY3 and isinstance(line, binary_type):
-            # in Python 3.x we require Unicode, period.
-            raise TypeError('Binary comand output is not supported '
-                            'in Python 3.x')
-
-        # in Python 2.x we accept bytes and convert them to Unicode.
-        try:
-            line = text_type(line)
-        except UnicodeDecodeError:
-            line = b(line).decode('utf-8')
-
-    if PY3:
-        return line
-
-    # Choose output encoding
-    if not encoding:
-        # choose between terminal's and system's preferred encodings
-        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)
-
-
-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
-    with ordinary signature). Yields the results line by line. If CommandError
-    is raised, its message is appended to the results (i.e. yielded by the
-    generator as a string). All other exceptions propagate unless marked as
-    wrappable by :func:`wrap_errors`.
-    """
-    assert hasattr(args, 'function') and hasattr(args.function, '__call__')
-
-    # the function is nested to catch certain exceptions (see below)
-    def _call():
-        # Actually call the function
-        if getattr(args.function, ATTR_NO_NAMESPACE, False):
-            # filter the namespace variables so that only those expected by the
-            # actual function will pass
-            f = args.function
-            if hasattr(f, 'func_code'):
-                # Python 2
-                expected_args = f.func_code.co_varnames[:f.func_code.co_argcount]
-            else:
-                # Python 3
-                expected_args = f.__code__.co_varnames[:f.__code__.co_argcount]
-            ok_args = [x for x in args._get_args() if x in expected_args]
-            ok_kwargs = dict((k,v) for k,v in args._get_kwargs()
-                             if k in expected_args)
-            result = args.function(*ok_args, **ok_kwargs)
-        else:
-            result = args.function(args)
-
-        # Yield the results
-        if isinstance(result, (GeneratorType, list, tuple)):
-            # yield each line ASAP, convert CommandError message to a line
-            for line in result:
-                yield line
-        else:
-            # yield non-empty non-iterable result as a single line
-            if result is not None:
-                yield result
-
-    wrappable_exceptions = [CommandError]
-    wrappable_exceptions += getattr(args.function, ATTR_WRAPPED_EXCEPTIONS, [])
-
-    try:
-        result = _call()
-        for line in result:
-            yield line
-    except tuple(wrappable_exceptions) as e:
-        yield text_type(e)
+__all__ = ['ArghParser']
 
 
 class ArghParser(argparse.ArgumentParser):
-    """An :class:`ArgumentParser` suclass which adds a couple of convenience
+    """An :class:`ArgumentParser` subclass which adds a couple of convenience
     methods.
 
     There is actually no need to subclass the parser. The methods are but
     def dispatch(self, *args, **kwargs):
         "Wrapper for :func:`dispatch`."
         return dispatch(self, *args, **kwargs)
-
-
-def confirm(action, default=None, skip=False):
-    """A shortcut for typical confirmation prompt.
-
-    :param action:
-
-        a string describing the action, e.g. "Apply changes". A question mark
-        will be appended.
-
-    :param default:
-
-        `bool` or `None`. Determines what happens when user hits :kbd:`Enter`
-        without typing in a choice. If `True`, default choice is "yes". If
-        `False`, it is "no". If `None` the prompt keeps reappearing until user
-        types in a choice (not necessarily acceptable) or until the number of
-        iteration reaches the limit. Default is `None`.
-
-    :param skip:
-
-        `bool`; if `True`, no interactive prompt is used and default choice is
-        returned (useful for batch mode). Default is `False`.
-
-    Usage::
-
-        @arg('key')
-        @arg('--silent', help='do not prompt, always give default answers')
-        def delete(args):
-            item = db.get(Item, args.key)
-            if confirm('Delete '+item.title, default=True, skip=args.silent):
-                item.delete()
-                print('Item deleted.')
-            else:
-                print('Operation cancelled.')
-
-    Returns `None` on `KeyboardInterrupt` event.
-    """
-    MAX_ITERATIONS = 3
-    if skip:
-        return default
-    else:
-        defaults = {
-            None: ('y','n'),
-            True: ('Y','n'),
-            False: ('y','N'),
-        }
-        y, n = defaults[default]
-        prompt = u('{action}? ({y}/{n})').format(**locals())
-        if not PY3:
-            prompt = prompt.encode('utf-8')
-        choice = None
-        try:
-            if default is None:
-                cnt = 1
-                while not choice and cnt < MAX_ITERATIONS:
-                    choice = raw_input(prompt)
-                    cnt += 1
-            else:
-                choice = raw_input(prompt)
-        except KeyboardInterrupt:
-            return None
-    if choice in ('yes', 'y', 'Y'):
-        return True
-    if choice in ('no', 'n', 'N'):
-        return False
-    if default is not None:
-        return default
-    return None
-
-
-def wrap_errors(*exceptions):
-    """Decorator. Wraps given exceptions into :class:`CommandError`. Usage::
-
-        @arg('-x')
-        @arg('-y')
-        @wrap_errors(AssertionError)
-        def foo(args):
-            assert args.x or args.y, 'x or y must be specified'
-
-    If the assertion fails, its message will be correctly printed and the
-    stack hidden. This helps to avoid boilerplate code.
-    """
-    def wrapper(func):
-        setattr(func, ATTR_WRAPPED_EXCEPTIONS, exceptions)
-        return func
-    return wrapper

argh/interaction.py

+# -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2010—2012 Andrey Mikhailenko and contributors
+#
+#  This file is part of Argh.
+#
+#  Argh is free software under terms of the GNU Lesser
+#  General Public License version 3 (LGPLv3) as published by the Free
+#  Software Foundation. See the file README for copying conditions.
+#
+"""
+Interaction
+===========
+"""
+from argh.six import text_type, PY3
+
+
+__all__ = ['confirm']
+
+
+def _input(prompt):
+    # this function can be mocked up in tests
+    if PY3:
+        return input(prompt)
+    else:
+        return raw_input(prompt)
+
+
+def safe_input(prompt):
+    "Prompts user for input. Correctly handles prompt message encoding."
+
+    if PY3:
+        if not isinstance(prompt, text_type):
+            # Python 3.x: bytes →  unicode
+            prompt = prompt.decode()
+    else:
+        if isinstance(prompt, text_type):
+            # Python 2.x: unicode →  bytes
+            prompt = prompt.encode('utf-8')
+
+    return _input(prompt)
+
+
+def confirm(action, default=None, skip=False):
+    """A shortcut for typical confirmation prompt.
+
+    :param action:
+
+        a string describing the action, e.g. "Apply changes". A question mark
+        will be appended.
+
+    :param default:
+
+        `bool` or `None`. Determines what happens when user hits :kbd:`Enter`
+        without typing in a choice. If `True`, default choice is "yes". If
+        `False`, it is "no". If `None` the prompt keeps reappearing until user
+        types in a choice (not necessarily acceptable) or until the number of
+        iteration reaches the limit. Default is `None`.
+
+    :param skip:
+
+        `bool`; if `True`, no interactive prompt is used and default choice is
+        returned (useful for batch mode). Default is `False`.
+
+    Usage::
+
+        @arg('key')
+        @arg('--silent', help='do not prompt, always give default answers')
+        def delete(args):
+            item = db.get(Item, args.key)
+            if confirm('Delete '+item.title, default=True, skip=args.silent):
+                item.delete()
+                print('Item deleted.')
+            else:
+                print('Operation cancelled.')
+
+    Returns `None` on `KeyboardInterrupt` event.
+    """
+    MAX_ITERATIONS = 3
+    if skip:
+        return default
+    else:
+        defaults = {
+            None: ('y','n'),
+            True: ('Y','n'),
+            False: ('y','N'),
+        }
+        y, n = defaults[default]
+        prompt = text_type('{action}? ({y}/{n})').format(**locals())
+        choice = None
+        try:
+            if default is None:
+                cnt = 1
+                while not choice and cnt < MAX_ITERATIONS:
+                    choice = safe_input(prompt)
+                    cnt += 1
+            else:
+                choice = safe_input(prompt)
+        except KeyboardInterrupt:
+            return None
+    if choice in ('yes', 'y', 'Y'):
+        return True
+    if choice in ('no', 'n', 'N'):
+        return False
+    if default is not None:
+        return default
+    return None
+# -*- coding: utf-8 -*-
+#
+#  Copyright (c) 2010—2012 Andrey Mikhailenko and contributors
+#
+#  This file is part of Argh.
+#
+#  Argh is free software under terms of the GNU Lesser
+#  General Public License version 3 (LGPLv3) as published by the Free
+#  Software Foundation. See the file README for copying conditions.
+#
+"""
+Output Processing
+=================
+"""
+import locale
+from argh.six import binary_type, text_type, PY3
+
+
+__all__ = ['encode_output']
+
+
+def encode_output(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.
+
+    .. note:: Compatibility
+
+       :Python 2.x:
+           `sys.stdout` is a file-like object that accepts `str` (bytes)
+           and breaks when `unicode` is passed to `sys.stdout.write()`.
+       :Python 3.x:
+           `sys.stdout` is a `_io.TextIOWrapper` instance that accepts `str`
+           (unicode) and breaks on `bytes`.
+
+       In Python 2.x arbitrary types are coerced to `unicode` and then to `str`.
+
+       In Python 3.x all types are coerced to `str` with the exception
+       for `bytes` which is **not allowed** to avoid confusion.
+
+    """
+    if not isinstance(line, text_type):
+        if PY3 and isinstance(line, binary_type):
+            # in Python 3.x we require Unicode, period.
+            raise TypeError('Binary comand output is not supported '
+                            'in Python 3.x')
+
+        # in Python 2.x we accept bytes and convert them to Unicode.
+        try:
+            line = text_type(line)
+        except UnicodeDecodeError:
+            line = binary_type(line).decode('utf-8')
+
+    if PY3:
+        return line
+
+    # Choose output encoding
+    if not encoding:
+        # choose between terminal's and system's preferred encodings
+        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)
 * 3.2
 * 3.3
 
+.. versionchanged:: 0.15
+   Added support for Python 3.x, dropped support for Python ≤ 2.5.
+
 .. versionchanged:: 0.18
    Improved support for Python 3.2, added support for Python 3.3.
 
-.. versionchanged:: 0.15
-   Added support for Python 3.x, dropped support for Python ≤ 2.5.
-
 Details
 -------
 
 
 class ConfirmTestCase(unittest.TestCase):
     def assert_choice(self, choice, expected, **kwargs):
-        argh.helpers.raw_input = lambda prompt: choice
+        argh.interaction._input = lambda prompt: choice
         self.assertEqual(argh.confirm('test', **kwargs), expected)
 
     def test_simple(self):
 
         def raw_input_mock(prompt):
             prompts.append(prompt)
-        argh.helpers.raw_input = raw_input_mock
+        argh.interaction._input = raw_input_mock
 
         argh.confirm('do smth')
         self.assertEqual(prompts[-1], 'do smth? (y/n)')
         def raw_input_mock(prompt):
             if not PY3:
                 assert isinstance(prompt, binary_type)
-        argh.helpers.raw_input = raw_input_mock
+        argh.interaction._input = raw_input_mock
         argh.confirm(u('привет'))