Skip to content

Commit cd0a2c8

Browse files
committed
1) Read tool name from the first line of the usage, 2) Replace <VERSION> and <--ESCAPES--> in usage automatically with given version and available escapes, 3) No need to have AP.check_args or get_escapes in public API anymore, 4) Enhanced doc
1 parent 6acbb8b commit cd0a2c8

File tree

2 files changed

+107
-42
lines changed

2 files changed

+107
-42
lines changed

src/robot/utils/argumentparser.py

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from misc import seq2str, plural_or_not
2626
from robottypes import is_list, is_boolean
27+
from text import wrap
2728

2829

2930
ESCAPES = { 'space' : ' ', 'apos' : "'", 'quot' : '"', 'lt' : '<',
@@ -35,12 +36,6 @@
3536
'bslash' : '\\' }
3637

3738

38-
def get_escapes():
39-
names = ESCAPES.keys()
40-
names.sort()
41-
return [ '%s (%s)' % (name, ESCAPES[name]) for name in names ]
42-
43-
4439
class ArgumentParser:
4540

4641
_short_opt_chars = '-?a-zA-Z'
@@ -64,12 +59,18 @@ class ArgumentParser:
6459
\s*$ #
6560
''', re.VERBOSE | re.IGNORECASE)
6661

67-
def __init__(self, usage, version='No version information available'):
68-
"""Initialization is done using a usage doc explaining options.
69-
70-
See for example 'runner.py' and 'rebot.py' for usage examples.
62+
def __init__(self, usage, version=None):
63+
"""Available options and tool name are read from the usage.
64+
65+
Tool name is got from the first row of the usage. It is either the
66+
whole row or anything before first ' -- '.
67+
68+
See for example 'runner.py' and 'rebot.py' for examples.
7169
"""
70+
if not usage:
71+
raise FrameworkError('Usage cannot be empty')
7272
self._usage = usage
73+
self._name = usage.splitlines()[0].split(' -- ')[0].strip()
7374
self._version = version
7475
self._short_opts = ''
7576
self._long_opts = []
@@ -91,7 +92,8 @@ def parse_args(self, args_list, unescape=None, argfile=None, pythonpath=None,
9192
are given multiple times the last value is used) or None if the option
9293
is not used at all. Value for options that can be given multiple times
9394
(denoted with '*' in the usage) is a list which contains all the given
94-
values and is empty if options are not used.
95+
values and is empty if options are not used. Options not taken
96+
arguments have value False when they are not set and True otherwise.
9597
9698
Positional arguments are returned as a list in the order they are given.
9799
@@ -110,24 +112,37 @@ def parse_args(self, args_list, unescape=None, argfile=None, pythonpath=None,
110112
be added into 'sys.path'. Value can be either a string containing the
111113
name of the long option used for this purpose or a list containing
112114
all such long options (i.e. the latter format allows aliases).
115+
116+
'help' and 'version' make it possible to automatically generate help
117+
and version messages. Version is generated based on the tool name
118+
and version -- see __init__ for information how to set them. Help
119+
contains the whole usage given to __init__. Possible <VERSION> text
120+
in the usage is replaced with the given version. Possible <--ESCAPES-->
121+
is replaced with available escapes so that they are wrapped to multiple
122+
lines but take the same amount of horizontal space as <---ESCAPES--->.
123+
The numer of hyphens can be used to contrl the horizontal space. Both
124+
help and version are wrapped to Information exception.
113125
114126
If 'check_args' is True, this method will automatically check that
115-
correct number of arguments (as parsed from the usage line) is given.
116-
If wrong number of arguments is given DataError is risen in that case.
127+
correct number of arguments, as parsed from the usage line, are given.
128+
If the last argument in the usage line ends with the character 's',
129+
the maximum number of arguments is infinite.
130+
131+
Possible errors in processing arguments are reported using DataError.
117132
"""
118133
if argfile:
119134
args_list = self._add_args_from_file(args_list, argfile)
120135
opts, args = self._parse_args(args_list)
121136
if unescape:
122137
opts, args = self._unescape_opts_and_args(opts, args, unescape)
123138
if help and opts[help]:
124-
raise Information(self._usage)
139+
self._raise_help()
125140
if version and opts[version]:
126-
raise Information(self._version)
141+
self._raise_version()
127142
if pythonpath:
128143
sys.path = self._get_pythonpath(opts[pythonpath]) + sys.path
129144
if check_args:
130-
self.check_args(args)
145+
self._check_args(args)
131146
return opts, args
132147

133148
def _parse_args(self, args):
@@ -138,7 +153,7 @@ def _parse_args(self, args):
138153
raise DataError(err)
139154
return self._process_opts(opts), self._glob_args(args)
140155

141-
def check_args(self, args):
156+
def _check_args(self, args):
142157
if len(args) == len(self._expected_args):
143158
return
144159
elif len(args) < len(self._expected_args):
@@ -207,8 +222,8 @@ def _get_escapes(self, escape_strings):
207222
try:
208223
escapes[value] = ESCAPES[name.lower()]
209224
except KeyError:
210-
av = seq2str(get_escapes(), quote='', lastsep=', ')
211-
raise DataError("Invalid escape '%s'. Available: %s" % (name, av))
225+
raise DataError("Invalid escape '%s'. Available: %s"
226+
% (name, self._get_available_escapes()))
212227
return escapes
213228

214229
def _unescape(self, value, escapes):
@@ -353,5 +368,25 @@ def _split_pythonpath(self, paths):
353368
ret.append(drive)
354369
return ret
355370

371+
def _get_available_escapes(self):
372+
names = ESCAPES.keys()
373+
names.sort()
374+
return ', '.join([ '%s (%s)' % (n, ESCAPES[n]) for n in names ])
375+
376+
def _raise_help(self):
377+
msg = self._usage
378+
if self._version:
379+
msg = msg.replace('<VERSION>', self._version)
380+
def replace_escapes(res):
381+
escapes = 'Available escapes:\n' + self._get_available_escapes()
382+
return wrap(escapes, len(res.group(2)), len(res.group(1)))
383+
msg = re.sub('( *)(<-+ESCAPES-+>)', replace_escapes, msg)
384+
raise Information(msg)
385+
386+
def _raise_version(self):
387+
if not self._version:
388+
raise FrameworkError('Version not set')
389+
raise Information('%s %s' % (self._name, self._version))
390+
356391
def _raise_option_multiple_times_in_usage(self, opt):
357392
raise FrameworkError("Option '%s' multiple times in usage" % opt)

utest/utils/test_argumentparser.py

Lines changed: 53 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33

44
from robot.utils.argumentparser import ArgumentParser
55
from robot.utils.asserts import *
6-
from robot.errors import *
6+
from robot.errors import Information, DataError, FrameworkError
77

88

9-
USAGE = """
10-
usage: robot.py [options] datafile
9+
USAGE = """Example Tool -- Stuff before hyphens is considered name
1110
12-
options:
11+
Usage: robot.py [options] datafile
12+
13+
Version: <VERSION>
14+
15+
Options:
1316
-d --reportdir dir Explanation
1417
-r --reportfile file This explanation continues ............... 78
1518
........... to multiple lines.
@@ -31,12 +34,13 @@
3134
* denotes options that can be set multiple times
3235
"""
3336

34-
USAGE2 = """
37+
USAGE2 = """Just Name Here
3538
usage: robot.py [options] arg1 arg2
3639
3740
options:
3841
-v --variable name=value
39-
-x --var-able name=v1,v2 Explanation
42+
-x --var-able name=v1,v2 Explanation
43+
--42
4044
"""
4145

4246

@@ -120,26 +124,26 @@ def test_single_option_multiple_times(self):
120124
def test_non_ascii_chars(self):
121125
ap = ArgumentParser(USAGE2)
122126
inargs = '-x foo=bar --variable a=1,2,3 arg1 arg2'.split()
123-
exp_opts = { 'var-able':'foo=bar', 'variable':'a=1,2,3' }
124-
exp_args = [ 'arg1', 'arg2' ]
127+
exp_opts = {'var-able':'foo=bar', 'variable':'a=1,2,3', '42': False}
128+
exp_args = ['arg1', 'arg2']
125129
opts, args = ap.parse_args(inargs)
126130
assert_equals(opts, exp_opts)
127131
assert_equals(args, exp_args)
128132

129133
def test_check_args_with_correct_args(self):
130134
for args in [ ('hello',), ('hello world',) ]:
131-
self.ap.check_args(args)
135+
self.ap.parse_args(args, check_args=True)
132136

133137
def test_check_args_with_wrong_number_of_args(self):
134138
for args in [ (), ('arg1','arg2','arg3') ]:
135-
assert_raises(DataError, self.ap.check_args, args)
139+
assert_raises(DataError, self.ap._check_args, args)
136140

137141
def test_check_variable_number_of_args(self):
138142
ap = ArgumentParser('usage: robot.py [options] args')
139-
ap.check_args(['one_is_ok'])
140-
ap.check_args(['two', 'ok'])
141-
ap.check_args(['this', 'should', 'also', 'work', 'pretty', 'well'])
142-
assert_raises(DataError, ap.check_args, [])
143+
ap.parse_args(['one_is_ok'], check_args=True)
144+
ap.parse_args(['two', 'ok'], check_args=True)
145+
ap.parse_args(['this', 'should', 'also', 'work', '!'], check_args=True)
146+
assert_raises(DataError, ap._check_args, [])
143147

144148
def test_unescape_options(self):
145149
cli = '--escape quot:Q -E space:SP -E lt:LT -E gt:GT ' \
@@ -150,7 +154,7 @@ def test_unescape_options(self):
150154
assert_equals(args, ['source with spaces'])
151155

152156
def test_split_pythonpath(self):
153-
ap = ArgumentParser('')
157+
ap = ArgumentParser('ignored')
154158
data = [ (['path'], ['path']),
155159
(['path1','path2'], ['path1','path2']),
156160
(['path1:path2'], ['path1','path2']),
@@ -165,7 +169,7 @@ def test_split_pythonpath(self):
165169
assert_equals(ap._split_pythonpath(inp), exp)
166170

167171
def test_get_pythonpath(self):
168-
ap = ArgumentParser('')
172+
ap = ArgumentParser('ignored')
169173
p1 = os.path.abspath('.')
170174
p2 = os.path.abspath('..')
171175
assert_equals(ap._get_pythonpath(p1), [p1])
@@ -187,20 +191,46 @@ def test_arguments_with_glob_patterns_arent_removed_if_they_dont_match(self):
187191
class TestPrintHelpAndVersion(unittest.TestCase):
188192

189193
def setUp(self):
190-
self.ap = ArgumentParser(USAGE, version='testing 1.0')
194+
self.ap = ArgumentParser(USAGE, version='1.0 alpha')
195+
self.ap2 = ArgumentParser(USAGE2)
191196

192197
def test_print_help(self):
193-
assert_raises_with_msg(Information, USAGE,
194-
self.ap.parse_args, ['--help'], help='help')
198+
assert_raises_with_msg(Information, USAGE2,
199+
self.ap2.parse_args, ['--42'], help='42')
200+
201+
def test_name_is_got_from_first_line_of_the_usage(self):
202+
assert_equals(self.ap._name, 'Example Tool')
203+
assert_equals(self.ap2._name, 'Just Name Here')
195204

196205
def test_print_version(self):
197-
assert_raises_with_msg(Information, 'testing 1.0',
206+
assert_raises_with_msg(Information, 'Example Tool 1.0 alpha',
198207
self.ap.parse_args, ['--version'], version='version')
199208

200209
def test_print_version_when_version_not_set(self):
201-
ap = ArgumentParser(USAGE)
202-
assert_raises_with_msg(Information, "No version information available",
203-
ap.parse_args, ['--version'], version='version')
210+
assert_raises(FrameworkError, self.ap2.parse_args, ['--42', '-x a'], version='42')
211+
212+
def test_version_is_replaced_in_help(self):
213+
assert_raises_with_msg(Information, USAGE.replace('<VERSION>', '1.0 alpha'),
214+
self.ap.parse_args, ['--help'], help='help')
215+
216+
def test_escapes_are_replaced_in_help(self):
217+
usage = """Name
218+
--escape x:y blaa blaa .............................................. end
219+
<-----------------------ESCAPES---------------------------->
220+
-- next line --
221+
--he"""
222+
expected = """Name
223+
--escape x:y blaa blaa .............................................. end
224+
Available escapes:
225+
amp (&), apos ('), at (@), bslash (\), colon (:), comma (,),
226+
curly1 ({), curly2 (}), dollar ($), exclam (!), gt (>), hash
227+
(#), lt (<), paren1 ((), paren2 ()), percent (%), pipe (|),
228+
quest (?), quot ("), semic (;), slash (/), space ( ),
229+
square1 ([), square2 (]), star (*)
230+
-- next line --
231+
--he"""
232+
assert_raises_with_msg(Information, expected,
233+
ArgumentParser(usage).parse_args, ['--he'], help='he')
204234

205235

206236
if __name__ == "__main__":

0 commit comments

Comments
 (0)