Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions argh/assembling.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
from argh.compat import OrderedDict
from argh.constants import (ATTR_ALIASES, ATTR_ARGS, ATTR_NAME,
ATTR_EXPECTS_NAMESPACE_OBJECT,
PARSER_FORMATTER, DEFAULT_ARGUMENT_TEMPLATE)
PARSER_FORMATTER, DEFAULT_ARGUMENT_TEMPLATE,
ATTR_TOGGLEABLES)
from argh.utils import get_subparsers, get_arg_spec
from argh.exceptions import AssemblingError

Expand Down Expand Up @@ -201,6 +202,8 @@ def set_default_command(parser, function):
declared_args = getattr(function, ATTR_ARGS, [])
inferred_args = list(_get_args_from_signature(function))

toggleables = getattr(function, ATTR_TOGGLEABLES, [])

if inferred_args and declared_args:
# We've got a mixture of declared and inferred arguments

Expand Down Expand Up @@ -271,6 +274,22 @@ def set_default_command(parser, function):
# pack the modified data back into a list
inferred_args = dests.values()

opt_string_togmap = {}

for toggleable, inv_prefix in [(t, i) for t,i in toggleables]:
if toggleable.find('_') >= 0:
raise AssemblingError("Toggleable destinations cannot contain underscores")

#for each toggleable, verify there exists a matching destination
matched_dest = False
for arg in inferred_args:
for opt_str in arg['option_strings']:
if opt_str == toggleable:
matched_dest = True
opt_string_togmap[opt_str] = (toggleable[2:], inv_prefix)
if not matched_dest:
raise AssemblingError("Unrecognized destination for toggleable: {}".format(toggleable))

command_args = inferred_args or declared_args

# add types, actions, etc. (e.g. default=3 implies type=int)
Expand All @@ -280,12 +299,33 @@ def set_default_command(parser, function):
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 dest_or_opt_strings[-1] in opt_string_togmap:
# if we're working with a toggleable list of opt_strings, make mutually exclusive
# and set to opposite defaults & storing actions
toggleable, inv_prefix = opt_string_togmap[dest_or_opt_strings[-1]]
group = parser.add_mutually_exclusive_group()

draft['action'] = 'store_true'
draft['dest'] = toggleable.replace('-', '_')

# XXX unsure about desired behavior in autocompletion of toggleables case
action = group.add_argument(*dest_or_opt_strings, **draft)

not_dest_or_opt_strings = tuple(map(lambda x : '--%s-%s' % (inv_prefix, x.lstrip('-')),
dest_or_opt_strings))

draft['action'] = 'store_false'

group.add_argument(*not_dest_or_opt_strings, **draft)
else:
action = parser.add_argument(*dest_or_opt_strings, **draft)

if COMPLETION_ENABLED and completer:
action.completer = completer
except Exception as e:
Expand Down
5 changes: 4 additions & 1 deletion argh/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
__all__ = (
'ATTR_NAME', 'ATTR_ALIASES', 'ATTR_ARGS', 'ATTR_WRAPPED_EXCEPTIONS',
'ATTR_WRAPPED_EXCEPTIONS_PROCESSOR', 'ATTR_EXPECTS_NAMESPACE_OBJECT',
'PARSER_FORMATTER', 'DEFAULT_ARGUMENT_TEMPLATE'
'PARSER_FORMATTER', 'DEFAULT_ARGUMENT_TEMPLATE', 'ATTR_TOGGLEABLES'
)


Expand All @@ -39,6 +39,9 @@
# forcing argparse.Namespace object instead of signature introspection
ATTR_EXPECTS_NAMESPACE_OBJECT = 'argh_expects_namespace_object'

#toggleable
ATTR_TOGGLEABLES = 'argh_toggleables'

#
# Other library-wide stuff
#
Expand Down
48 changes: 46 additions & 2 deletions argh/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
from argh.constants import (ATTR_ALIASES, ATTR_ARGS, ATTR_NAME,
ATTR_WRAPPED_EXCEPTIONS,
ATTR_WRAPPED_EXCEPTIONS_PROCESSOR,
ATTR_EXPECTS_NAMESPACE_OBJECT)
ATTR_EXPECTS_NAMESPACE_OBJECT,
ATTR_TOGGLEABLES)


__all__ = ['aliases', 'named', 'arg', 'wrap_errors', 'expects_obj']
__all__ = ['aliases', 'named', 'arg', 'wrap_errors', 'expects_obj', 'set_toggleable', 'set_all_toggleable']

from argh.utils import get_arg_spec

def named(new_name):
"""
Expand Down Expand Up @@ -173,3 +175,45 @@ def foo(bar, quux=123):
"""
setattr(func, ATTR_EXPECTS_NAMESPACE_OBJECT, True)
return func

def set_toggleable(name, inv_prefix = 'no'):
"""
Marks given name as toggleable, creating an additional,
mutually exclusive parser argument to toggle a boolean.

:param name:
Name of boolean argument for which to create a toggleable argument
:param inv_prefix:
Prefix of inversion argument

Usage::

@arg('--do-foo', '--do-foo-alias')
@set_toggleable('--do-foo')
@set_toggleable('--do-foo-2', inv_prefix = 'invert')
def foo(x, do_foo = True, do_foo_2 = False):
print x, do_foo, do_foo_2
"""

def wrapper(func):
toggleables = getattr(func, ATTR_TOGGLEABLES, [])
toggleables.append((name, inv_prefix))
setattr(func, ATTR_TOGGLEABLES, toggleables)
return func
return wrapper

def set_all_toggleable(inv_prefix = 'no'):

def wrapper(func):
spec = get_arg_spec(func)

toggleables = getattr(func, ATTR_TOGGLEABLES, [])

for (dest, default) in zip(spec.args[-len(spec.defaults):], spec.defaults):
if isinstance(default, bool):
cmd_dest = dest.replace('_', '-')
toggleables.append(('--' + cmd_dest, inv_prefix))

setattr(func, ATTR_TOGGLEABLES, toggleables)
return func
return wrapper
2 changes: 1 addition & 1 deletion argh/dispatching.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def dispatch(parser, argv=None, add_help_command=True,

# this will raise SystemExit if parsing fails
args = parse_args(argv, namespace=namespace)

if hasattr(args, 'function'):
if pre_call: # XXX undocumented because I'm unsure if it's OK
# Actually used in real projects:
Expand Down
85 changes: 83 additions & 2 deletions test/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

from .base import DebugArghParser, run, CmdResult as R


@pytest.mark.xfail(reason='TODO')
def test_guessing_integration():
"guessing is used in dispatching"
Expand Down Expand Up @@ -760,8 +759,89 @@ def func():
assert func.__doc__ in p.format_help()


def test_unknown_args():
def test_simple_toggle():
@argh.set_toggleable('--toggle')
def cmd(toggle = False):
return "Toggle is {0}".format(toggle)

p = DebugArghParser()
p.set_default_command(cmd)

assert run(p, '') == R(out='Toggle is False\n', err='')
assert run(p, '--toggle') == R(out='Toggle is True\n', err='')
assert run(p, '--no-toggle') == R(out='Toggle is False\n', err='')
assert run(p, '--not-toggle', exit = True) == \
'unrecognized arguments: --not-toggle'
assert run(p, '--toggle --no-toggle', exit = True) == \
'argument --no-t/--no-toggle: not allowed with argument -t/--toggle'

def test_multiarg_toggle():
@argh.set_toggleable('--toggle1')
@argh.set_toggleable('--toggle2', inv_prefix = 'dont')
def cmd(toggle1 = False,
toggle2 = True,
toggle3 = False,
foo = 3):
return "toggle1: {0}, toggle2: {1}, toggle3: {2}, foo: {3}".format(\
toggle1, toggle2, toggle3, foo)

p = DebugArghParser()
p.set_default_command(cmd)

assert run(p, '') == R(out="toggle1: False, toggle2: True, toggle3: False, foo: 3\n",
err='')
assert run(p, '--dont-toggle2') == \
R(out="toggle1: False, toggle2: False, toggle3: False, foo: 3\n",
err='')
assert run(p, '--foo 10 --toggle1 --dont-toggle2') == \
R(out="toggle1: True, toggle2: False, toggle3: False, foo: 10\n",
err='')


def test_multitoggle():

@argh.set_all_toggleable(inv_prefix = 'temper')
@argh.arg('usa_nuke_count', type = int)
@argh.arg('russia_nuke_count', type = int)
def faceoff(usa_nuke_count,
russia_nuke_count,
scenario = 'cold_war',
usa_aggression = True,
russia_aggression = False):


if usa_aggression and russia_aggression and usa_nuke_count >= 1 and russia_nuke_count >= 1:
result = 'M.A.D.'
elif usa_aggression and usa_nuke_count >= 1:
result = 'Russia is destroyed :('
elif russia_aggression and russia_nuke_count >= 1:
result = 'USA is destroyed :('
elif usa_nuke_count + russia_nuke_count >= 1:
result = 'isolationism'
elif usa_aggression and russia_aggression:
result = 'impotence'
else:
result = 'peace'

return result

p = DebugArghParser()
p.set_default_command(faceoff)

assert run(p, '1 1') == \
R(out='Russia is destroyed :(\n',
err='')
assert run(p, '0 0 --temper-usa-aggression') == R(out='peace\n', err='')
assert run(p, '0 1 --temper-usa-aggression --russia-aggression') == \
R(out='USA is destroyed :(\n', err='')
assert run(p, '0 0 --usa-aggression --russia-aggression') == \
R(out='impotence\n', err='')
assert run(p, '100 100 --usa-aggression --russia-aggression') == \
R(out='M.A.D.\n', err='')
assert run(p, '0 0 --temper-usa-aggression --temper-russia-aggression') == \
R(out='peace\n', err='')

def test_unknown_args():
def cmd(foo=1):
return foo

Expand All @@ -770,6 +850,7 @@ def cmd(foo=1):

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: py.test [-h] [-f FOO]\n\n', err='')