argh-0.24.1/0000755000175000001440000000000012262445620012634 5ustar andyusers00000000000000argh-0.24.1/MANIFEST.in0000644000175000001440000000012212204176401014357 0ustar andyusers00000000000000# needed for `python setup.py test` include test/__init__.py include test/base.py argh-0.24.1/argh.egg-info/0000755000175000001440000000000012262445620015247 5ustar andyusers00000000000000argh-0.24.1/argh.egg-info/PKG-INFO0000644000175000001440000001665412262445620016360 0ustar andyusers00000000000000Metadata-Version: 1.1 Name: argh Version: 0.24.1 Summary: An unobtrusive argparse wrapper with natural syntax Home-page: http://github.com/neithere/argh/ Author: Andrey Mikhaylenko Author-email: neithere@gmail.com License: GNU Lesser General Public License (LGPL), Version 3 Description: Argh: The Natural CLI ===================== .. image:: https://travis-ci.org/neithere/argh.png?branch=master :target: https://travis-ci.org/neithere/argh Building a command-line interface? Found yourself uttering "argh!" while struggling with the API of `argparse`? Don't like the complexity but need the power? .. epigraph:: Everything should be made as simple as possible, but no simpler. -- Albert Einstein (probably) `Argh` is a smart wrapper for `argparse`. `Argparse` is a very powerful tool; `Argh` just makes it easy to use. In a nutshell ------------- `Argh`-powered applications are *simple* but *flexible*: :Modular: Declaration of commands can be decoupled from assembling and dispatching; :Pythonic: Commands are declared naturally, no complex API calls in most cases; :Reusable: Commands are plain functions, can be used directly outside of CLI context; :Layered: The complexity of code raises with requirements; :Transparent: The full power of argparse is available whenever needed; :Namespaced: Nested commands are a piece of cake, no messing with subparsers (though they are of course used under the hood); :Term-Friendly: Command output is processed with respect to stream encoding; :Unobtrusive: `Argh` can dispatch a subset of pure-`argparse` code, and pure-`argparse` code can update and dispatch a parser assembled with `Argh`; :DRY: The amount of boilerplate code is minimal; among other things, `Argh` will: * infer command name from function name; * infer arguments from function signature; * infer argument type from the default value; * infer argument action from the default value (for booleans); * add an alias root command ``help`` for the ``--help`` argument. Sounds good? Check the tutorial! Relation to argparse -------------------- `Argh` is fully compatible with `argparse`. You can mix `Argh`-agnostic and `Argh`-aware code. Just keep in mind that the dispatcher does some extra work that a custom dispatcher may not do. Installation ------------ Using pip:: $ pip install argh Arch Linux (AUR):: $ yaourt python-argh Examples -------- A very simple application with one command: .. code-block:: python import argh def main(): return 'Hello world' argh.dispatch_command(main) A potentially modular application with multiple commands: .. code-block:: python import argh # declaring: def echo(text): return text def greeter(name, greeting='hello'): return greeting + ', ' + name # assembling: parser = argh.ArghParser() parser.add_commands([echo, greeter]) # dispatching: if __name__ == '__main__': parser.dispatch() The powerful API of `argparse` is also available: .. code-block:: python @arg('text', default='hello world', nargs='+', help='The message') def echo(text): print text The approaches can be safely combined even up to this level: .. code-block:: python # adding help to `foo` which is in the function signature: @arg('foo', help='blah') # these are not in the signature so they go to **kwargs: @arg('baz') @arg('-q', '--quux') # the function itself: def cmd(foo, bar=1, *args, **kwargs): yield foo yield bar yield ', '.join(args) yield kwargs['baz'] yield kwargs['quux'] Links ----- * `Project home page`_ (GitHub) * `Documentation`_ (Read the Docs) * `Package distribution`_ (PyPI) * Questions, requests, bug reports, etc.: * `Issue tracker`_ (GitHub) * `Mailing list`_ (subscribe to get important announcements) * Direct e-mail (neithere at gmail com) * Twitter_ (to get notified of commits; mostly for lulz) .. _project home page: http://github.com/neithere/argh/ .. _documentation: http://argh.readthedocs.org .. _package distribution: http://pypi.python.org/pypi/argh .. _issue tracker: http://github.com/neithere/argh/issues/ .. _mailing list: http://groups.google.com/group/argh-users .. _twitter: http://twitter.com/python_argh Author ------ Developed by Andrey Mikhaylenko since 2010. See file `AUTHORS` for a complete list of contributors to this library. Licensing --------- Argh is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Argh is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Argh. If not, see . Keywords: cli command line argparse optparse argument option Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides: argh argh-0.24.1/argh.egg-info/SOURCES.txt0000644000175000001440000000104412262445620017132 0ustar andyusers00000000000000MANIFEST.in README.rst setup.py argh/__init__.py argh/assembling.py argh/compat.py argh/completion.py argh/constants.py argh/decorators.py argh/dispatching.py argh/exceptions.py argh/helpers.py argh/interaction.py argh/io.py argh/utils.py argh.egg-info/PKG-INFO argh.egg-info/SOURCES.txt argh.egg-info/dependency_links.txt argh.egg-info/top_level.txt test/__init__.py test/base.py test/test_assembling.py test/test_compat.py test/test_decorators.py test/test_dispatching.py test/test_integration.py test/test_interaction.py test/test_regressions.pyargh-0.24.1/argh.egg-info/top_level.txt0000644000175000001440000000000512262445620017774 0ustar andyusers00000000000000argh argh-0.24.1/argh.egg-info/dependency_links.txt0000644000175000001440000000000112262445620021315 0ustar andyusers00000000000000 argh-0.24.1/PKG-INFO0000644000175000001440000001665412262445620013745 0ustar andyusers00000000000000Metadata-Version: 1.1 Name: argh Version: 0.24.1 Summary: An unobtrusive argparse wrapper with natural syntax Home-page: http://github.com/neithere/argh/ Author: Andrey Mikhaylenko Author-email: neithere@gmail.com License: GNU Lesser General Public License (LGPL), Version 3 Description: Argh: The Natural CLI ===================== .. image:: https://travis-ci.org/neithere/argh.png?branch=master :target: https://travis-ci.org/neithere/argh Building a command-line interface? Found yourself uttering "argh!" while struggling with the API of `argparse`? Don't like the complexity but need the power? .. epigraph:: Everything should be made as simple as possible, but no simpler. -- Albert Einstein (probably) `Argh` is a smart wrapper for `argparse`. `Argparse` is a very powerful tool; `Argh` just makes it easy to use. In a nutshell ------------- `Argh`-powered applications are *simple* but *flexible*: :Modular: Declaration of commands can be decoupled from assembling and dispatching; :Pythonic: Commands are declared naturally, no complex API calls in most cases; :Reusable: Commands are plain functions, can be used directly outside of CLI context; :Layered: The complexity of code raises with requirements; :Transparent: The full power of argparse is available whenever needed; :Namespaced: Nested commands are a piece of cake, no messing with subparsers (though they are of course used under the hood); :Term-Friendly: Command output is processed with respect to stream encoding; :Unobtrusive: `Argh` can dispatch a subset of pure-`argparse` code, and pure-`argparse` code can update and dispatch a parser assembled with `Argh`; :DRY: The amount of boilerplate code is minimal; among other things, `Argh` will: * infer command name from function name; * infer arguments from function signature; * infer argument type from the default value; * infer argument action from the default value (for booleans); * add an alias root command ``help`` for the ``--help`` argument. Sounds good? Check the tutorial! Relation to argparse -------------------- `Argh` is fully compatible with `argparse`. You can mix `Argh`-agnostic and `Argh`-aware code. Just keep in mind that the dispatcher does some extra work that a custom dispatcher may not do. Installation ------------ Using pip:: $ pip install argh Arch Linux (AUR):: $ yaourt python-argh Examples -------- A very simple application with one command: .. code-block:: python import argh def main(): return 'Hello world' argh.dispatch_command(main) A potentially modular application with multiple commands: .. code-block:: python import argh # declaring: def echo(text): return text def greeter(name, greeting='hello'): return greeting + ', ' + name # assembling: parser = argh.ArghParser() parser.add_commands([echo, greeter]) # dispatching: if __name__ == '__main__': parser.dispatch() The powerful API of `argparse` is also available: .. code-block:: python @arg('text', default='hello world', nargs='+', help='The message') def echo(text): print text The approaches can be safely combined even up to this level: .. code-block:: python # adding help to `foo` which is in the function signature: @arg('foo', help='blah') # these are not in the signature so they go to **kwargs: @arg('baz') @arg('-q', '--quux') # the function itself: def cmd(foo, bar=1, *args, **kwargs): yield foo yield bar yield ', '.join(args) yield kwargs['baz'] yield kwargs['quux'] Links ----- * `Project home page`_ (GitHub) * `Documentation`_ (Read the Docs) * `Package distribution`_ (PyPI) * Questions, requests, bug reports, etc.: * `Issue tracker`_ (GitHub) * `Mailing list`_ (subscribe to get important announcements) * Direct e-mail (neithere at gmail com) * Twitter_ (to get notified of commits; mostly for lulz) .. _project home page: http://github.com/neithere/argh/ .. _documentation: http://argh.readthedocs.org .. _package distribution: http://pypi.python.org/pypi/argh .. _issue tracker: http://github.com/neithere/argh/issues/ .. _mailing list: http://groups.google.com/group/argh-users .. _twitter: http://twitter.com/python_argh Author ------ Developed by Andrey Mikhaylenko since 2010. See file `AUTHORS` for a complete list of contributors to this library. Licensing --------- Argh is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Argh is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Argh. If not, see . Keywords: cli command line argparse optparse argument option Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: User Interfaces Classifier: Topic :: Software Development :: Libraries :: Python Modules Provides: argh argh-0.24.1/argh/0000755000175000001440000000000012262445620013555 5ustar andyusers00000000000000argh-0.24.1/argh/completion.py0000644000175000001440000000602712262445426016311 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Shell completion ~~~~~~~~~~~~~~~~ Command and argument completion is a great way to reduce the number of keystrokes and improve user experience. To display suggestions when you press :kbd:`tab`, a shell must obtain choices from your program. It calls the program in a specific environment and expects it to return a list of relevant choices. `Argparse` does not support completion out of the box. However, there are 3rd-party apps that do the job, such as argcomplete_ and python-selfcompletion_. `Argh` supports only argcomplete_ which doesn't require subclassing the parser and monkey-patches it instead. Combining `Argh` with python-selfcompletion_ isn't much harder though: simply use `SelfCompletingArgumentParser` instead of vanilla `ArgumentParser`. See installation details and gotchas in the documentation of the 3rd-party app you've chosen for the completion backend. `Argh` automatically enables completion if argcomplete_ is available (see :attr:`COMPLETION_ENABLED`). If completion is undesirable in given app by design, it can be turned off by setting ``completion=False`` in :func:`argh.dispatching.dispatch`. Note that you don't *have* to add completion via `Argh`; it doesn't matter whether you let it do it for you or use the underlying API. .. _argcomplete: https://github.com/kislyuk/argcomplete .. _python-selfcompletion: https://github.com/dbarnett/python-selfcompletion Argument-level completion ------------------------- Argcomplete_ supports custom "completers". The documentation suggests adding the completer as an attribute of the argument parser action:: parser.add_argument("--env-var1").completer = EnvironCompleter However, this doesn't fit the normal `Argh`-assisted workflow. It is recommended to use the :func:`~argh.decorators.arg` decorator:: @arg('--env-var1', completer=EnvironCompleter) def func(...): ... """ import os COMPLETION_ENABLED = False "Dynamically set to `True` on load if argcomplete_ was successfully imported." try: import argcomplete except ImportError: pass else: COMPLETION_ENABLED = True __all__ = ['autocomplete', 'COMPLETION_ENABLED'] def autocomplete(parser, allow_warnings=True): """ Adds support for shell completion via argcomplete_ by patching given `argparse.ArgumentParser` (sub)class. If completion is not enabled, issues a warning. The warning is suppressed either if the shell is not `bash` or if `allow_warnings` is `False`. The latter can be due to the output stream not being a TTY. """ if COMPLETION_ENABLED: argcomplete.autocomplete(parser) elif allow_warnings and 'bash' in os.getenv('SHELL', ''): import warnings warnings.warn('Bash completion not available. Install argcomplete.') argh-0.24.1/argh/exceptions.py0000644000175000001440000000252012262445426016313 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Exceptions ~~~~~~~~~~ """ class AssemblingError(Exception): """ Raised if the parser could not be configured due to malformed or conflicting command declarations. """ class CommandError(Exception): """ Intended to be raised from within a command. The dispatcher wraps this exception by default and prints its message without traceback. Useful for print-and-exit tasks when you expect a failure and don't want to startle the ordinary user by the cryptic output. Consider the following example:: def foo(args): try: ... except KeyError as e: print(u'Could not fetch item: {0}'.format(e)) return It is exactly the same as:: def bar(args): try: ... except KeyError as e: raise CommandError(u'Could not fetch item: {0}'.format(e)) This exception can be safely used in both print-style and yield-style commands (see :doc:`tutorial`). """ argh-0.24.1/argh/decorators.py0000644000175000001440000001763612262445426016315 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Command decorators ~~~~~~~~~~~~~~~~~~ """ from argh.assembling import _fix_compat_issue29 from argh.constants import (ATTR_ALIASES, ATTR_ARGS, ATTR_NAME, ATTR_WRAPPED_EXCEPTIONS, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR, ATTR_INFER_ARGS_FROM_SIGNATURE, ATTR_EXPECTS_NAMESPACE_OBJECT) __all__ = ['alias', 'aliases', 'named', 'arg', 'plain_signature', 'command', 'wrap_errors', 'expects_obj'] def named(new_name): """ Sets given string as command name instead of the function name. The string is used verbatim without further processing. Usage:: @named('load') def do_load_some_stuff_and_keep_the_original_function_name(args): ... The resulting command will be available only as ``load``. To add aliases without renaming the command, check :func:`aliases`. .. versionadded:: 0.19 """ def wrapper(func): setattr(func, ATTR_NAME, new_name) return func return wrapper def alias(new_name): # pragma: nocover """ .. deprecated:: 0.19 Use :func:`named` or :func:`aliases` instead. """ import warnings warnings.warn('Decorator @alias() is deprecated. ' 'Use @aliases() or @named() instead.', DeprecationWarning) def wrapper(func): setattr(func, ATTR_NAME, new_name) _fix_compat_issue29(func) return func return wrapper def aliases(*names): """ Defines alternative command name(s) for given function (along with its original name). Usage:: @aliases('co', 'check') def checkout(args): ... The resulting command will be available as ``checkout``, ``check`` and ``co``. .. note:: This decorator only works with a recent version of argparse (see `Python issue 9324`_ and `Python rev 4c0426`_). Such version ships with **Python 3.2+** and may be available in other environments as a separate package. Argh does not issue warnings and simply ignores aliases if they are not supported. See :attr:`~argh.assembling.SUPPORTS_ALIASES`. .. _Python issue 9324: http://bugs.python.org/issue9324 .. _Python rev 4c0426: http://hg.python.org/cpython/rev/4c0426261148/ .. versionadded:: 0.19 """ def wrapper(func): setattr(func, ATTR_ALIASES, names) return func return wrapper def plain_signature(func): # pragma: nocover """ .. deprecated:: 0.20 Function signature is now introspected by default. Use :func:`expects_obj` for inverted behaviour. """ import warnings warnings.warn('Decorator @plain_signature is deprecated. ' 'Function signature is now introspected by default.', DeprecationWarning) 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 exactly the same as that of :meth:`argparse.ArgumentParser.add_argument`, only some keywords are not required if they can be easily guessed. Usage:: @arg('path') @arg('--format', choices=['yaml','json'], default='json') @arg('--dry-run', default=False) @arg('-v', '--verbosity', choices=range(0,3), default=1) def load(args): loaders = {'json': json.load, 'yaml': yaml.load} loader = loaders[args.format] data = loader(args.path) if not args.dry_run: if 1 < verbosity: print('saving to the database') put_to_database(data) Note that: * you didn't have to specify ``action="store_true"`` for ``--dry-run``; * you didn't have to specify ``type=int`` for ``--verbosity``. """ def wrapper(func): declared_args = getattr(func, ATTR_ARGS, []) # The innermost decorator is called first but appears last in the code. # We need to preserve the expected order of positional arguments, so # the outermost decorator inserts its value before the innermost's: declared_args.insert(0, dict(option_strings=args, **kwargs)) setattr(func, ATTR_ARGS, declared_args) _fix_compat_issue29(func) return func return wrapper def command(func): """ .. deprecated:: 0.21 Function signature is now introspected by default. Use :func:`expects_obj` for inverted behaviour. """ import warnings warnings.warn('Decorator @command is deprecated. ' 'Function signature is now introspected by default.', DeprecationWarning) setattr(func, ATTR_INFER_ARGS_FROM_SIGNATURE, True) return func def _fix_compat_issue36(func, errors, processor, args): # # TODO: remove before 1.0 release (will break backwards compatibility) # if errors and not hasattr(errors, '__iter__'): # what was expected to be a list is actually its first item errors = [errors] # what was expected to be a function is actually the second item if processor: errors.append(processor) processor = None # *args, if any, are the remaining items if args: errors.extend(args) import warnings warnings.warn('{func.__name__}: wrappable exceptions must be declared ' 'as list, i.e. @wrap_errors([{errors}]) instead of ' '@wrap_errors({errors})'.format( func=func, errors=', '.join(x.__name__ for x in errors)), DeprecationWarning) return errors, processor def wrap_errors(errors=None, processor=None, *args): """ Decorator. Wraps given exceptions into :class:`~argh.exceptions.CommandError`. Usage:: @wrap_errors([AssertionError]) def foo(x=None, y=None): assert x or 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. :param errors: A list of exception classes to catch. :param processor: A callable that expects the exception object and returns a string. For example, this renders all wrapped errors in red colour:: from termcolor import colored def failure(err): return colored(str(err), 'red') @wrap_errors(processor=failure) def my_command(...): ... .. warning:: The `exceptions` argument **must** be a list. For backward compatibility reasons the old way is still allowed:: @wrap_errors(KeyError, ValueError) However, the hack that allows that will be **removed** in Argh 1.0. Please make sure to update your code. """ def wrapper(func): errors_, processor_ = _fix_compat_issue36(func, errors, processor, args) if errors_: setattr(func, ATTR_WRAPPED_EXCEPTIONS, errors_) if processor_: setattr(func, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR, processor_) return func return wrapper def expects_obj(func): """ Marks given function as expecting a namespace object. Usage:: @arg('bar') @arg('--quux', default=123) @expects_obj def foo(args): yield args.bar, args.quux This is equivalent to:: def foo(bar, quux=123): yield bar, quux In most cases you don't need this decorator. """ setattr(func, ATTR_EXPECTS_NAMESPACE_OBJECT, True) return func argh-0.24.1/argh/__init__.py0000644000175000001440000000075712262445536015705 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # from .assembling import * from .decorators import * from .dispatching import * from .exceptions import * from .interaction import * from .helpers import * __version__ = '0.24.1' argh-0.24.1/argh/constants.py0000644000175000001440000000273112262445426016152 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # import argparse __all__ = ( 'ATTR_NAME', 'ATTR_ALIASES', 'ATTR_ARGS', 'ATTR_WRAPPED_EXCEPTIONS', 'ATTR_WRAPPED_EXCEPTIONS_PROCESSOR', 'ATTR_EXPECTS_NAMESPACE_OBJECT', 'PARSER_FORMATTER' ) # # Names of function attributes where Argh stores command behaviour # # explicit command name (differing from function name) ATTR_NAME = 'argh_name' # alternative command names ATTR_ALIASES = 'argh_aliases' # declared arguments ATTR_ARGS = 'argh_args' # list of exception classes that should be wrapped and printed as results ATTR_WRAPPED_EXCEPTIONS = 'argh_wrap_errors' # a function to preprocess the exception object when it is wrapped ATTR_WRAPPED_EXCEPTIONS_PROCESSOR = 'argh_wrap_errors_processor' # forcing argparse.Namespace object instead of signature introspection ATTR_EXPECTS_NAMESPACE_OBJECT = 'argh_expects_namespace_object' # # Other library-wide stuff # PARSER_FORMATTER = argparse.ArgumentDefaultsHelpFormatter """ Default formatter to be used in implicitly instantiated ArgumentParser. """ #----------------------------------------------------------------------------- # # deprecated # ATTR_INFER_ARGS_FROM_SIGNATURE = 'argh_infer_args_from_signature' argh-0.24.1/argh/compat.py0000644000175000001440000000302412262445426015415 0ustar andyusers00000000000000# originally inspired by "six" by Benjamin Peterson import inspect import sys if sys.version_info < (3,0): text_type = unicode binary_type = str import StringIO StringIO = BytesIO = StringIO.StringIO else: text_type = str binary_type = bytes import io StringIO = io.StringIO BytesIO = io.BytesIO if sys.version_info < (3,0): getargspec = inspect.getargspec else: # in Python 3 the basic getargspec doesn't support keyword-only arguments # and annotations and raises ValueError if they are discovered getargspec = inspect.getfullargspec class _PrimitiveOrderedDict(dict): """ A poor man's OrderedDict replacement for compatibility with Python 2.6. Implements only the basic features. May easily break if non-overloaded methods are used. """ def __init__(self, *args, **kwargs): super(_PrimitiveOrderedDict, self).__init__(*args, **kwargs) self._seq = [] def __setitem__(self, key, value): super(_PrimitiveOrderedDict, self).__setitem__(key, value) if key not in self._seq: self._seq.append(key) def __delitem__(self, key): super(_PrimitiveOrderedDict, self).__delitem__(key) idx = self._seq.index(key) del self._seq[idx] def __iter__(self): return iter(self._seq) def keys(self): return list(self) def values(self): return [self[k] for k in self] try: from collections import OrderedDict except ImportError: OrderedDict = _PrimitiveOrderedDict argh-0.24.1/argh/dispatching.py0000644000175000001440000002060212262445426016430 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Dispatching ~~~~~~~~~~~ """ import argparse import sys from types import GeneratorType from argh import compat, io from argh.constants import (ATTR_WRAPPED_EXCEPTIONS, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR, ATTR_EXPECTS_NAMESPACE_OBJECT, PARSER_FORMATTER) from argh.completion import autocomplete from argh.assembling import add_commands, set_default_command from argh.exceptions import CommandError from argh.utils import get_arg_spec __all__ = ['dispatch', 'dispatch_command', 'dispatch_commands', 'PARSER_FORMATTER'] def dispatch(parser, argv=None, add_help_command=True, completion=True, pre_call=None, output_file=sys.stdout, errors_file=sys.stderr, 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:`~argh.decorators.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 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 errors_file: Same as `output_file` but for ``sys.stderr``. :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.) See :mod:`argh.completion`. By default the exceptions are not wrapped and will propagate. The only exception that is always wrapped is :class:`~argh.exceptions.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:`~argh.decorators.wrap_errors` decorator. """ if completion: isatty = hasattr(output_file, 'isatty') and output_file.isatty() autocomplete(parser, allow_warnings=isatty) 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, errors_file) 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 if sys.version_info < (3,0): f = compat.BytesIO() else: f = compat.StringIO() 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 io.dump(line, f) if not raw_output: # in most cases user wants on message per line io.dump('\n', f) 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, errors_file): """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 :class:`~argh.exceptions.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_EXPECTS_NAMESPACE_OBJECT, False): result = args.function(args) else: # namespace -> dictionary _flat_key = lambda key: key.replace('-', '_') all_input = dict((_flat_key(k), v) for k,v in vars(args).items()) # filter the namespace variables so that only those expected by the # actual function will pass spec = get_arg_spec(args.function) positional = [all_input[k] for k in spec.args] kwonly = getattr(spec, 'kwonlyargs', []) keywords = dict((k, all_input[k]) for k in kwonly) # *args if spec.varargs: positional += getattr(args, spec.varargs) # **kwargs varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', [])) if varkw: not_kwargs = ['function'] + spec.args + [spec.varargs] + kwonly extra = [k for k in vars(args) if k not in not_kwargs] for k in extra: keywords[k] = getattr(args, k) result = args.function(*positional, **keywords) # 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: processor = getattr(args.function, ATTR_WRAPPED_EXCEPTIONS_PROCESSOR, lambda e: '{0.__class__.__name__}: {0}'.format(e)) errors_file.write(compat.text_type(processor(e))) errors_file.write('\n') def dispatch_command(function, *args, **kwargs): """ A wrapper for :func:`dispatch` that creates a one-command parser. Uses :attr:`PARSER_FORMATTER`. This:: dispatch_command(foo) ...is a shortcut for:: parser = ArgumentParser() set_default_command(parser, foo) dispatch(parser) This function can be also used as a decorator. """ parser = argparse.ArgumentParser(formatter_class=PARSER_FORMATTER) 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. Uses :attr:`PARSER_FORMATTER`. This:: dispatch_commands([foo, bar]) ...is a shortcut for:: parser = ArgumentParser() add_commands(parser, [foo, bar]) dispatch(parser) """ parser = argparse.ArgumentParser(formatter_class=PARSER_FORMATTER) add_commands(parser, functions) dispatch(parser, *args, **kwargs) argh-0.24.1/argh/helpers.py0000644000175000001440000000314112262445426015574 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Helpers ~~~~~~~ """ import argparse from argh.completion import autocomplete from argh.assembling import add_commands, set_default_command from argh.dispatching import PARSER_FORMATTER, dispatch __all__ = ['ArghParser'] class ArghParser(argparse.ArgumentParser): """ A subclass of :class:`ArgumentParser` with a couple of convenience methods. There is actually no need to subclass the parser. The methods are but wrappers for stand-alone functions :func:`~argh.assembling.add_commands`, :func:`~argh.completion.autocomplete` and :func:`~argh.dispatching.dispatch`. Uses :attr:`~argh.dispatching.PARSER_FORMATTER`. """ def __init__(self, *args, **kwargs): kwargs.setdefault('formatter_class', PARSER_FORMATTER) super(ArghParser, self).__init__(*args, **kwargs) 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) def autocomplete(self): return autocomplete(self) def dispatch(self, *args, **kwargs): "Wrapper for :func:`dispatch`." return dispatch(self, *args, **kwargs) argh-0.24.1/argh/io.py0000644000175000001440000000562412262445426014551 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Output Processing ~~~~~~~~~~~~~~~~~ """ import locale import sys from argh import compat __all__ = ['dump', 'encode_output', 'safe_input'] def _input(prompt): # this function can be mocked up in tests if sys.version_info < (3,0): return raw_input(prompt) else: return input(prompt) def safe_input(prompt): "Prompts user for input. Correctly handles prompt message encoding." if sys.version_info < (3,0): if isinstance(prompt, compat.text_type): # Python 2.x: unicode → bytes encoding = locale.getpreferredencoding() or 'utf-8' prompt = prompt.encode(encoding) else: if not isinstance(prompt, compat.text_type): # Python 3.x: bytes → unicode prompt = prompt.decode() return _input(prompt) def encode_output(value, output_file): """ Encodes given value so it can be written to given file object. Value may be Unicode, binary string or any other data type. The exact behaviour depends on the Python version: Python 3.x `sys.stdout` is a `_io.TextIOWrapper` instance that accepts `str` (unicode) and breaks on `bytes`. It is OK to simply assume that everything is Unicode unless special handling is introduced in the client code. Thus, no additional processing is performed. Python 2.x `sys.stdout` is a file-like object that accepts `str` (bytes) and breaks when `unicode` is passed to `sys.stdout.write()`. We can expect both Unicode and bytes. They need to be encoded so as to match the file object encoding. The output is binary if the object doesn't explicitly require Unicode. """ if sys.version_info > (3,0): # Python 3: whatever → unicode return compat.text_type(value) else: # Python 2: handle special cases stream_encoding = getattr(output_file, 'encoding', None) if stream_encoding: if stream_encoding.upper() == 'UTF-8': return compat.text_type(value) else: return value.encode(stream_encoding, 'ignore') else: # no explicit encoding requirements; force binary if isinstance(value, compat.text_type): # unicode → binary return value.encode('utf-8') else: return str(value) def dump(raw_data, output_file): """ Writes given line to given output file. See :func:`encode_output` for details. """ data = encode_output(raw_data, output_file) output_file.write(data) argh-0.24.1/argh/assembling.py0000644000175000001440000003776512262445426016301 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Assembling ~~~~~~~~~~ Functions and classes to properly assemble your commands in a parser. """ import argparse import sys from argh.completion import COMPLETION_ENABLED from argh.compat import OrderedDict from argh.constants import (ATTR_ALIASES, ATTR_ARGS, ATTR_NAME, ATTR_INFER_ARGS_FROM_SIGNATURE, ATTR_EXPECTS_NAMESPACE_OBJECT, PARSER_FORMATTER) from argh.utils import get_subparsers, get_arg_spec from argh.exceptions import AssemblingError __all__ = ['SUPPORTS_ALIASES', 'set_default_command', 'add_commands'] def _check_support_aliases(): p = argparse.ArgumentParser() s = p.add_subparsers() try: s.add_parser('x', aliases=[]) except TypeError: return False else: return True SUPPORTS_ALIASES = _check_support_aliases() """ Calculated on load. If `True`, current version of argparse supports alternative command names (can be set via :func:`~argh.decorators.aliases`). """ def _get_args_from_signature(function): if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False): return spec = get_arg_spec(function) defaults = dict(zip(*[reversed(x) for x in (spec.args, spec.defaults or [])])) defaults.update(getattr(spec, 'kwonlydefaults', None) or {}) kwonly = getattr(spec, 'kwonlyargs', []) if sys.version_info < (3,0): annotations = {} else: annotations = dict((k,v) for k,v in function.__annotations__.items() if isinstance(v, str)) # define the list of conflicting option strings # (short forms, i.e. single-character ones) chars = [a[0] for a in spec.args + kwonly] char_counts = dict((char, chars.count(char)) for char in set(chars)) conflicting_opts = tuple(char for char in char_counts if 1 < char_counts[char]) for name in spec.args + kwonly: flags = [] # name_or_flags akwargs = {} # keyword arguments for add_argument() if name in annotations: # help message: func(a : "b") -> add_argument("a", help="b") akwargs.update(help=annotations.get(name)) if name in defaults or name in kwonly: if name in defaults: akwargs.update(default=defaults.get(name)) else: akwargs.update(required=True) flags = ('-{0}'.format(name[0]), '--{0}'.format(name)) if name.startswith(conflicting_opts): # remove short name flags = flags[1:] else: # positional argument flags = (name,) # cmd(foo_bar) -> add_argument('foo-bar') flags = tuple(x.replace('_', '-') for x in flags) yield dict(option_strings=flags, **akwargs) if spec.varargs: # *args yield dict(option_strings=[spec.varargs], nargs='*') def _guess(kwargs): """ Adds types, actions, etc. to given argument specification. For example, ``default=3`` implies ``type=int``. :param arg: a :class:`argh.utils.Arg` instance """ guessed = {} TYPE_AWARE_ACTIONS = 'store', 'append' "Parser actions that accept argument 'type'." # guess type/action from default value value = kwargs.get('default') if value is not None: if isinstance(value, bool): if kwargs.get('action') is None: # infer action from default value guessed['action'] = 'store_false' if value else 'store_true' elif kwargs.get('type') is None: # infer type from default value # (make sure that action handler supports this keyword) if kwargs.get('action', 'store') in TYPE_AWARE_ACTIONS: guessed['type'] = type(value) # guess type from choices (first item) if kwargs.get('choices') and 'type' not in list(guessed) + list(kwargs): guessed['type'] = type(kwargs['choices'][0]) return dict(kwargs, **guessed) def _fix_compat_issue29(function): # # TODO: remove before 1.0 release (will break backwards compatibility) # if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False): # a modern decorator is used, no compatibility issues return function if getattr(function, ATTR_INFER_ARGS_FROM_SIGNATURE, False): # wrapped in outdated decorator but it implies modern behaviour return function # Okay, now we've got either a modern-style function (plain signature) # or an old-style function which implicitly expects a namespace object. # It's very likely that in the latter case the function accepts one and # only argument named "args". If so, we simply wrap this function in # @expects_obj and issue a warning. spec = get_arg_spec(function) if spec.args in [['arg'], ['args'], ['self', 'arg'], ['self', 'args']]: # this is it -- a classic old-style function, goddamnit. # no checking *args and **kwargs because they are unlikely to matter. import warnings warnings.warn('Function {0} is very likely to be old-style, i.e. ' 'implicitly expects a namespace object. This behaviour ' 'is deprecated. Wrap it in @expects_obj decorator or ' 'convert to plain signature.'.format(function.__name__), DeprecationWarning) setattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, True) return function def _is_positional(args, prefix_chars='-'): assert args if 1 < len(args) or args[0][0].startswith(tuple(prefix_chars)): return False else: return True def _get_parser_param_kwargs(parser, argspec): argspec = argspec.copy() # parser methods modify source data args = argspec['option_strings'] if _is_positional(args, prefix_chars=parser.prefix_chars): kwargs = parser._get_positional_kwargs(*args, **argspec) else: kwargs = parser._get_optional_kwargs(*args, **argspec) kwargs['dest'] = kwargs['dest'].replace('-', '_') return kwargs def _get_dest(parser, argspec): kwargs = _get_parser_param_kwargs(parser, argspec) return kwargs['dest'] 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.assembling.add_commands`) results in a `RuntimeError`. .. note:: If there are both explicitly declared arguments (e.g. via :func:`~argh.decorators.arg`) and ones inferred from the function signature (e.g. via :func:`~argh.decorators.command`), declared ones will be merged into inferred ones. If an argument does not conform function signature, `AssemblingError` is raised. .. note:: If the parser was created with ``add_help=True`` (which is by default), option name ``-h`` is silently removed from any argument. """ if parser._subparsers: raise RuntimeError('Cannot set default command to a parser with ' 'existing subparsers') function = _fix_compat_issue29(function) spec = get_arg_spec(function) declared_args = getattr(function, ATTR_ARGS, []) inferred_args = list(_get_args_from_signature(function)) if inferred_args and declared_args: # We've got a mixture of declared and inferred arguments # a mapping of "dest" strings to argument declarations. # # * a "dest" string is a normalized form of argument name, i.e.: # # '-f', '--foo' → 'foo' # 'foo-bar' → 'foo_bar' # # * argument declaration is a dictionary representing an argument; # it is obtained either from _get_args_from_signature() or from # an @arg decorator (as is). # dests = OrderedDict() for argspec in inferred_args: dest = _get_parser_param_kwargs(parser, argspec)['dest'] dests[dest] = argspec for declared_kw in declared_args: # an argument is declared via decorator dest = _get_dest(parser, declared_kw) if dest in dests: # the argument is already known from function signature # # now make sure that this declared arg conforms to the function # signature and therefore only refines an inferred arg: # # @arg('my-foo') maps to func(my_foo) # @arg('--my-bar') maps to func(my_bar=...) # either both arguments are positional or both are optional decl_positional = _is_positional(declared_kw['option_strings']) infr_positional = _is_positional(dests[dest]['option_strings']) if decl_positional != infr_positional: kinds = {True: 'positional', False: 'optional'} raise AssemblingError( '{func}: argument "{dest}" declared as {kind_i} ' '(in function signature) and {kind_d} (via decorator)' .format( func=function.__name__, dest=dest, kind_i=kinds[infr_positional], kind_d=kinds[decl_positional], )) # merge explicit argument declaration into the inferred one # (e.g. `help=...`) dests[dest].update(**declared_kw) else: # the argument is not in function signature varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', [])) if varkw: # function accepts **kwargs; the argument goes into it dests[dest] = declared_kw else: # there's no way we can map the argument declaration # to function signature xs = (dests[x]['option_strings'] for x in dests) raise AssemblingError( '{func}: argument {flags} does not fit ' 'function signature: {sig}'.format( flags=', '.join(declared_kw['option_strings']), func=function.__name__, sig=', '.join('/'.join(x) for x in xs))) # pack the modified data back into a list inferred_args = dests.values() command_args = inferred_args or declared_args # add types, actions, etc. (e.g. default=3 implies type=int) command_args = [_guess(x) for x in command_args] for draft in command_args: draft = draft.copy() dest_or_opt_strings = draft.pop('option_strings') if parser.add_help and '-h' in dest_or_opt_strings: dest_or_opt_strings = [x for x in dest_or_opt_strings if x != '-h'] completer = draft.pop('completer', None) try: action = parser.add_argument(*dest_or_opt_strings, **draft) if COMPLETION_ENABLED and completer: action.completer = completer except Exception as e: raise type(e)('{func}: cannot add arg {args}: {msg}'.format( args='/'.join(dest_or_opt_strings), func=function.__name__, msg=e)) 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:`~argh.decorators.arg`, the arguments are passed to :class:`argparse.ArgumentParser.add_argument`. See also :func:`~argh.dispatching.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:`argparse.ArgumentParser.add_subparsers` as `title`. :param description: passed to :meth:`argparse.ArgumentParser.add_subparsers` as `description`. :param help: passed to :meth:`argparse.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:`~argh.helpers.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.assembling.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 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: # use explicitly defined name; if none, use function name (a_b → a-b) cmd_name = getattr(func, ATTR_NAME, func.__name__.replace('_','-')) parser_kwargs = { # add command help from function's docstring 'help': func.__doc__, # set default formatter 'formatter_class': PARSER_FORMATTER, } # try adding aliases for command name if SUPPORTS_ALIASES: parser_kwargs['aliases'] = getattr(func, ATTR_ALIASES, []) # create and set up the parser for this command command_parser = subparsers.add_parser(cmd_name, **parser_kwargs) set_default_command(command_parser, func) argh-0.24.1/argh/utils.py0000644000175000001440000000320212262445426015270 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Utilities ~~~~~~~~~ """ import argparse import inspect from argh import compat def get_subparsers(parser, create=False): """Returns the :class:`argparse._SubParsersAction` instance for given :class:`ArgumentParser` instance as would have been returned by :meth:`ArgumentParser.add_subparsers`. The problem with the latter is that it only works once and raises an exception on the second attempt, and the public API seems to lack a method to get *existing* subparsers. :param create: If `True`, creates the subparser if it does not exist. Default if `False`. """ # note that ArgumentParser._subparsers is *not* what is returned by # ArgumentParser.add_subparsers(). if parser._subparsers: actions = [a for a in parser._actions if isinstance(a, argparse._SubParsersAction)] assert len(actions) == 1 return actions[0] else: if create: return parser.add_subparsers() def get_arg_spec(function): """Returns argument specification for given function. Omits special arguments of instance methods (`self`) and static methods (usually `cls` or something like this). """ spec = compat.getargspec(function) if inspect.ismethod(function): spec = spec._replace(args=spec.args[1:]) return spec argh-0.24.1/argh/interaction.py0000644000175000001440000000453612262445426016462 0ustar andyusers00000000000000# coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko 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.rst for copying conditions. # """ Interaction ~~~~~~~~~~~ """ from argh.compat import text_type from argh.io import safe_input __all__ = ['confirm', 'safe_input'] 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:: def delete(key, silent=False): item = db.get(Item, args.key) if confirm('Delete '+item.title, default=True, skip=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 argh-0.24.1/README.rst0000644000175000001440000001137212262445223014326 0ustar andyusers00000000000000Argh: The Natural CLI ===================== .. image:: https://travis-ci.org/neithere/argh.png?branch=master :target: https://travis-ci.org/neithere/argh Building a command-line interface? Found yourself uttering "argh!" while struggling with the API of `argparse`? Don't like the complexity but need the power? .. epigraph:: Everything should be made as simple as possible, but no simpler. -- Albert Einstein (probably) `Argh` is a smart wrapper for `argparse`. `Argparse` is a very powerful tool; `Argh` just makes it easy to use. In a nutshell ------------- `Argh`-powered applications are *simple* but *flexible*: :Modular: Declaration of commands can be decoupled from assembling and dispatching; :Pythonic: Commands are declared naturally, no complex API calls in most cases; :Reusable: Commands are plain functions, can be used directly outside of CLI context; :Layered: The complexity of code raises with requirements; :Transparent: The full power of argparse is available whenever needed; :Namespaced: Nested commands are a piece of cake, no messing with subparsers (though they are of course used under the hood); :Term-Friendly: Command output is processed with respect to stream encoding; :Unobtrusive: `Argh` can dispatch a subset of pure-`argparse` code, and pure-`argparse` code can update and dispatch a parser assembled with `Argh`; :DRY: The amount of boilerplate code is minimal; among other things, `Argh` will: * infer command name from function name; * infer arguments from function signature; * infer argument type from the default value; * infer argument action from the default value (for booleans); * add an alias root command ``help`` for the ``--help`` argument. Sounds good? Check the tutorial! Relation to argparse -------------------- `Argh` is fully compatible with `argparse`. You can mix `Argh`-agnostic and `Argh`-aware code. Just keep in mind that the dispatcher does some extra work that a custom dispatcher may not do. Installation ------------ Using pip:: $ pip install argh Arch Linux (AUR):: $ yaourt python-argh Examples -------- A very simple application with one command: .. code-block:: python import argh def main(): return 'Hello world' argh.dispatch_command(main) A potentially modular application with multiple commands: .. code-block:: python import argh # declaring: def echo(text): return text def greeter(name, greeting='hello'): return greeting + ', ' + name # assembling: parser = argh.ArghParser() parser.add_commands([echo, greeter]) # dispatching: if __name__ == '__main__': parser.dispatch() The powerful API of `argparse` is also available: .. code-block:: python @arg('text', default='hello world', nargs='+', help='The message') def echo(text): print text The approaches can be safely combined even up to this level: .. code-block:: python # adding help to `foo` which is in the function signature: @arg('foo', help='blah') # these are not in the signature so they go to **kwargs: @arg('baz') @arg('-q', '--quux') # the function itself: def cmd(foo, bar=1, *args, **kwargs): yield foo yield bar yield ', '.join(args) yield kwargs['baz'] yield kwargs['quux'] Links ----- * `Project home page`_ (GitHub) * `Documentation`_ (Read the Docs) * `Package distribution`_ (PyPI) * Questions, requests, bug reports, etc.: * `Issue tracker`_ (GitHub) * `Mailing list`_ (subscribe to get important announcements) * Direct e-mail (neithere at gmail com) * Twitter_ (to get notified of commits; mostly for lulz) .. _project home page: http://github.com/neithere/argh/ .. _documentation: http://argh.readthedocs.org .. _package distribution: http://pypi.python.org/pypi/argh .. _issue tracker: http://github.com/neithere/argh/issues/ .. _mailing list: http://groups.google.com/group/argh-users .. _twitter: http://twitter.com/python_argh Author ------ Developed by Andrey Mikhaylenko since 2010. See file `AUTHORS` for a complete list of contributors to this library. Licensing --------- Argh is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Argh is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with Argh. If not, see . argh-0.24.1/test/0000755000175000001440000000000012262445620013613 5ustar andyusers00000000000000argh-0.24.1/test/test_dispatching.py0000644000175000001440000000166212262445451017530 0ustar andyusers00000000000000# coding: utf-8 """ Dispatching tests ~~~~~~~~~~~~~~~~~ """ import argh from .base import make_IO def _dispatch_and_capture(func, command_string, **kwargs): if hasattr(command_string, 'split'): args = command_string.split() else: args = command_string io = make_IO() if 'output_file' not in kwargs: kwargs['output_file'] = io result = argh.dispatch_command(func, args, **kwargs) if kwargs.get('output_file') is None: return result else: io.seek(0) return io.read() def run_func(func, command_string, **kwargs): try: result = _dispatch_and_capture(func, command_string, **kwargs) except SystemExit: raise else: return result def test_dispatch_command_shortcut(): @argh.arg('--foo', default=1) def cmd(args): return args.foo assert run_func(cmd, '') == '1\n' assert run_func(cmd, '--foo 2') == '2\n' argh-0.24.1/test/test_compat.py0000644000175000001440000000046612262445451016517 0ustar andyusers00000000000000from argh.compat import _PrimitiveOrderedDict def test_ordered_dict(): d = _PrimitiveOrderedDict() d['a'] = 1 d['b'] = 2 d['c'] = 3 assert list(d) == ['a', 'b', 'c'] assert d.keys() == ['a', 'b', 'c'] assert d.values() == [1, 2, 3] del d['b'] assert list(d) == ['a', 'c'] argh-0.24.1/test/__init__.py0000644000175000001440000000000012262445451015714 0ustar andyusers00000000000000argh-0.24.1/test/base.py0000644000175000001440000000403212262445451015100 0ustar andyusers00000000000000# coding: utf-8 """ Common stuff for tests ~~~~~~~~~~~~~~~~~~~~~~ """ import sys from collections import namedtuple from argh import ArghParser from argh.compat import BytesIO, StringIO CmdResult = namedtuple('CmdResult', ('out', 'err')) class DebugArghParser(ArghParser): "(does not print stuff to stderr on exit)" def exit(self, status=0, message=None): raise SystemExit(message) def error(self, message): self.exit(2, message) def make_IO(): "Returns a file object of the same type as `sys.stdout`." if sys.version_info < (3,0): return BytesIO() else: return StringIO() def call_cmd(parser, command_string, **kwargs): if hasattr(command_string, 'split'): args = command_string.split() else: args = command_string io_out = make_IO() io_err = make_IO() if 'output_file' not in kwargs: kwargs['output_file'] = io_out kwargs['errors_file'] = io_err result = parser.dispatch(args, **kwargs) if kwargs.get('output_file') is None: return CmdResult(out=result, err=io_err.read()) else: io_out.seek(0) io_err.seek(0) return CmdResult(out=io_out.read(), err=io_err.read()) def run(parser, command_string, kwargs=None, exit=False): """ Calls the command and returns result. If SystemExit is raised, it propagates. :exit: if set to `True`, then any SystemExit exception is catched and its string representation is returned; if the exception is not raised, an AssertionError is raised. In other words, this parameter inverts the function's behaviour and expects SystemExit as the correct event. """ kwargs = kwargs or {} try: return call_cmd(parser, command_string, **kwargs) except SystemExit as error: if exit: if error.args == (None,): return None else: return str(error) else: raise else: if exit: raise AssertionError('Did not exit') argh-0.24.1/test/test_assembling.py0000644000175000001440000001406712262445451017362 0ustar andyusers00000000000000# coding: utf-8 """ Unit Tests For Assembling Phase ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """ import sys import mock import pytest import argh def test_guess_type_from_choices(): old = dict(option_strings=('foo',), choices=[1,2]) new = dict(option_strings=('foo',), choices=[1,2], type=int) assert new == argh.assembling._guess(old) # ensure no overrides same = dict(option_strings=('foo',), choices=[1,2], type='NO_MATTER_WHAT') assert same == argh.assembling._guess(same) def test_guess_type_from_default(): old = dict(option_strings=('foo',), default=1) new = dict(option_strings=('foo',), default=1, type=int) assert new == argh.assembling._guess(old) # ensure no overrides same = dict(option_strings=('foo',), default=1, type='NO_MATTER_WHAT') assert same == argh.assembling._guess(same) def test_guess_action_from_default(): # True → store_false old = dict(option_strings=('foo',), default=False) new = dict(option_strings=('foo',), default=False, action='store_true') assert new == argh.assembling._guess(old) # True → store_false old = dict(option_strings=('foo',), default=True) new = dict(option_strings=('foo',), default=True, action='store_false') assert new == argh.assembling._guess(old) # ensure no overrides same = dict(option_strings=('foo',), default=False, action='NO_MATTER_WHAT') assert same == argh.assembling._guess(same) def test_set_default_command(): def func(): pass setattr(func, argh.constants.ATTR_ARGS, ( dict(option_strings=('foo',), nargs='+', choices=[1,2], help='me'), dict(option_strings=('-b', '--bar',), default=False), )) parser = argh.ArghParser() parser.add_argument = mock.MagicMock() parser.set_defaults = mock.MagicMock() argh.set_default_command(parser, func) assert parser.add_argument.mock_calls == [ mock.call('foo', nargs='+', choices=[1,2], help='me', type=int), mock.call('-b', '--bar', default=False, action='store_true') ] assert parser.set_defaults.mock_calls == [ mock.call(function=func) ] def test_set_default_command_docstring(): def func(): "docstring" pass parser = argh.ArghParser() argh.set_default_command(parser, func) assert parser.description == 'docstring' def test_set_default_command_vs_multiple(): def one(): return 1 def two(): return 2 p = argh.ArghParser() p.set_default_command(one) with pytest.raises(RuntimeError) as excinfo: p.add_commands([two]) msg = 'Cannot add commands to a single-command parser' assert msg == str(excinfo.value) def test_set_default_command_vs_subparsers(): def one(): return 1 def two(): return 2 p = argh.ArghParser() p.add_commands([one]) with pytest.raises(RuntimeError) as excinfo: p.set_default_command(two) msg = 'Cannot set default command to a parser with existing subparsers' assert msg == str(excinfo.value) def test_set_default_command_mixed_arg_types(): def func(): pass setattr(func, argh.constants.ATTR_ARGS, [ dict(option_strings=('x','--y')) ]) p = argh.ArghParser() with pytest.raises(ValueError) as excinfo: p.set_default_command(func) msg = "func: cannot add arg x/--y: invalid option string" assert msg in str(excinfo.value) def test_set_default_command_varargs(): def func(*file_paths): yield ', '.join(file_paths) parser = argh.ArghParser() parser.add_argument = mock.MagicMock() argh.set_default_command(parser, func) assert parser.add_argument.mock_calls == [ mock.call('file_paths', nargs='*'), ] def test_set_default_command_kwargs(): @argh.arg('foo') @argh.arg('--bar') def func(x, **kwargs): pass parser = argh.ArghParser() parser.add_argument = mock.MagicMock() argh.set_default_command(parser, func) assert parser.add_argument.mock_calls == [ mock.call('x'), mock.call('foo'), mock.call('--bar'), ] def test_annotation(): "Extracting argument help from function annotations (Python 3 only)." if sys.version_info < (3,0): pytest.skip('unsupported configuration') # Yes, this looks horrible, but otherwise Python 2 would die # screaming and cursing as it is completely in the dark about the # sweetness of annotations and we can but tolerate its ignorance. ns = {} exec("def cmd(foo : 'quux' = 123):\n 'bar'\npass", None, ns) cmd = ns['cmd'] p = argh.ArghParser() p.set_default_command(argh.command(cmd)) prog_help = p.format_help() assert 'quux' in prog_help def test_kwonlyargs(): "Correctly processing required and optional keyword-only arguments" if sys.version_info < (3,0): pytest.skip('unsupported configuration') ns = {} exec("def cmd(*args, foo='abcd', bar):\n" " return (args, foo, bar)", None, ns) cmd = ns['cmd'] p = argh.ArghParser() p.add_argument = mock.MagicMock() p.set_default_command(argh.command(cmd)) assert p.add_argument.mock_calls == [ mock.call('-f', '--foo', type=str, default='abcd'), mock.call('-b', '--bar', required=True), mock.call('args', nargs='*') ] @mock.patch('argh.assembling.COMPLETION_ENABLED', True) def test_custom_argument_completer(): "Issue #33: Enable custom per-argument shell completion" def func(foo): pass setattr(func, argh.constants.ATTR_ARGS, [ dict(option_strings=('foo',), completer='STUB') ]) p = argh.ArghParser() p.set_default_command(func) assert p._actions[-1].completer == 'STUB' @mock.patch('argh.assembling.COMPLETION_ENABLED', False) def test_custom_argument_completer_no_backend(): "If completion backend is not available, nothing breaks" def func(foo): pass setattr(func, argh.constants.ATTR_ARGS, [ dict(option_strings=('foo',), completer='STUB') ]) p = argh.ArghParser() p.set_default_command(func) assert not hasattr(p._actions[-1], 'completer') argh-0.24.1/test/test_decorators.py0000644000175000001440000000337712262445451017405 0ustar andyusers00000000000000# coding: utf-8 """ Unit Tests For Decorators ~~~~~~~~~~~~~~~~~~~~~~~~~ """ import argh def test_aliases(): @argh.aliases('one', 'two') def func(): pass attr = getattr(func, argh.constants.ATTR_ALIASES) assert attr == ('one', 'two') def test_arg(): @argh.arg('foo', help='help', nargs='+') @argh.arg('--bar', default=1) def func(): pass attrs = getattr(func, argh.constants.ATTR_ARGS) assert attrs == [ dict(option_strings=('foo',), help='help', nargs='+'), dict(option_strings=('--bar',), default=1), ] def test_named(): @argh.named('new-name') def func(): pass attr = getattr(func, argh.constants.ATTR_NAME) assert attr == 'new-name' def test_command(): @argh.command def func(): pass attr = getattr(func, argh.constants.ATTR_INFER_ARGS_FROM_SIGNATURE) assert attr == True def test_wrap_errors(): @argh.wrap_errors([KeyError, ValueError]) def func(): pass attr = getattr(func, argh.constants.ATTR_WRAPPED_EXCEPTIONS) assert attr == [KeyError, ValueError] def test_wrap_errors_processor(): @argh.wrap_errors(processor='STUB') def func(): pass attr = getattr(func, argh.constants.ATTR_WRAPPED_EXCEPTIONS_PROCESSOR) assert attr == 'STUB' def test_wrap_errors_compat(): "Legacy decorator signature. TODO: remove in 1.0" @argh.wrap_errors(KeyError, ValueError, TypeError) def func(): pass attr = getattr(func, argh.constants.ATTR_WRAPPED_EXCEPTIONS) assert attr == [KeyError, ValueError, TypeError] def test_expects_obj(): @argh.expects_obj def func(args): pass attr = getattr(func, argh.constants.ATTR_EXPECTS_NAMESPACE_OBJECT) assert attr == True argh-0.24.1/test/test_integration.py0000644000175000001440000004610312262445451017555 0ustar andyusers00000000000000# coding: utf-8 """ Integration Tests ~~~~~~~~~~~~~~~~~ """ import sys import re import argparse import mock import pytest import argh from argh.exceptions import AssemblingError from .base import DebugArghParser, run, CmdResult as R @pytest.mark.xfail(reason='TODO') def test_guessing_integration(): "guessing is used in dispatching" assert 0 def test_set_default_command_integration(): @argh.arg('--foo', default=1) def cmd(args): return args.foo p = DebugArghParser() p.set_default_command(cmd) assert run(p, '') == R(out='1\n', err='') assert run(p, '--foo 2') == R(out='2\n', err='') assert run(p, '--help', exit=True) == None def test_set_default_command_integration_merging(): @argh.arg('--foo', help='bar') def cmd(foo=1): return foo p = DebugArghParser() p.set_default_command(cmd) assert run(p, '') == R(out='1\n', err='') assert run(p, '--foo 2') == R(out='2\n', err='') assert 'bar' in p.format_help() # # Function can be added to parser as is # def test_simple_function_no_args(): def cmd(): yield 1 p = DebugArghParser() p.set_default_command(cmd) assert run(p, '') == R(out='1\n', err='') def test_simple_function_positional(): def cmd(x): yield x p = DebugArghParser() p.set_default_command(cmd) if sys.version_info < (3,3): msg = 'too few arguments' else: msg = 'the following arguments are required: x' assert run(p, '', exit=True) == msg assert run(p, 'foo') == R(out='foo\n', err='') def test_simple_function_defaults(): def cmd(x='foo'): yield x p = DebugArghParser() p.set_default_command(cmd) assert run(p, '') == R(out='foo\n', err='') assert run(p, 'bar', exit=True) == 'unrecognized arguments: bar' assert run(p, '--x bar') == R(out='bar\n', err='') def test_simple_function_varargs(): def func(*file_paths): # `paths` is the single positional argument with nargs='*' yield ', '.join(file_paths) p = DebugArghParser() p.set_default_command(func) assert run(p, '') == R(out='\n', err='') assert run(p, 'foo') == R(out='foo\n', err='') assert run(p, 'foo bar') == R(out='foo, bar\n', err='') def test_simple_function_kwargs(): @argh.arg('foo') @argh.arg('--bar') def cmd(**kwargs): # `kwargs` contain all arguments not fitting ArgSpec.args and .varargs. # if ArgSpec.keywords in None, all @arg()'s will have to fit ArgSpec.args for k in sorted(kwargs): yield '{0}: {1}'.format(k, kwargs[k]) p = DebugArghParser() p.set_default_command(cmd) if sys.version_info < (3,3): msg = 'too few arguments' else: msg = 'the following arguments are required: foo' assert run(p, '', exit=True) == msg assert run(p, 'hello') == R(out='bar: None\nfoo: hello\n', err='') assert run(p, '--bar 123', exit=True) == msg assert run(p, 'hello --bar 123') == R(out='bar: 123\nfoo: hello\n', err='') def test_simple_function_multiple(): pass def test_simple_function_nested(): pass def test_class_method_as_command(): pass def test_all_specs_in_one(): @argh.arg('foo') @argh.arg('--bar') @argh.arg('fox') @argh.arg('--baz') def cmd(foo, bar=1, *args, **kwargs): yield 'foo: {0}'.format(foo) yield 'bar: {0}'.format(bar) yield '*args: {0}'.format(args) for k in sorted(kwargs): yield '** {0}: {1}'.format(k, kwargs[k]) p = DebugArghParser() p.set_default_command(cmd) # 1) bar=1 is treated as --bar so positionals from @arg that go **kwargs # will still have higher priority than bar. # 2) *args, a positional with nargs='*', sits between two required # positionals (foo and fox), so it gets nothing. assert run(p, 'one two') == R(out= 'foo: one\n' 'bar: 1\n' '*args: ()\n' '** baz: None\n' '** fox: two\n', err='') # two required positionals (foo and fox) get an argument each and one extra # is left; therefore the middle one is given to *args. assert run(p, 'one two three') == R(out= 'foo: one\n' 'bar: 1\n' "*args: ('two',)\n" '** baz: None\n' '** fox: three\n', err='') # two required positionals (foo and fox) get an argument each and two extra # are left; both are given to *args (it's greedy). assert run(p, 'one two three four') == R(out= 'foo: one\n' 'bar: 1\n' "*args: ('two', 'three')\n" '** baz: None\n' '** fox: four\n', err='') def test_arg_merged(): """ @arg merges into function signature. """ @argh.arg('my', help='a moose once bit my sister') @argh.arg('-b', '--brain', help='i am made entirely of wood') def gumby(my, brain=None): return my, brain, 'hurts' p = DebugArghParser('PROG') p.set_default_command(gumby) help_msg = p.format_help() assert 'a moose once bit my sister' in help_msg assert 'i am made entirely of wood' in help_msg def test_arg_mismatch_positional(): """ An `@arg('positional')` must match function signature. """ @argh.arg('bogus-argument') def confuse_a_cat(vet, funny_things=123): return vet, funny_things p = DebugArghParser('PROG') with pytest.raises(AssemblingError) as excinfo: p.set_default_command(confuse_a_cat) msg = ("confuse_a_cat: argument bogus-argument does not fit " "function signature: vet, -f/--funny-things") assert msg in str(excinfo.value) def test_arg_mismatch_flag(): """ An `@arg('--flag')` must match function signature. """ @argh.arg('--bogus-argument') def confuse_a_cat(vet, funny_things=123): return vet, funny_things p = DebugArghParser('PROG') with pytest.raises(AssemblingError) as excinfo: p.set_default_command(confuse_a_cat) msg = ("confuse_a_cat: argument --bogus-argument does not fit " "function signature: vet, -f/--funny-things") assert msg in str(excinfo.value) def test_arg_mismatch_positional_vs_flag(): """ An `@arg('arg')` must match a positional arg in function signature. """ @argh.arg('foo') def func(foo=123): return foo p = DebugArghParser('PROG') with pytest.raises(AssemblingError) as excinfo: p.set_default_command(func) msg = ('func: argument "foo" declared as optional (in function signature)' ' and positional (via decorator)') assert msg in str(excinfo.value) def test_arg_mismatch_flag_vs_positional(): """ An `@arg('--flag')` must match a keyword in function signature. """ @argh.arg('--foo') def func(foo): return foo p = DebugArghParser('PROG') with pytest.raises(AssemblingError) as excinfo: p.set_default_command(func) msg = ('func: argument "foo" declared as positional (in function signature)' ' and optional (via decorator)') assert msg in str(excinfo.value) def test_backwards_compatibility_issue29(): @argh.arg('foo') @argh.arg('--bar', default=1) def old(args): yield '{0} {1}'.format(args.foo, args.bar) @argh.command def old_marked(foo, bar=1): yield '{0} {1}'.format(foo, bar) def new(foo, bar=1): yield '{0} {1}'.format(foo, bar) p = DebugArghParser('PROG') p.add_commands([old, old_marked, new]) assert R('ok 1\n', '') == run(p, 'old ok') assert R('ok 5\n', '') == run(p, 'old ok --bar 5') assert R('ok 1\n', '') == run(p, 'old-marked ok') assert R('ok 5\n', '') == run(p, 'old-marked ok --bar 5') assert R('ok 1\n', '') == run(p, 'new ok') assert R('ok 5\n', '') == run(p, 'new ok --bar 5') class TestErrorWrapping: def _get_parrot(self): def parrot(dead=False): if dead: raise ValueError('this parrot is no more') else: return 'beautiful plumage' return parrot def test_error_raised(self): parrot = self._get_parrot() p = DebugArghParser() p.set_default_command(parrot) assert run(p, '') == R('beautiful plumage\n', '') with pytest.raises(ValueError) as excinfo: run(p, '--dead') assert re.match('this parrot is no more', str(excinfo.value)) def test_error_wrapped(self): parrot = self._get_parrot() wrapped_parrot = argh.wrap_errors(ValueError)(parrot) p = DebugArghParser() p.set_default_command(wrapped_parrot) assert run(p, '') == R('beautiful plumage\n', '') assert run(p, '--dead') == R('', 'ValueError: this parrot is no more\n') def test_processor(self): parrot = self._get_parrot() wrapped_parrot = argh.wrap_errors(ValueError)(parrot) def failure(err): return 'ERR: ' + str(err) + '!' processed_parrot = argh.wrap_errors(processor=failure)(wrapped_parrot) p = argh.ArghParser() p.set_default_command(processed_parrot) assert run(p, '--dead') == R('', 'ERR: this parrot is no more!\n') def test_stderr_vs_stdout(self): @argh.wrap_errors([KeyError]) def func(key): db = {'a': 1} return db[key] p = argh.ArghParser() p.set_default_command(func) assert run(p, 'a') == R(out='1\n', err='') assert run(p, 'b') == R(out='', err="KeyError: 'b'\n") def test_argv(): @argh.arg('text') def echo(args): return 'you said {0}'.format(args.text) p = DebugArghParser() p.add_commands([echo]) _argv = sys.argv sys.argv = sys.argv[:1] + ['echo', 'hi there'] assert run(p, None) == R('you said hi there\n', '') sys.argv = _argv def test_commands_not_defined(): p = DebugArghParser() assert run(p, '', {'raw_output': True}).out == p.format_usage() assert run(p, '').out == p.format_usage() + '\n' assert 'unrecognized arguments' in run(p, 'foo', exit=True) assert 'unrecognized arguments' in run(p, '--foo', exit=True) def test_command_not_chosen(): def cmd(args): return 1 p = DebugArghParser() p.add_commands([cmd]) if sys.version_info < (3,3): # Python before 3.3 exits with an error assert 'too few arguments' in run(p, '', exit=True) else: # Python since 3.3 returns a help message and doesn't exit assert 'usage:' in run(p, '').out def test_invalid_choice(): def cmd(args): return 1 # root level command p = DebugArghParser() p.add_commands([cmd]) assert run(p, 'bar', exit=True).startswith('invalid choice') if sys.version_info < (3,3): # Python before 3.3 exits with a less informative error assert 'too few arguments' in run(p, '--bar', exit=True) else: # Python since 3.3 exits with a more informative error assert run(p, '--bar', exit=True) == 'unrecognized arguments: --bar' # nested command p = DebugArghParser() p.add_commands([cmd], namespace='nest') assert run(p, 'nest bar', exit=True).startswith('invalid choice') if sys.version_info < (3,3): # Python before 3.3 exits with a less informative error assert 'too few arguments' in run(p, 'nest --bar', exit=True) else: # Python since 3.3 exits with a more informative error assert run(p, 'nest --bar', exit=True) == 'unrecognized arguments: --bar' def test_unrecognized_arguments(): def cmd(args): return 1 # single-command parser p = DebugArghParser() p.set_default_command(cmd) assert run(p, '--bar', exit=True) == 'unrecognized arguments: --bar' assert run(p, 'bar', exit=True) == 'unrecognized arguments: bar' # multi-command parser p = DebugArghParser() p.add_commands([cmd]) assert run(p, 'cmd --bar', exit=True) == 'unrecognized arguments: --bar' assert run(p, 'cmd bar', exit=True) == 'unrecognized arguments: bar' def test_echo(): "A simple command is resolved to a function." @argh.arg('text') def echo(args): return 'you said {0}'.format(args.text) p = DebugArghParser() p.add_commands([echo]) assert run(p, 'echo foo') == R(out='you said foo\n', err='') def test_bool_action(): "Action `store_true`/`store_false` is inferred from default value." @argh.arg('--dead', default=False) def parrot(args): return 'this parrot is no more' if args.dead else 'beautiful plumage' p = DebugArghParser() p.add_commands([parrot]) assert run(p, 'parrot').out == 'beautiful plumage\n' assert run(p, 'parrot --dead').out == 'this parrot is no more\n' def test_bare_namespace(): "A command can be resolved to a function, not a namespace." def hello(): return 'hello world' p = DebugArghParser() p.add_commands([hello], namespace='greet') # without arguments if sys.version_info < (3,3): # Python before 3.3 exits with an error assert run(p, 'greet', exit=True) == 'too few arguments' else: # Python since 3.3 returns a help message and doesn't exit assert 'usage:' in run(p, 'greet', exit=True).out # with an argument if sys.version_info < (3,3): # Python before 3.3 exits with a less informative error message = 'too few arguments' else: # Python since 3.3 exits with a more informative error message = 'unrecognized arguments: --name=world' assert run(p, 'greet --name=world', exit=True) == message def test_namespaced_function(): "A subcommand is resolved to a function." def hello(name='world'): return 'Hello {0}!'.format(name or 'world') def howdy(buddy): return 'Howdy {0}?'.format(buddy) p = DebugArghParser() p.add_commands([hello, howdy], namespace='greet') assert run(p, 'greet hello').out == 'Hello world!\n' assert run(p, 'greet hello --name=John').out == 'Hello John!\n' assert run(p, 'greet hello John', exit=True) == 'unrecognized arguments: John' if sys.version_info < (3,3): # Python before 3.3 exits with a less informative error message = 'too few arguments' else: # Python since 3.3 exits with a more informative error message = 'the following arguments are required: buddy' assert message in run(p, 'greet howdy --name=John', exit=True) assert run(p, 'greet howdy John').out == 'Howdy John?\n' def test_explicit_cmd_name(): @argh.named('new-name') def orig_name(): return 'ok' p = DebugArghParser() p.add_commands([orig_name]) assert run(p, 'orig-name', exit=True).startswith('invalid choice') assert run(p, 'new-name').out == 'ok\n' def test_aliases(): @argh.aliases('alias2', 'alias3') def alias1(): return 'ok' p = DebugArghParser() p.add_commands([alias1]) if argh.assembling.SUPPORTS_ALIASES: assert run(p, 'alias1').out == 'ok\n' assert run(p, 'alias2').out == 'ok\n' assert run(p, 'alias3').out == 'ok\n' def test_help_alias(): p = DebugArghParser() # assert the commands don't fail assert None == run(p, '--help', exit=True) assert None == run(p, 'greet --help', exit=True) assert None == run(p, 'greet hello --help', exit=True) assert None == run(p, 'help', exit=True) assert None == run(p, 'help greet', exit=True) assert None == run(p, 'help greet hello', exit=True) def test_arg_order(): """Positional arguments are resolved in the order in which the @arg decorators are defined. """ @argh.arg('foo') @argh.arg('bar') def cmd(args): return args.foo, args.bar p = DebugArghParser() p.set_default_command(cmd) assert run(p, 'foo bar').out == 'foo\nbar\n' def test_raw_output(): "If the raw_output flag is set, no extra whitespace is added" def cmd(foo, bar): return foo, bar p = DebugArghParser() p.set_default_command(cmd) assert run(p, 'foo bar').out == 'foo\nbar\n' assert run(p, 'foo bar', {'raw_output': True}).out == 'foobar' def test_output_file(): def cmd(): return 'Hello world!' p = DebugArghParser() p.set_default_command(cmd) assert run(p, '').out == 'Hello world!\n' assert run(p, '', {'output_file': None}).out == 'Hello world!\n' def test_command_error(): def whiner_plain(): raise argh.CommandError('I feel depressed.') def whiner_iterable(): yield 'Hello...' raise argh.CommandError('I feel depressed.') p = DebugArghParser() p.add_commands([whiner_plain, whiner_iterable]) assert run(p, 'whiner-plain') == R( out='', err='CommandError: I feel depressed.\n') assert run(p, 'whiner-iterable') == R( out='Hello...\n', err='CommandError: I feel depressed.\n') def test_custom_namespace(): @argh.expects_obj def cmd(args): return args.custom_value p = DebugArghParser() p.set_default_command(cmd) namespace = argparse.Namespace() namespace.custom_value = 'foo' assert run(p, '', {'namespace': namespace}).out == 'foo\n' def test_normalized_keys(): """ Underscores in function args are converted to dashes and back. """ def cmd(a_b): return a_b p = DebugArghParser() p.set_default_command(cmd) assert run(p, 'hello').out == 'hello\n' @mock.patch('argh.assembling.COMPLETION_ENABLED', True) def test_custom_argument_completer(): "Issue #33: Enable custom per-argument shell completion" @argh.arg('foo', completer='STUB') def func(foo): pass p = argh.ArghParser() p.set_default_command(func) assert p._actions[-1].completer == 'STUB' def test_class_members(): "Issue #34: class members as commands" class Controller: var = 123 def instance_meth(self, value): return value, self.var @classmethod def class_meth(cls, value): return value, cls.var @staticmethod def static_meth(value): return value, 'w00t?' @staticmethod def static_meth2(value): return value, 'huh!' controller = Controller() p = DebugArghParser() p.add_commands([ controller.instance_meth, controller.class_meth, controller.static_meth, Controller.static_meth2, ]) assert run(p, 'instance-meth foo').out == 'foo\n123\n' assert run(p, 'class-meth foo').out == 'foo\n123\n' assert run(p, 'static-meth foo').out == 'foo\nw00t?\n' assert run(p, 'static-meth2 foo').out == 'foo\nhuh!\n' def test_kwonlyargs(): "Correct dispatch in presence of keyword-only arguments" if sys.version_info < (3,0): pytest.skip('unsupported configuration') ns = {} exec("""def cmd(*args, foo='1', bar, baz='3', **kwargs): return ' '.join(args), foo, bar, baz, len(kwargs) """, None, ns) cmd = ns['cmd'] p = DebugArghParser() p.set_default_command(cmd) assert (run(p, '--baz=done test this --bar=do').out == 'test this\n1\ndo\ndone\n0\n') if sys.version_info < (3,3): message = 'argument --bar is required' else: message = 'the following arguments are required: --bar' assert run(p, 'test --foo=do', exit=True) == message argh-0.24.1/test/test_regressions.py0000644000175000001440000000737712262445451017607 0ustar andyusers00000000000000# coding: utf-8 """ Regression tests ~~~~~~~~~~~~~~~~ """ import pytest import argh from .base import DebugArghParser, run def test_regression_issue12(): """Issue #12: @command was broken if there were more than one argument to begin with same character (i.e. short option names were inferred incorrectly). """ def cmd(foo=1, fox=2): yield 'foo {0}, fox {1}'.format(foo, fox) p = DebugArghParser() p.set_default_command(cmd) assert run(p, '').out == 'foo 1, fox 2\n' assert run(p, '--foo 3').out == 'foo 3, fox 2\n' assert run(p, '--fox 3').out == 'foo 1, fox 3\n' assert 'unrecognized' in run(p, '-f 3', exit=True) def test_regression_issue12_help_flag(): """Issue #12: if an argument starts with "h", e.g. "--host", ArgumentError is raised because "--help" is always added by argh without decorators. """ def ddos(host='localhost'): return 'so be it, {0}!'.format(host) # no help → no conflict p = DebugArghParser('PROG', add_help=False) p.set_default_command(ddos) assert run(p, '-h 127.0.0.1').out == 'so be it, 127.0.0.1!\n' # help added → conflict → short name ignored p = DebugArghParser('PROG', add_help=True) p.set_default_command(ddos) assert None == run(p, '-h 127.0.0.1', exit=True) def test_regression_issue27(): """Issue #27: store_true is not set for inferred bool argument. :Reason: when @command was refactored, it stopped using @arg, but it is it was there that guesses (choices→type, default→type and default→action) were made. """ def parrot(dead=False): return 'this parrot is no more' if dead else 'beautiful plumage' def grenade(count=3): if count == 3: return 'Three shall be the number thou shalt count' else: return '{0!r} is right out'.format(count) p = DebugArghParser() p.add_commands([parrot, grenade]) # default → type (int) assert run(p, 'grenade').out == ('Three shall be the number ' 'thou shalt count\n') assert run(p, 'grenade --count 5').out == '5 is right out\n' # default → action (store_true) assert run(p, 'parrot').out == 'beautiful plumage\n' assert run(p, 'parrot --dead').out == 'this parrot is no more\n' def test_regression_issue29(): class Tool(object): def __init__(self): self.p = DebugArghParser() self.p.add_commands([self.test_command]) @argh.alias("test") @argh.arg("a1") @argh.arg("a2") def test_command(self, args): return "test" tool = Tool() assert run(tool.p, "test 123 456").out == "test\n" def test_regression_issue31(): """ Issue #31: Argh fails with parameter action type 'count' if a default value is provided. :Reason: assembling._guess() would infer type from default value without regard to the action. _CountAction does not accept argument "type". :Solution: restricted type inferring to actions "store" and "append". """ @argh.arg('-v', '--verbose', dest='verbose', action='count', default=0) def cmd(**kwargs): yield kwargs.get('verbose', -1) p = DebugArghParser() p.set_default_command(cmd) assert '0\n' == run(p, '').out assert '1\n' == run(p, '-v').out assert '2\n' == run(p, '-vv').out def test_regression_issue47(): @argh.arg('--foo-bar', default="full") def func(foo_bar): return 'hello' p = DebugArghParser() with pytest.raises(argh.assembling.AssemblingError) as excinfo: p.set_default_command(func) msg = ('func: argument "foo_bar" declared as positional (in function ' 'signature) and optional (via decorator)') assert excinfo.exconly().endswith(msg) argh-0.24.1/test/test_interaction.py0000644000175000001440000000331512262445451017547 0ustar andyusers00000000000000# coding: utf-8 """ Interaction Tests ~~~~~~~~~~~~~~~~~ """ import sys import mock import argh def parse_choice(choice, **kwargs): argh.io._input = lambda prompt: choice return argh.confirm('test', **kwargs) def test_simple(): assert None == parse_choice('') assert None == parse_choice('', default=None) assert True == parse_choice('', default=True) assert False == parse_choice('', default=False) assert True == parse_choice('y') assert True == parse_choice('y', default=True) assert True == parse_choice('y', default=False) assert True == parse_choice('y', default=None) assert False == parse_choice('n') assert False == parse_choice('n', default=True) assert False == parse_choice('n', default=False) assert False == parse_choice('n', default=None) assert None == parse_choice('x') == None def test_prompt(): "Prompt is properly formatted" prompts = [] def raw_input_mock(prompt): prompts.append(prompt) argh.io._input = raw_input_mock argh.confirm('do smth') assert prompts[-1] == 'do smth? (y/n)' argh.confirm('do smth', default=None) assert prompts[-1] == 'do smth? (y/n)' argh.confirm('do smth', default=True) assert prompts[-1] == 'do smth? (Y/n)' argh.confirm('do smth', default=False) assert prompts[-1] == 'do smth? (y/N)' def test_encoding(): "Unicode is accepted as prompt message" raw_input_mock = mock.MagicMock() argh.io._input = raw_input_mock msg = 'привет' if sys.version_info <= (3,0): msg = msg.decode('utf-8') argh.confirm(msg) # bytes in Python 2.x, Unicode in Python 3.x raw_input_mock.assert_called_once_with('привет? (y/n)') argh-0.24.1/setup.cfg0000644000175000001440000000007312262445620014455 0ustar andyusers00000000000000[egg_info] tag_date = 0 tag_svn_revision = 0 tag_build = argh-0.24.1/setup.py0000755000175000001440000000716612262445453014367 0ustar andyusers00000000000000#!/usr/bin/env python # coding: utf-8 # # Copyright © 2010—2014 Andrey Mikhaylenko and contributors # # This file is part of Argh. # # Argh is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as published # by the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Argh is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License # along with Argh. If not, see . import io import os import sys from setuptools import setup from setuptools.command.test import test as TestCommand if sys.version_info < (2,7): # # Python 2.6 # install_requires = ['argparse >= 1.1'] # Importing `__version__` from `argh` would trigger a cascading import # of `argparse`. Avoiding this as Python < 2.7 ships without argparse. __version__ = None with io.open('argh/__init__.py', encoding='utf8') as f: for line in f: if line.startswith('__version__'): exec(line) break assert __version__, 'argh.__version__ must be imported correctly' else: # # Python 2.7, 3.x # install_requires = [] from argh import __version__ with io.open(os.path.join(os.path.dirname(__file__), 'README.rst'), encoding='ascii') as f: readme = f.read() class PyTest(TestCommand): # see http://pytest.org/latest/goodpractises.html#integration-with-setuptools-distribute-test-commands def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True def run_tests(self): #import here, cause outside the eggs aren't loaded import pytest errno = pytest.main(self.test_args) sys.exit(errno) setup( # overview name = 'argh', description = 'An unobtrusive argparse wrapper with natural syntax', long_description = readme, # technical info version = __version__, packages = ['argh'], provides = ['argh'], install_requires = install_requires, # testing tests_require = ['pytest'], cmdclass = {'test': PyTest}, # copyright author = 'Andrey Mikhaylenko', author_email = 'neithere@gmail.com', license = 'GNU Lesser General Public License (LGPL), Version 3', # more info url = 'http://github.com/neithere/argh/', # categorization keywords = ('cli command line argparse optparse argument option'), classifiers = [ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', 'Intended Audience :: Information Technology', 'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Topic :: Software Development :: User Interfaces', 'Topic :: Software Development :: Libraries :: Python Modules', ], )