argh-0.26.2/ 0000755 0001750 0000144 00000000000 12714716035 012642 5 ustar andy users 0000000 0000000 argh-0.26.2/MANIFEST.in 0000644 0001750 0000144 00000000122 12204176401 014362 0 ustar andy users 0000000 0000000 # needed for `python setup.py test`
include test/__init__.py
include test/base.py
argh-0.26.2/argh.egg-info/ 0000755 0001750 0000144 00000000000 12714716035 015255 5 ustar andy users 0000000 0000000 argh-0.26.2/argh.egg-info/PKG-INFO 0000644 0001750 0000144 00000024750 12714716033 016360 0 ustar andy users 0000000 0000000 Metadata-Version: 1.1
Name: argh
Version: 0.26.2
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://img.shields.io/coveralls/neithere/argh.svg
:target: https://coveralls.io/r/neithere/argh
.. image:: https://img.shields.io/travis/neithere/argh.svg
:target: https://travis-ci.org/neithere/argh
.. image:: https://img.shields.io/pypi/format/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/status/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/v/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/pyversions/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/dd/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://readthedocs.org/projects/argh/badge/?version=stable
:target: http://argh.readthedocs.org/en/stable/
.. image:: https://readthedocs.org/projects/argh/badge/?version=latest
:target: http://argh.readthedocs.org/en/latest/
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.
:NIH free:
`Argh` supports *completion*, *progress bars* and everything else by being
friendly to excellent 3rd-party libraries. No need to reinvent the wheel.
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)
Run it:
.. code-block:: bash
$ ./app.py
Hello world
A potentially modular application with multiple commands:
.. code-block:: python
import argh
# declaring:
def echo(text):
"Returns given word as is."
return text
def greet(name, greeting='Hello'):
"Greets the user with given name. The greeting is customizable."
return greeting + ', ' + name
# assembling:
parser = argh.ArghParser()
parser.add_commands([echo, greet])
# dispatching:
if __name__ == '__main__':
parser.dispatch()
Of course it works:
.. code-block:: bash
$ ./app.py greet Andy
Hello, Andy
$ ./app.py greet Andy -g Arrrgh
Arrrgh, Andy
Here's the auto-generated help for this application (note how the docstrings
are reused)::
$ ./app.py help
usage: app.py {echo,greet} ...
positional arguments:
echo Returns given word as is.
greet Greets the user with given name. The greeting is customizable.
...and for a specific command (an ordinary function signature is converted
to CLI arguments)::
$ ./app.py help greet
usage: app.py greet [-g GREETING] name
Greets the user with given name. The greeting is customizable.
positional arguments:
name
optional arguments:
-g GREETING, --greeting GREETING 'Hello'
(The help messages have been simplified a bit for brevity.)
`Argh` easily maps plain Python functions to CLI. Sometimes this is not
enough; in these cases 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)
.. _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
Author
------
Developed by Andrey Mikhaylenko since 2010.
See file `AUTHORS` for a complete list of contributors to this library.
Support
-------
The fastest way to improve this project is to submit tested and documented
patches or detailed bug reports.
Otherwise you can "flattr" me: |FlattrLink|_
.. _FlattrLink: https://flattr.com/submit/auto?user_id=neithere&url=http%3A%2F%2Fpypi.python.org%2Fpypi%2Fargh
.. |FlattrLink| image:: https://api.flattr.com/button/flattr-badge-large.png
:alt: Flattr the Argh project
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.4
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.26.2/argh.egg-info/SOURCES.txt 0000644 0001750 0000144 00000001056 12714716034 017142 0 ustar andy users 0000000 0000000 MANIFEST.in
README.rst
setup.cfg
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.py argh-0.26.2/argh.egg-info/top_level.txt 0000644 0001750 0000144 00000000005 12714716033 020000 0 ustar andy users 0000000 0000000 argh
argh-0.26.2/argh.egg-info/dependency_links.txt 0000644 0001750 0000144 00000000001 12714716033 021321 0 ustar andy users 0000000 0000000
argh-0.26.2/PKG-INFO 0000644 0001750 0000144 00000024750 12714716035 013747 0 ustar andy users 0000000 0000000 Metadata-Version: 1.1
Name: argh
Version: 0.26.2
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://img.shields.io/coveralls/neithere/argh.svg
:target: https://coveralls.io/r/neithere/argh
.. image:: https://img.shields.io/travis/neithere/argh.svg
:target: https://travis-ci.org/neithere/argh
.. image:: https://img.shields.io/pypi/format/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/status/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/v/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/pyversions/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/dd/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://readthedocs.org/projects/argh/badge/?version=stable
:target: http://argh.readthedocs.org/en/stable/
.. image:: https://readthedocs.org/projects/argh/badge/?version=latest
:target: http://argh.readthedocs.org/en/latest/
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.
:NIH free:
`Argh` supports *completion*, *progress bars* and everything else by being
friendly to excellent 3rd-party libraries. No need to reinvent the wheel.
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)
Run it:
.. code-block:: bash
$ ./app.py
Hello world
A potentially modular application with multiple commands:
.. code-block:: python
import argh
# declaring:
def echo(text):
"Returns given word as is."
return text
def greet(name, greeting='Hello'):
"Greets the user with given name. The greeting is customizable."
return greeting + ', ' + name
# assembling:
parser = argh.ArghParser()
parser.add_commands([echo, greet])
# dispatching:
if __name__ == '__main__':
parser.dispatch()
Of course it works:
.. code-block:: bash
$ ./app.py greet Andy
Hello, Andy
$ ./app.py greet Andy -g Arrrgh
Arrrgh, Andy
Here's the auto-generated help for this application (note how the docstrings
are reused)::
$ ./app.py help
usage: app.py {echo,greet} ...
positional arguments:
echo Returns given word as is.
greet Greets the user with given name. The greeting is customizable.
...and for a specific command (an ordinary function signature is converted
to CLI arguments)::
$ ./app.py help greet
usage: app.py greet [-g GREETING] name
Greets the user with given name. The greeting is customizable.
positional arguments:
name
optional arguments:
-g GREETING, --greeting GREETING 'Hello'
(The help messages have been simplified a bit for brevity.)
`Argh` easily maps plain Python functions to CLI. Sometimes this is not
enough; in these cases 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)
.. _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
Author
------
Developed by Andrey Mikhaylenko since 2010.
See file `AUTHORS` for a complete list of contributors to this library.
Support
-------
The fastest way to improve this project is to submit tested and documented
patches or detailed bug reports.
Otherwise you can "flattr" me: |FlattrLink|_
.. _FlattrLink: https://flattr.com/submit/auto?user_id=neithere&url=http%3A%2F%2Fpypi.python.org%2Fpypi%2Fargh
.. |FlattrLink| image:: https://api.flattr.com/button/flattr-badge-large.png
:alt: Flattr the Argh project
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.4
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.26.2/argh/ 0000755 0001750 0000144 00000000000 12714716035 013563 5 ustar andy users 0000000 0000000 argh-0.26.2/argh/completion.py 0000644 0001750 0000144 00000005577 12423015563 016317 0 ustar andy users 0000000 0000000 # 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 logging
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']
logger = logging.getLogger(__package__)
def autocomplete(parser):
"""
Adds support for shell completion via argcomplete_ by patching given
`argparse.ArgumentParser` (sub)class.
If completion is not enabled, logs a debug-level message.
"""
if COMPLETION_ENABLED:
argcomplete.autocomplete(parser)
elif 'bash' in os.getenv('SHELL', ''):
logger.debug('Bash completion not available. Install argcomplete.')
argh-0.26.2/argh/exceptions.py 0000644 0001750 0000144 00000003011 12423272746 016314 0 ustar andy users 0000000 0000000 # 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 DispatchingError(Exception):
"""
Raised if the dispatching could not be completed due to misconfiguration
which could not be determined on an earlier stage.
"""
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.26.2/argh/decorators.py 0000644 0001750 0000144 00000013775 12714704024 016312 0 ustar andy users 0000000 0000000 # 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.constants import (ATTR_ALIASES, ATTR_ARGS, ATTR_NAME,
ATTR_WRAPPED_EXCEPTIONS,
ATTR_WRAPPED_EXCEPTIONS_PROCESSOR,
ATTR_EXPECTS_NAMESPACE_OBJECT)
__all__ = ['aliases', 'named', 'arg', '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 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 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 of the decorator matches that of
:meth:`argparse.ArgumentParser.add_argument`, only some keywords are not
required if they can be easily guessed (e.g. you don't have to specify type
or action when an `int` or `bool` default value is supplied).
Typical use cases:
- In combination with :func:`expects_obj` (which is not recommended);
- in combination with ordinary function signatures to add details that
cannot be expressed with that syntax (e.g. help message).
Usage::
from argh import arg
@arg('path', help='path to the file to load')
@arg('--format', choices=['yaml','json'])
@arg('-v', '--verbosity', choices=range(0,3), default=2)
def load(path, something=None, format='json', dry_run=False, verbosity=1):
loaders = {'json': json.load, 'yaml': yaml.load}
loader = loaders[args.format]
data = loader(args.path)
if not args.dry_run:
if verbosity < 1:
print('saving to the database')
put_to_database(data)
In this example:
- `path` declaration is extended with `help`;
- `format` declaration is extended with `choices`;
- `dry_run` declaration is not duplicated;
- `verbosity` is extended with `choices` and the default value is
overridden. (If both function signature and `@arg` define a default
value for an argument, `@arg` wins.)
.. note::
It is recommended to avoid using this decorator unless there's no way
to tune the argument's behaviour or presentation using ordinary
function signatures. Readability counts, don't repeat yourself.
"""
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)
return func
return wrapper
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(...):
...
"""
def wrapper(func):
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.26.2/argh/__init__.py 0000644 0001750 0000144 00000000757 12714714165 015707 0 ustar andy users 0000000 0000000 # 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.26.2'
argh-0.26.2/argh/constants.py 0000644 0001750 0000144 00000006554 12423265721 016161 0 ustar andy users 0000000 0000000 # 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', 'DEFAULT_ARGUMENT_TEMPLATE', 'DEST_FUNCTION',
)
#
# 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'
#
# Dest names in parser defaults
#
#: dest name for a function mapped to given endpoint (goes to Namespace obj)
DEST_FUNCTION = 'function'
#
# Other library-wide stuff
#
class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter,
argparse.RawDescriptionHelpFormatter):
def _expand_help(self, action):
"""
This method is copied verbatim from ArgumentDefaultsHelpFormatter with
a couple of lines added just before the end. Reason: we need to
`repr()` default values instead of simply inserting them as is.
This helps notice, for example, an empty string as the default value;
moreover, it prevents breaking argparse due to logical quirks inside
of its formatters.
Ideally this could be achieved by simply defining
:attr:`DEFAULT_ARGUMENT_TEMPLATE` as ``{default!r}`` but unfortunately
argparse only supports the old printf syntax.
"""
params = dict(vars(action), prog=self._prog)
for name in list(params):
if params[name] is argparse.SUPPRESS:
del params[name]
for name in list(params):
if hasattr(params[name], '__name__'):
params[name] = params[name].__name__
if params.get('choices') is not None:
choices_str = ', '.join([str(c) for c in params['choices']])
params['choices'] = choices_str
# XXX this is added in Argh vs. argparse.ArgumentDefaultsHelpFormatter
# (avoiding empty strings, otherwise Argparse would die with
# an IndexError in _format_action)
#
if 'default' in params:
if params['default'] is None:
params['default'] = '-'
else:
params['default'] = repr(params['default'])
#
# /
return self._get_help_string(action) % params
#: Default formatter to be used in implicitly instantiated ArgumentParser.
PARSER_FORMATTER = CustomFormatter
DEFAULT_ARGUMENT_TEMPLATE = '%(default)s'
"""
Default template of argument help message (see issue #64).
The template ``%(default)s`` is used by `argparse` to display the argument's
default value.
"""
argh-0.26.2/argh/compat.py 0000644 0001750 0000144 00000005422 12356005765 015426 0 ustar andy users 0000000 0000000 # 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
def getargspec_permissive(func):
"""
An `inspect.getargspec` with a relaxed sanity check to support Cython.
Motivation:
A Cython-compiled function is *not* an instance of Python's
types.FunctionType. That is the sanity check the standard Py2
library uses in `inspect.getargspec()`. So, an exception is raised
when calling `argh.dispatch_command(cythonCompiledFunc)`. However,
the CyFunctions do have perfectly usable `.func_code` and
`.func_defaults` which is all `inspect.getargspec` needs.
This function just copies `inspect.getargspec()` from the standard
library but relaxes the test to a more duck-typing one of having
both `.func_code` and `.func_defaults` attributes.
"""
if inspect.ismethod(func):
func = func.im_func
# Py2 Stdlib uses isfunction(func) which is too strict for Cython-compiled
# functions though such have perfectly usable func_code, func_defaults.
if not (hasattr(func, "func_code") and hasattr(func, "func_defaults")):
raise TypeError('{!r} missing func_code or func_defaults'.format(func))
args, varargs, varkw = inspect.getargs(func.func_code)
return inspect.ArgSpec(args, varargs, varkw, func.func_defaults)
if sys.version_info < (3,0):
getargspec = getargspec_permissive
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.26.2/argh/dispatching.py 0000644 0001750 0000144 00000030261 12424440373 016431 0 ustar andy users 0000000 0000000 # 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,
DEST_FUNCTION,
)
from argh.completion import autocomplete
from argh.assembling import add_commands, set_default_command
from argh.exceptions import DispatchingError, CommandError
from argh.utils import get_arg_spec
__all__ = ['dispatch', 'dispatch_command', 'dispatch_commands',
'PARSER_FORMATTER', 'EntryPoint']
class ArghNamespace(argparse.Namespace):
"""
A namespace object which collects the stack of functions (the
:attr:`~argh.constants.DEST_FUNCTION` arguments passed to it via
parser's defaults).
"""
def __init__(self, *args, **kw):
super(ArghNamespace, self).__init__(*args, **kw)
self._functions_stack = []
def __setattr__(self, k, v):
if k == DEST_FUNCTION:
# don't register the function under DEST_FUNCTION name.
# If `ArgumentParser.parse_known_args()` sees that we already have
# such attribute, it skips it. However, it goes from the topmost
# parser to subparsers. We need the function mapped to the
# subparser. So we fool the `ArgumentParser` and pretend that we
# didn't get a DEST_FUNCTION attribute; however, in fact we collect
# all its values in a stack. The last item in the stack would be
# the function mapped to the innermost parser — the one we need.
self._functions_stack.append(v)
else:
super(ArghNamespace, self).__setattr__(k, v)
def get_function(self):
return self._functions_stack[-1]
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,
skip_unknown_args=False):
"""
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`.
:param skip_unknown_args:
If `True`, unknown arguments do not cause an error
(`ArgumentParser.parse_known_args` is used).
:param namespace:
An `argparse.Namespace`-like object. By default an
:class:`ArghNamespace` object is used. Please note that support for
combined default and nested functions may be broken if a different
type of object is forced.
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:
autocomplete(parser)
if argv is None:
argv = sys.argv[1:]
if add_help_command:
if argv and argv[0] == 'help':
argv.pop(0)
argv.append('--help')
if skip_unknown_args:
parse_args = parser.parse_known_args
else:
parse_args = parser.parse_args
if not namespace:
namespace = ArghNamespace()
# this will raise SystemExit if parsing fails
namespace_obj = parse_args(argv, namespace=namespace)
function = _get_function_from_namespace_obj(namespace_obj)
if function:
lines = _execute_command(function, namespace_obj, errors_file,
pre_call=pre_call)
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 one 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 _get_function_from_namespace_obj(namespace_obj):
if isinstance(namespace_obj, ArghNamespace):
# our special namespace object keeps the stack of assigned functions
try:
function = namespace_obj.get_function()
except (AttributeError, IndexError):
return None
else:
# a custom (probably vanilla) namespace object keeps the last assigned
# function; this may be wrong but at least something may work
if not hasattr(namespace_obj, DEST_FUNCTION):
return None
function = getattr(namespace_obj, DEST_FUNCTION)
if not function or not hasattr(function, '__call__'):
return None
return function
def _execute_command(function, namespace_obj, errors_file, pre_call=None):
"""
Assumes that `function` is a callable. Tries different approaches
to call it (with `namespace_obj` 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`.
"""
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/neithere/argh/issues/63
pre_call(namespace_obj)
# the function is nested to catch certain exceptions (see below)
def _call():
# Actually call the function
if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
result = function(namespace_obj)
else:
# namespace -> dictionary
_flat_key = lambda key: key.replace('-', '_')
all_input = dict((_flat_key(k), v)
for k,v in vars(namespace_obj).items())
# filter the namespace variables so that only those expected
# by the actual function will pass
spec = get_arg_spec(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(namespace_obj, spec.varargs)
# **kwargs
varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
if varkw:
not_kwargs = [DEST_FUNCTION] + spec.args + [spec.varargs] + kwonly
for k in vars(namespace_obj):
if k.startswith('_') or k in not_kwargs:
continue
keywords[k] = getattr(namespace_obj, k)
result = 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(function, ATTR_WRAPPED_EXCEPTIONS, [])
try:
result = _call()
for line in result:
yield line
except tuple(wrappable_exceptions) as e:
processor = getattr(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)
class EntryPoint(object):
"""
An object to which functions can be attached and then dispatched.
When called with an argument, the argument (a function) is registered
at this entry point as a command.
When called without an argument, dispatching is triggered with all
previously registered commands.
Usage::
from argh import EntryPoint
app = EntryPoint('main', dict(description='This is a cool app'))
@app
def ls():
for i in range(10):
print i
@app
def greet():
print 'hello'
if __name__ == '__main__':
app()
"""
def __init__(self, name=None, parser_kwargs=None):
self.name = name or 'unnamed'
self.commands = []
self.parser_kwargs = parser_kwargs or {}
def __call__(self, f=None):
if f:
self._register_command(f)
return f
return self._dispatch()
def _register_command(self, f):
self.commands.append(f)
def _dispatch(self):
if not self.commands:
raise DispatchingError('no commands for entry point "{0}"'
.format(self.name))
parser = argparse.ArgumentParser(**self.parser_kwargs)
add_commands(parser, self.commands)
dispatch(parser)
argh-0.26.2/argh/helpers.py 0000644 0001750 0000144 00000004147 12423272746 015610 0 ustar andy users 0000000 0000000 # 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, ArghNamespace, dispatch
__all__ = ['ArghParser']
class ArghParser(argparse.ArgumentParser):
"""
A subclass of :class:`ArgumentParser` with support for and a couple
of convenience methods.
All 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:`~argh.assembling.set_default_command`."
return set_default_command(self, *args, **kwargs)
def add_commands(self, *args, **kwargs):
"Wrapper for :func:`~argh.assembling.add_commands`."
return add_commands(self, *args, **kwargs)
def autocomplete(self):
"Wrapper for :func:`~argh.completion.autocomplete`."
return autocomplete(self)
def dispatch(self, *args, **kwargs):
"Wrapper for :func:`~argh.dispatching.dispatch`."
return dispatch(self, *args, **kwargs)
def parse_args(self, args=None, namespace=None):
"""
Wrapper for :meth:`argparse.ArgumentParser.parse_args`. If `namespace`
is not defined, :class:`argh.dispatching.ArghNamespace` is used.
This is required for functions to be properly used as commands.
"""
namespace = namespace or ArghNamespace()
return super(ArghParser, self).parse_args(args, namespace)
argh-0.26.2/argh/io.py 0000644 0001750 0000144 00000005652 12622127535 014553 0 ustar andy users 0000000 0000000 # 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.26.2/argh/assembling.py 0000644 0001750 0000144 00000042223 12423272746 016267 0 ustar andy users 0000000 0000000 # 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
import warnings
from argh.completion import COMPLETION_ENABLED
from argh.compat import OrderedDict
from argh.constants import (
ATTR_ALIASES,
ATTR_ARGS,
ATTR_NAME,
ATTR_EXPECTS_NAMESPACE_OBJECT,
PARSER_FORMATTER,
DEFAULT_ARGUMENT_TEMPLATE,
DEST_FUNCTION,
)
from argh.utils import get_subparsers, get_arg_spec
from argh.exceptions import AssemblingError
__all__ = [
'SUPPORTS_ALIASES',
'set_default_command',
'add_commands',
'add_subcommands',
]
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 = {}
# Parser actions that accept argument 'type'
TYPE_AWARE_ACTIONS = 'store', 'append'
# 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 _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):
get_kwargs = parser._get_positional_kwargs
else:
get_kwargs = parser._get_optional_kwargs
kwargs = get_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 _require_support_for_default_command_with_subparsers():
if sys.version_info < (3,4):
raise AssemblingError(
'Argparse library bundled with this version of Python '
'does not support combining a default command with nested ones.')
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 `AssemblingError`.
.. 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:
_require_support_for_default_command_with_subparsers()
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()
if 'help' not in draft:
draft.update(help=DEFAULT_ARGUMENT_TEMPLATE)
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(**{
DEST_FUNCTION: function,
})
def add_commands(parser, functions, namespace=None, namespace_kwargs=None,
func_kwargs=None,
# deprecated args:
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.
:param func_kwargs:
a `dict` of keyword arguments to be passed to each nested ArgumentParser
instance created per command (i.e. per function). Members of this
dictionary have the highest priority, so a function's docstring is
overridden by a `help` in `func_kwargs` (if present).
:param namespace_kwargs:
a `dict` of keyword arguments to be passed to the nested ArgumentParser
instance under given `namespace`.
Deprecated params that should be moved into `namespace_kwargs`:
:param title:
passed to :meth:`argparse.ArgumentParser.add_subparsers` as `title`.
.. deprecated:: 0.26.0
Please use `namespace_kwargs` instead.
:param description:
passed to :meth:`argparse.ArgumentParser.add_subparsers` as
`description`.
.. deprecated:: 0.26.0
Please use `namespace_kwargs` instead.
:param help:
passed to :meth:`argparse.ArgumentParser.add_subparsers` as `help`.
.. deprecated:: 0.26.0
Please use `namespace_kwargs` instead.
.. 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.
.. 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 `AssemblingError`.
"""
# FIXME "namespace" is a correct name but it clashes with the "namespace"
# that represents arguments (argparse.Namespace and our ArghNamespace).
# We should rename the argument here.
if DEST_FUNCTION in parser._defaults:
_require_support_for_default_command_with_subparsers()
namespace_kwargs = namespace_kwargs or {}
# FIXME remove this by 1.0
#
if title:
warnings.warn('argument `title` is deprecated in add_commands(),'
' use `parser_kwargs` instead', DeprecationWarning)
namespace_kwargs['description'] = title
if help:
warnings.warn('argument `help` is deprecated in add_commands(),'
' use `parser_kwargs` instead', DeprecationWarning)
namespace_kwargs['help'] = help
if description:
warnings.warn('argument `description` is deprecated in add_commands(),'
' use `parser_kwargs` instead', DeprecationWarning)
namespace_kwargs['description'] = description
#
# /
subparsers_action = get_subparsers(parser, create=True)
if namespace:
# Make a nested parser and init a deeper _SubParsersAction under it.
# Create a named group of commands. It will be listed along with
# root-level commands in ``app.py --help``; in that context its `title`
# can be used as a short description on the right side of its name.
# Normally `title` is shown above the list of commands
# in ``app.py my-namespace --help``.
subsubparser_kw = {
'help': namespace_kwargs.get('title'),
}
subsubparser = subparsers_action.add_parser(namespace, **subsubparser_kw)
subparsers_action = subsubparser.add_subparsers(**namespace_kwargs)
else:
assert not namespace_kwargs, ('`parser_kwargs` only makes sense '
'with `namespace`.')
for func in functions:
cmd_name, func_parser_kwargs = _extract_command_meta_from_func(func)
# override any computed kwargs by manually supplied ones
if func_kwargs:
func_parser_kwargs.update(func_kwargs)
# create and set up the parser for this command
command_parser = subparsers_action.add_parser(cmd_name, **func_parser_kwargs)
set_default_command(command_parser, func)
def _extract_command_meta_from_func(func):
# use explicitly defined name; if none, use function name (a_b → a-b)
cmd_name = getattr(func, ATTR_NAME,
func.__name__.replace('_','-'))
func_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:
func_parser_kwargs['aliases'] = getattr(func, ATTR_ALIASES, [])
return cmd_name, func_parser_kwargs
def add_subcommands(parser, namespace, functions, **namespace_kwargs):
"""
A wrapper for :func:`add_commands`.
These examples are equivalent::
add_commands(parser, [get, put], namespace='db',
namespace_kwargs={
'title': 'database commands',
'help': 'CRUD for our silly database'
})
add_subcommands(parser, 'db', [get, put],
title='database commands',
help='CRUD for our silly database')
"""
add_commands(parser, functions, namespace=namespace,
namespace_kwargs=namespace_kwargs)
argh-0.26.2/argh/utils.py 0000644 0001750 0000144 00000003214 12356012461 015267 0 ustar andy users 0000000 0000000 # 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.26.2/argh/interaction.py 0000644 0001750 0000144 00000004543 12356012450 016452 0 ustar andy users 0000000 0000000 # 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.26.2/README.rst 0000644 0001750 0000144 00000016206 12607017646 014341 0 ustar andy users 0000000 0000000 Argh: The Natural CLI
=====================
.. image:: https://img.shields.io/coveralls/neithere/argh.svg
:target: https://coveralls.io/r/neithere/argh
.. image:: https://img.shields.io/travis/neithere/argh.svg
:target: https://travis-ci.org/neithere/argh
.. image:: https://img.shields.io/pypi/format/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/status/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/v/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/pyversions/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://img.shields.io/pypi/dd/argh.svg
:target: https://pypi.python.org/pypi/argh
.. image:: https://readthedocs.org/projects/argh/badge/?version=stable
:target: http://argh.readthedocs.org/en/stable/
.. image:: https://readthedocs.org/projects/argh/badge/?version=latest
:target: http://argh.readthedocs.org/en/latest/
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.
:NIH free:
`Argh` supports *completion*, *progress bars* and everything else by being
friendly to excellent 3rd-party libraries. No need to reinvent the wheel.
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)
Run it:
.. code-block:: bash
$ ./app.py
Hello world
A potentially modular application with multiple commands:
.. code-block:: python
import argh
# declaring:
def echo(text):
"Returns given word as is."
return text
def greet(name, greeting='Hello'):
"Greets the user with given name. The greeting is customizable."
return greeting + ', ' + name
# assembling:
parser = argh.ArghParser()
parser.add_commands([echo, greet])
# dispatching:
if __name__ == '__main__':
parser.dispatch()
Of course it works:
.. code-block:: bash
$ ./app.py greet Andy
Hello, Andy
$ ./app.py greet Andy -g Arrrgh
Arrrgh, Andy
Here's the auto-generated help for this application (note how the docstrings
are reused)::
$ ./app.py help
usage: app.py {echo,greet} ...
positional arguments:
echo Returns given word as is.
greet Greets the user with given name. The greeting is customizable.
...and for a specific command (an ordinary function signature is converted
to CLI arguments)::
$ ./app.py help greet
usage: app.py greet [-g GREETING] name
Greets the user with given name. The greeting is customizable.
positional arguments:
name
optional arguments:
-g GREETING, --greeting GREETING 'Hello'
(The help messages have been simplified a bit for brevity.)
`Argh` easily maps plain Python functions to CLI. Sometimes this is not
enough; in these cases 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)
.. _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
Author
------
Developed by Andrey Mikhaylenko since 2010.
See file `AUTHORS` for a complete list of contributors to this library.
Support
-------
The fastest way to improve this project is to submit tested and documented
patches or detailed bug reports.
Otherwise you can "flattr" me: |FlattrLink|_
.. _FlattrLink: https://flattr.com/submit/auto?user_id=neithere&url=http%3A%2F%2Fpypi.python.org%2Fpypi%2Fargh
.. |FlattrLink| image:: https://api.flattr.com/button/flattr-badge-large.png
:alt: Flattr the Argh project
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.26.2/test/ 0000755 0001750 0000144 00000000000 12714716035 013621 5 ustar andy users 0000000 0000000 argh-0.26.2/test/test_dispatching.py 0000644 0001750 0000144 00000004122 12607031544 017522 0 ustar andy users 0000000 0000000 # coding: utf-8
"""
Dispatching tests
~~~~~~~~~~~~~~~~~
"""
import argh
from mock import Mock, patch
import pytest
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():
def cmd(foo=1):
return foo
assert run_func(cmd, '') == '1\n'
assert run_func(cmd, '--foo 2') == '2\n'
@patch('argh.dispatching.dispatch')
@patch('argh.dispatching.add_commands')
@patch('argparse.ArgumentParser')
def test_entrypoint(ap_cls_mock, add_commands_mock, dispatch_mock):
entrypoint = argh.EntryPoint('my cool app')
# no commands
with pytest.raises(argh.exceptions.DispatchingError) as excinfo:
entrypoint()
assert excinfo.exconly().endswith(
'DispatchingError: no commands for entry point "my cool app"')
mocked_parser = Mock()
ap_cls_mock.return_value = mocked_parser
# a single command
@entrypoint
def greet():
return 'hello'
entrypoint()
assert add_commands_mock.called
add_commands_mock.assert_called_with(mocked_parser, [greet])
assert dispatch_mock.called
dispatch_mock.assert_called_with(mocked_parser)
# multiple commands
add_commands_mock.reset_mock()
dispatch_mock.reset_mock()
@entrypoint
def hit():
return 'knight with a chicken'
entrypoint()
assert add_commands_mock.called
add_commands_mock.assert_called_with(mocked_parser, [greet, hit])
assert dispatch_mock.called
dispatch_mock.assert_called_with(mocked_parser)
argh-0.26.2/test/test_compat.py 0000644 0001750 0000144 00000000466 12262445451 016522 0 ustar andy users 0000000 0000000 from 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.26.2/test/__init__.py 0000644 0001750 0000144 00000000000 12262445451 015717 0 ustar andy users 0000000 0000000 argh-0.26.2/test/base.py 0000644 0001750 0000144 00000004274 12607026335 015112 0 ustar andy users 0000000 0000000 # coding: utf-8
"""
Common stuff for tests
~~~~~~~~~~~~~~~~~~~~~~
"""
import os
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')
def get_usage_string(definitions='{cmd} ...'):
prog = os.path.basename(sys.argv[0])
return 'usage: ' + prog + ' [-h] ' + definitions + '\n\n'
argh-0.26.2/test/test_assembling.py 0000644 0001750 0000144 00000017300 12423260742 017353 0 ustar andy users 0000000 0000000 # 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',
help=argh.constants.DEFAULT_ARGUMENT_TEMPLATE)
]
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'
@pytest.mark.skipif(sys.version_info >= (3,4), reason='supported since Python 3.4')
def test_add_subparsers_when_default_command_exists__unsupported():
def one(): return 1
def two(): return 2
p = argh.ArghParser()
p.set_default_command(one)
with pytest.raises(argh.AssemblingError) as excinfo:
p.add_commands([two])
assert excinfo.exconly().endswith(
'Argparse library bundled with this version of Python '
'does not support combining a default command with nested ones.')
@pytest.mark.skipif(sys.version_info >= (3,4), reason='supported since Python 3.4')
def test_set_default_command_when_subparsers_exist__unsupported():
def one(): return 1
def two(): return 2
p = argh.ArghParser()
p.add_commands([one])
with pytest.raises(argh.AssemblingError) as excinfo:
p.set_default_command(two)
assert excinfo.exconly().endswith(
'Argparse library bundled with this version of Python '
'does not support combining a default command with nested ones.')
@pytest.mark.skipif(sys.version_info < (3,4), reason='supported since Python 3.4')
def test_add_subparsers_when_default_command_exists__supported():
def one(): return 1
def two(): return 2
p = argh.ArghParser()
p.set_default_command(one)
p.add_commands([two])
ns_one = p.parse_args([])
ns_two = p.parse_args(['two'])
assert ns_one.get_function() == one
assert ns_two.get_function() == two
@pytest.mark.skipif(sys.version_info < (3,4), reason='supported since Python 3.4')
def test_set_default_command_when_subparsers_exist__supported():
def one(): return 1
def two(): return 2
p = argh.ArghParser()
p.add_commands([one])
p.set_default_command(two)
ns_two = p.parse_args([])
ns_one = p.parse_args(['one'])
assert ns_one.get_function() == one
assert ns_two.get_function() == two
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='*',
help=argh.constants.DEFAULT_ARGUMENT_TEMPLATE),
]
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', help=argh.constants.DEFAULT_ARGUMENT_TEMPLATE),
mock.call('foo', help=argh.constants.DEFAULT_ARGUMENT_TEMPLATE),
mock.call('--bar', help=argh.constants.DEFAULT_ARGUMENT_TEMPLATE),
]
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(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(cmd)
assert p.add_argument.mock_calls == [
mock.call('-f', '--foo', type=str, default='abcd',
help=argh.constants.DEFAULT_ARGUMENT_TEMPLATE),
mock.call('-b', '--bar', required=True,
help=argh.constants.DEFAULT_ARGUMENT_TEMPLATE),
mock.call('args', nargs='*',
help=argh.constants.DEFAULT_ARGUMENT_TEMPLATE)
]
@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.26.2/test/test_decorators.py 0000644 0001750 0000144 00000002466 12423015563 017402 0 ustar andy users 0000000 0000000 # 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_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_expects_obj():
@argh.expects_obj
def func(args):
pass
attr = getattr(func, argh.constants.ATTR_EXPECTS_NAMESPACE_OBJECT)
assert attr == True
argh-0.26.2/test/test_integration.py 0000644 0001750 0000144 00000051256 12607035035 017561 0 ustar andy users 0000000 0000000 # coding: utf-8
"""
Integration Tests
~~~~~~~~~~~~~~~~~
"""
import sys
import re
import argparse
import iocapture
import mock
import pytest
import argh
from argh.exceptions import AssemblingError
from .base import DebugArghParser, get_usage_string, 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():
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 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='')
@pytest.mark.xfail
def test_simple_function_multiple():
raise NotImplementedError
@pytest.mark.xfail
def test_simple_function_nested():
raise NotImplementedError
@pytest.mark.xfail
def test_class_method_as_command():
raise NotImplementedError
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)
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():
def echo(text):
return 'you said {0}'.format(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():
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."
def echo(text):
return 'you said {0}'.format(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."
def parrot(dead=False):
return 'this parrot is no more' if 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.
"""
def cmd(foo, bar):
return foo, 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
def test_default_arg_values_in_help():
"Argument defaults should appear in the help message implicitly"
@argh.arg('name', default='Basil')
@argh.arg('--task', default='hang the Moose')
@argh.arg('--note', help='why is it a remarkable animal?')
def remind(name, task=None, reason='there are creatures living in it',
note='it can speak English'):
return "Oh what is it now, can't you leave me in peace..."
p = DebugArghParser()
p.set_default_command(remind)
assert 'Basil' in p.format_help()
assert 'Moose' in p.format_help()
assert 'creatures' in p.format_help()
# explicit help message is not obscured by the implicit one...
assert 'remarkable animal' in p.format_help()
# ...but is still present
assert 'it can speak' in p.format_help()
def test_default_arg_values_in_help__regression():
"Empty string as default value → empty help string → broken argparse"
def foo(bar=''):
return bar
p = DebugArghParser()
p.set_default_command(foo)
# doesn't break
p.format_help()
# now check details
assert "-b BAR, --bar BAR ''" in p.format_help()
# note the empty str repr ^^^
def test_help_formatting_is_preserved():
"Formatting of docstrings should not be messed up in help messages"
def func():
"""
Sample function.
Parameters:
foo: float
An example argument.
bar: bool
Another argument.
"""
return 'hello'
p = DebugArghParser()
p.set_default_command(func)
assert func.__doc__ in p.format_help()
def test_prog():
"Program name propagates from sys.argv[0]"
def cmd(foo=1):
return foo
p = DebugArghParser()
p.add_commands([cmd])
usage = get_usage_string()
with iocapture.capture() as captured:
assert run(p, '-h', exit=True) == None
assert captured.stdout.startswith(usage)
def test_unknown_args():
def cmd(foo=1):
return foo
p = DebugArghParser()
p.set_default_command(cmd)
usage = get_usage_string('[-f FOO]')
assert run(p, '--foo 1') == R(out='1\n', err='')
assert run(p, '--bar 1', exit=True) == 'unrecognized arguments: --bar 1'
assert run(p, '--bar 1', exit=False,
kwargs={'skip_unknown_args': True}) == R(out=usage, err='')
argh-0.26.2/test/test_regressions.py 0000644 0001750 0000144 00000007314 12423016113 017565 0 ustar andy users 0000000 0000000 # 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_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)
def test_regression_issue76():
"""
Issue #76: optional arguments defaulting to the empty string break --help.
This is also tested in integration tests but in a different way.
"""
def cmd(foo=''):
pass
p = DebugArghParser()
p.set_default_command(cmd)
run(p, '--help', exit=True)
argh-0.26.2/test/test_interaction.py 0000644 0001750 0000144 00000003315 12262445451 017552 0 ustar andy users 0000000 0000000 # 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.26.2/setup.cfg 0000644 0001750 0000144 00000000130 12714716035 014455 0 ustar andy users 0000000 0000000 [bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
argh-0.26.2/setup.py 0000755 0001750 0000144 00000007213 12714701137 014357 0 ustar andy users 0000000 0000000 #!/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', 'mock', 'iocapture'],
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.4',
'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy',
'Topic :: Software Development :: User Interfaces',
'Topic :: Software Development :: Libraries :: Python Modules',
],
)