Skip to content

Commit 839d957

Browse files
committed
Added support for different argument modes and tests to validate.
1 parent 4a36b8c commit 839d957

File tree

6 files changed

+96
-19
lines changed

6 files changed

+96
-19
lines changed

cmd2/pyscript_bridge.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# coding=utf-8
12
"""
23
Bridges calls made inside of pyscript with the Cmd2 host app while maintaining a reasonable
34
degree of isolation between the two
@@ -8,9 +9,13 @@
89

910
import argparse
1011
from typing import List, Tuple
12+
from .argparse_completer import _RangeAction
1113

1214

1315
class ArgparseFunctor:
16+
"""
17+
Encapsulates translating python object traversal
18+
"""
1419
def __init__(self, cmd2_app, item, parser):
1520
self._cmd2_app = cmd2_app
1621
self._item = item
@@ -59,10 +64,46 @@ def __call__(self, *args, **kwargs):
5964
# This is a positional argument, search the positional arguments passed in.
6065
if not isinstance(action, argparse._SubParsersAction):
6166
if action.dest in kwargs:
67+
# if this positional argument happens to be passed in as a keyword argument
68+
# go ahead and consume the matching keyword argument
6269
self._args[action.dest] = kwargs[action.dest]
6370
elif next_pos_index < len(args):
64-
self._args[action.dest] = args[next_pos_index]
65-
next_pos_index += 1
71+
# Make sure we actually have positional arguments to consume
72+
pos_remain = len(args) - next_pos_index
73+
74+
# Check if this argument consumes a range of values
75+
if isinstance(action, _RangeAction) and action.nargs_min is not None \
76+
and action.nargs_max is not None:
77+
# this is a cmd2 ranged action.
78+
79+
if pos_remain >= action.nargs_min:
80+
# Do we meet the minimum count?
81+
if pos_remain > action.nargs_max:
82+
# Do we exceed the maximum count?
83+
self._args[action.dest] = args[next_pos_index:next_pos_index + action.nargs_max]
84+
next_pos_index += action.nargs_max
85+
else:
86+
self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
87+
next_pos_index += pos_remain
88+
else:
89+
raise ValueError('Expected at least {} values for {}'.format(action.nargs_min,
90+
action.dest))
91+
elif action.nargs is not None:
92+
if action.nargs == '+':
93+
if pos_remain > 0:
94+
self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
95+
next_pos_index += pos_remain
96+
else:
97+
raise ValueError('Expected at least 1 value for {}'.format(action.dest))
98+
elif action.nargs == '*':
99+
self._args[action.dest] = args[next_pos_index:next_pos_index + pos_remain]
100+
next_pos_index += pos_remain
101+
elif action.nargs == '?':
102+
self._args[action.dest] = args[next_pos_index]
103+
next_pos_index += 1
104+
else:
105+
self._args[action.dest] = args[next_pos_index]
106+
next_pos_index += 1
66107
else:
67108
has_subcommand = True
68109

@@ -85,6 +126,22 @@ def _run(self):
85126
cmd_str = ['']
86127

87128
def process_flag(action, value):
129+
if isinstance(action, argparse._CountAction):
130+
if isinstance(value, int):
131+
for c in range(value):
132+
cmd_str[0] += '{} '.format(action.option_strings[0])
133+
return
134+
else:
135+
raise TypeError('Expected int for ' + action.dest)
136+
if isinstance(action, argparse._StoreConstAction) or isinstance(action, argparse._AppendConstAction):
137+
if value:
138+
# Nothing else to append to the command string, just the flag is enough.
139+
cmd_str[0] += '{} '.format(action.option_strings[0])
140+
return
141+
else:
142+
# value is not True so we default to false, which means don't include the flag
143+
return
144+
88145
# was the argument a flag?
89146
if action.option_strings:
90147
cmd_str[0] += '{} '.format(action.option_strings[0])
@@ -114,7 +171,6 @@ def traverse_parser(parser):
114171
process_flag(action, values)
115172
else:
116173
process_flag(action, self._args[action.dest])
117-
# TODO: StoreTrue/StoreFalse
118174
else:
119175
process_flag(action, self._args[action.dest])
120176

tests/pyscript/bar1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
app.bar('11', '22')

tests/pyscript/foo1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
app.foo('aaa', 'bbb', counter=3, trueval=True, constval=True)

tests/pyscript/foo2.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
app.foo('11', '22', '33', '44', counter=3, trueval=True, constval=True)

tests/pyscript/foo3.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
app.foo('11', '22', '33', '44', '55', '66', counter=3, trueval=False, constval=False)

tests/test_pyscript.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ def do_media(self, args):
7171
# No subcommand was provided, so call help
7272
self.do_help('media')
7373

74+
foo_parser = argparse_completer.ACArgumentParser(prog='foo')
75+
foo_parser.add_argument('-c', dest='counter', action='count')
76+
foo_parser.add_argument('-t', dest='trueval', action='store_true')
77+
foo_parser.add_argument('-n', dest='constval', action='store_const', const=42)
78+
foo_parser.add_argument('variable', nargs=(2, 3))
79+
foo_parser.add_argument('optional', nargs='?')
80+
foo_parser.add_argument('zeroormore', nargs='*')
81+
82+
@with_argparser(foo_parser)
83+
def do_foo(self, args):
84+
print('foo ' + str(args.__dict__))
85+
86+
bar_parser = argparse_completer.ACArgumentParser(prog='bar')
87+
bar_parser.add_argument('first')
88+
bar_parser.add_argument('oneormore', nargs='+')
89+
bar_parser.add_argument('-a', dest='aaa')
90+
91+
@with_argparser(bar_parser)
92+
def do_bar(self, args):
93+
print('bar ' + str(args.__dict__))
94+
7495

7596
@pytest.fixture
7697
def ps_app():
@@ -108,6 +129,10 @@ def test_pyscript_help(ps_app, capsys, request, command, pyscript_file):
108129
'media_movies_add1.py'),
109130
('media movies add "My Movie" PG-13 --director "George Lucas" "J. J. Abrams" "Mark Hamill"',
110131
'media_movies_add2.py'),
132+
('foo aaa bbb -ccc -t -n', 'foo1.py'),
133+
('foo 11 22 33 44 -ccc -t -n', 'foo2.py'),
134+
('foo 11 22 33 44 55 66 -ccc', 'foo3.py'),
135+
('bar 11 22', 'bar1.py')
111136
])
112137
def test_pyscript_out(ps_app, capsys, request, command, pyscript_file):
113138
test_dir = os.path.dirname(request.module.__file__)
@@ -122,26 +147,18 @@ def test_pyscript_out(ps_app, capsys, request, command, pyscript_file):
122147
assert out == expected
123148

124149

125-
@pytest.mark.parametrize('command', [
126-
'app.noncommand',
127-
'app.media.noncommand',
150+
@pytest.mark.parametrize('command, error', [
151+
('app.noncommand', 'AttributeError'),
152+
('app.media.noncommand', 'AttributeError'),
153+
('app.media.movies.list(artist="Invalid Keyword")', 'TypeError'),
154+
('app.foo(counter="a")', 'TypeError'),
155+
('app.foo("aaa")', 'ValueError'),
128156
])
129-
def test_pyscript_unknown_command(ps_app, capsys, command):
157+
def test_pyscript_errors(ps_app, capsys, command, error):
130158
run_cmd(ps_app, 'py {}'.format(command))
131159
_, err = capsys.readouterr()
132160

133161
assert len(err) > 0
134162
assert 'Traceback' in err
135-
assert 'AttributeError' in err
136-
163+
assert error in err
137164

138-
@pytest.mark.parametrize('command', [
139-
'app.media.movies.list(artist="Invalid Keyword")',
140-
])
141-
def test_pyscript_unknown_kw(ps_app, capsys, command):
142-
run_cmd(ps_app, 'py {}'.format(command))
143-
_, err = capsys.readouterr()
144-
145-
assert len(err) > 0
146-
assert 'Traceback' in err
147-
assert 'TypeError' in err

0 commit comments

Comments
 (0)