Skip to content

Commit 007a77c

Browse files
committed
drop help for long options if longer versions with hyphens are available
--HG-- branch : opt-drop-non-hyphened-long-options
1 parent 18fa7d8 commit 007a77c

File tree

2 files changed

+170
-31
lines changed

2 files changed

+170
-31
lines changed

_pytest/config.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,13 @@ def __init__(self, name, description="", parser=None):
277277
self.parser = parser
278278

279279
def addoption(self, *optnames, **attrs):
280-
""" add an option to this group. """
280+
""" add an option to this group.
281+
282+
if a shortened version of a long option is specified it will
283+
be suppressed in the help. addoption('--twowords', '--two-words')
284+
results in help showing '--two-words' only, but --twowords gets
285+
accepted **and** the automatic destination is in args.twowords
286+
"""
281287
option = Argument(*optnames, **attrs)
282288
self._addoption_instance(option, shortupper=False)
283289

@@ -299,7 +305,7 @@ class MyOptionParser(py.std.argparse.ArgumentParser):
299305
def __init__(self, parser):
300306
self._parser = parser
301307
py.std.argparse.ArgumentParser.__init__(self, usage=parser._usage,
302-
add_help=False)
308+
add_help=False, formatter_class=DropShorterLongHelpFormatter)
303309

304310
def format_epilog(self, formatter):
305311
hints = self._parser.hints
@@ -320,6 +326,67 @@ def parse_args(self, args=None, namespace=None):
320326
getattr(args, Config._file_or_dir).extend(argv)
321327
return args
322328

329+
# #pylib 2013-07-31
330+
# (12:05:53) anthon: hynek: can you get me a list of preferred py.test
331+
# long-options with '-' inserted at the right places?
332+
# (12:08:29) hynek: anthon, hpk: generally I'd love the following, decide
333+
# yourself which you agree and which not:
334+
# (12:10:51) hynek: --exit-on-first --full-trace --junit-xml --junit-prefix
335+
# --result-log --collect-only --conf-cut-dir --trace-config
336+
# --no-magic
337+
# (12:18:21) hpk: hynek,anthon: makes sense to me.
338+
# (13:40:30) hpk: hynek: let's not change names, rather only deal with
339+
# hyphens for now
340+
# (13:40:50) hynek: then --exit-first *shrug*
341+
342+
class DropShorterLongHelpFormatter(py.std.argparse.HelpFormatter):
343+
"""shorten help for long options that differ only in extra hyphens
344+
345+
- collapse **long** options that are the same except for extra hyphens
346+
- special action attribute map_long_option allows surpressing additional
347+
long options
348+
- shortcut if there are only two options and one of them is a short one
349+
- cache result on action object as this is called at least 2 times
350+
"""
351+
def _format_action_invocation(self, action):
352+
orgstr = py.std.argparse.HelpFormatter._format_action_invocation(self, action)
353+
if orgstr and orgstr[0] != '-': # only optional arguments
354+
return orgstr
355+
res = getattr(action, '_formatted_action_invocation', None)
356+
if res:
357+
return res
358+
options = orgstr.split(', ')
359+
if len(options) == 2 and (len(options[0]) == 2 or len(options[1]) == 2):
360+
# a shortcut for '-h, --help' or '--abc', '-a'
361+
action._formatted_action_invocation = orgstr
362+
return orgstr
363+
return_list = []
364+
option_map = getattr(action, 'map_long_option', {})
365+
if option_map is None:
366+
option_map = {}
367+
short_long = {}
368+
for option in options:
369+
if len(option) == 2 or option[2] == ' ':
370+
continue
371+
if not option.startswith('--'):
372+
raise ArgumentError('long optional argument without "--": [%s]'
373+
% (option), self)
374+
xxoption = option[2:]
375+
if xxoption.split()[0] not in option_map:
376+
shortened = xxoption.replace('-', '')
377+
if shortened not in short_long or \
378+
len(short_long[shortened]) < len(xxoption):
379+
short_long[shortened] = xxoption
380+
# now short_long has been filled out to the longest with dashes
381+
# **and** we keep the right option ordering from add_argument
382+
for option in options: #
383+
if len(option) == 2 or option[2] == ' ':
384+
return_list.append(option)
385+
if option[2:] == short_long.get(option.replace('-', '')):
386+
return_list.append(option)
387+
action._formatted_action_invocation = ', '.join(return_list)
388+
return action._formatted_action_invocation
389+
323390

324391
class Conftest(object):
325392
""" the single place for accessing values and interacting

testing/test_parseopt.py

Lines changed: 101 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
from _pytest import config as parseopt
44
from textwrap import dedent
55

6+
@pytest.fixture
7+
def parser():
8+
return parseopt.Parser()
9+
610
class TestParser:
711
def test_no_help_by_default(self, capsys):
812
parser = parseopt.Parser(usage="xyz")
913
pytest.raises(SystemExit, 'parser.parse(["-h"])')
1014
out, err = capsys.readouterr()
1115
assert err.find("error: unrecognized arguments") != -1
1216

13-
def test_argument(self):
17+
def test_argument(self, parser):
1418
with pytest.raises(parseopt.ArgumentError):
1519
# need a short or long option
1620
argument = parseopt.Argument()
@@ -25,7 +29,7 @@ def test_argument(self):
2529
argument = parseopt.Argument('-t', '--test', dest='abc')
2630
assert argument.dest == 'abc'
2731

28-
def test_argument_type(self):
32+
def test_argument_type(self, parser):
2933
argument = parseopt.Argument('-t', dest='abc', type='int')
3034
assert argument.type is int
3135
argument = parseopt.Argument('-t', dest='abc', type='string')
@@ -46,22 +50,19 @@ def test_argument_processopt(self):
4650
assert res['default'] == 42
4751
assert res['dest'] == 'abc'
4852

49-
def test_group_add_and_get(self):
50-
parser = parseopt.Parser()
53+
def test_group_add_and_get(self, parser):
5154
group = parser.getgroup("hello", description="desc")
5255
assert group.name == "hello"
5356
assert group.description == "desc"
5457

55-
def test_getgroup_simple(self):
56-
parser = parseopt.Parser()
58+
def test_getgroup_simple(self, parser):
5759
group = parser.getgroup("hello", description="desc")
5860
assert group.name == "hello"
5961
assert group.description == "desc"
6062
group2 = parser.getgroup("hello")
6163
assert group2 is group
6264

63-
def test_group_ordering(self):
64-
parser = parseopt.Parser()
65+
def test_group_ordering(self, parser):
6566
group0 = parser.getgroup("1")
6667
group1 = parser.getgroup("2")
6768
group1 = parser.getgroup("3", after="1")
@@ -75,8 +76,7 @@ def test_group_addoption(self):
7576
assert len(group.options) == 1
7677
assert isinstance(group.options[0], parseopt.Argument)
7778

78-
def test_group_shortopt_lowercase(self):
79-
parser = parseopt.Parser()
79+
def test_group_shortopt_lowercase(self, parser):
8080
group = parser.getgroup("hello")
8181
pytest.raises(ValueError, """
8282
group.addoption("-x", action="store_true")
@@ -85,36 +85,31 @@ def test_group_shortopt_lowercase(self):
8585
group._addoption("-x", action="store_true")
8686
assert len(group.options) == 1
8787

88-
def test_parser_addoption(self):
89-
parser = parseopt.Parser()
88+
def test_parser_addoption(self, parser):
9089
group = parser.getgroup("custom options")
9190
assert len(group.options) == 0
9291
group.addoption("--option1", action="store_true")
9392
assert len(group.options) == 1
9493

95-
def test_parse(self):
96-
parser = parseopt.Parser()
94+
def test_parse(self, parser):
9795
parser.addoption("--hello", dest="hello", action="store")
9896
args = parser.parse(['--hello', 'world'])
9997
assert args.hello == "world"
10098
assert not getattr(args, parseopt.Config._file_or_dir)
10199

102-
def test_parse2(self):
103-
parser = parseopt.Parser()
100+
def test_parse2(self, parser):
104101
args = parser.parse([py.path.local()])
105102
assert getattr(args, parseopt.Config._file_or_dir)[0] == py.path.local()
106103

107-
def test_parse_will_set_default(self):
108-
parser = parseopt.Parser()
104+
def test_parse_will_set_default(self, parser):
109105
parser.addoption("--hello", dest="hello", default="x", action="store")
110106
option = parser.parse([])
111107
assert option.hello == "x"
112108
del option.hello
113109
args = parser.parse_setoption([], option)
114110
assert option.hello == "x"
115111

116-
def test_parse_setoption(self):
117-
parser = parseopt.Parser()
112+
def test_parse_setoption(self, parser):
118113
parser.addoption("--hello", dest="hello", action="store")
119114
parser.addoption("--world", dest="world", default=42)
120115
class A: pass
@@ -124,14 +119,12 @@ class A: pass
124119
assert option.world == 42
125120
assert not args
126121

127-
def test_parse_special_destination(self):
128-
parser = parseopt.Parser()
122+
def test_parse_special_destination(self, parser):
129123
x = parser.addoption("--ultimate-answer", type=int)
130124
args = parser.parse(['--ultimate-answer', '42'])
131125
assert args.ultimate_answer == 42
132126

133-
def test_parse_split_positional_arguments(self):
134-
parser = parseopt.Parser()
127+
def test_parse_split_positional_arguments(self, parser):
135128
parser.addoption("-R", action='store_true')
136129
parser.addoption("-S", action='store_false')
137130
args = parser.parse(['-R', '4', '2', '-S'])
@@ -162,6 +155,80 @@ def defaultget(option):
162155
assert option.this == 42
163156
assert option.no is False
164157

158+
@pytest.mark.skipif("sys.version_info < (2,5)")
159+
def test_drop_short_helper(self):
160+
parser = py.std.argparse.ArgumentParser(formatter_class=parseopt.DropShorterLongHelpFormatter)
161+
parser.add_argument('-t', '--twoword', '--duo', '--two-word', '--two',
162+
help='foo').map_long_option = {'two': 'two-word'}
163+
# throws error on --deux only!
164+
parser.add_argument('-d', '--deuxmots', '--deux-mots',
165+
action='store_true', help='foo').map_long_option = {'deux': 'deux-mots'}
166+
parser.add_argument('-s', action='store_true', help='single short')
167+
parser.add_argument('--abc', '-a',
168+
action='store_true', help='bar')
169+
parser.add_argument('--klm', '-k', '--kl-m',
170+
action='store_true', help='bar')
171+
parser.add_argument('-P', '--pq-r', '-p', '--pqr',
172+
action='store_true', help='bar')
173+
parser.add_argument('--zwei-wort', '--zweiwort', '--zweiwort',
174+
action='store_true', help='bar')
175+
parser.add_argument('-x', '--exit-on-first', '--exitfirst',
176+
action='store_true', help='spam').map_long_option = {'exitfirst': 'exit-on-first'}
177+
parser.add_argument('files_and_dirs', nargs='*')
178+
args = parser.parse_args(['-k', '--duo', 'hallo', '--exitfirst'])
179+
assert args.twoword == 'hallo'
180+
assert args.klm is True
181+
assert args.zwei_wort is False
182+
assert args.exit_on_first is True
183+
assert args.s is False
184+
args = parser.parse_args(['--deux-mots'])
185+
with pytest.raises(AttributeError):
186+
assert args.deux_mots is True
187+
assert args.deuxmots is True
188+
args = parser.parse_args(['file', 'dir'])
189+
assert '|'.join(args.files_and_dirs) == 'file|dir'
190+
191+
def test_drop_short_0(self, parser):
192+
parser.addoption('--funcarg', '--func-arg', action='store_true')
193+
parser.addoption('--abc-def', '--abc-def', action='store_true')
194+
parser.addoption('--klm-hij', action='store_true')
195+
args = parser.parse(['--funcarg', '--k'])
196+
assert args.funcarg is True
197+
assert args.abc_def is False
198+
assert args.klm_hij is True
199+
200+
@pytest.mark.skipif("sys.version_info < (2,5)")
201+
def test_drop_short_2(self, parser):
202+
parser.addoption('--func-arg', '--doit', action='store_true')
203+
args = parser.parse(['--doit'])
204+
assert args.func_arg is True
205+
206+
@pytest.mark.skipif("sys.version_info < (2,5)")
207+
def test_drop_short_3(self, parser):
208+
parser.addoption('--func-arg', '--funcarg', '--doit', action='store_true')
209+
args = parser.parse(['abcd'])
210+
assert args.func_arg is False
211+
assert args.file_or_dir == ['abcd']
212+
213+
@pytest.mark.skipif("sys.version_info < (2,5)")
214+
def test_drop_short_help0(self, parser, capsys):
215+
parser.addoption('--func-args', '--doit', help = 'foo',
216+
action='store_true')
217+
parser.parse([])
218+
help = parser.optparser.format_help()
219+
assert '--func-args, --doit foo' in help
220+
221+
# testing would be more helpful with all help generated
222+
@pytest.mark.skipif("sys.version_info < (2,5)")
223+
def test_drop_short_help1(self, parser, capsys):
224+
group = parser.getgroup("general")
225+
group.addoption('--doit', '--func-args', action='store_true', help='foo')
226+
group._addoption("-h", "--help", action="store_true", dest="help",
227+
help="show help message and configuration info")
228+
parser.parse(['-h'])
229+
help = parser.optparser.format_help()
230+
assert '-doit, --func-args foo' in help
231+
165232
@pytest.mark.skipif("sys.version_info < (2,5)")
166233
def test_addoption_parser_epilog(testdir):
167234
testdir.makeconftest("""
@@ -173,7 +240,7 @@ def pytest_addoption(parser):
173240
#assert result.ret != 0
174241
result.stdout.fnmatch_lines(["hint: hello world", "hint: from me too"])
175242

176-
@pytest.mark.skipif("sys.version_info < (2,5)")
243+
@pytest.mark.skipif("sys.version_info < (2,6)")
177244
def test_argcomplete(testdir, monkeypatch):
178245
if not py.path.local.sysfind('bash'):
179246
pytest.skip("bash not available")
@@ -196,17 +263,22 @@ def test_argcomplete(testdir, monkeypatch):
196263
monkeypatch.setenv('COMP_LINE', "py.test " + arg)
197264
monkeypatch.setenv('COMP_POINT', str(len("py.test " + arg)))
198265
result = testdir.run('bash', str(script), arg)
199-
#print dir(result), result.ret
200266
if result.ret == 255:
201267
# argcomplete not found
202268
pytest.skip("argcomplete not available")
203269
else:
204-
result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"])
205-
270+
#print 'type ---------------', result.stdout, result.stdout.lines
271+
if py.std.sys.version_info < (2,7):
272+
result.stdout.lines = result.stdout.lines[0].split('\x0b')
273+
result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"])
274+
else:
275+
result.stdout.fnmatch_lines(["--funcargs", "--fulltrace"])
276+
if py.std.sys.version_info < (2,7):
277+
return
206278
os.mkdir('test_argcomplete.d')
207279
arg = 'test_argc'
208280
monkeypatch.setenv('COMP_LINE', "py.test " + arg)
209281
monkeypatch.setenv('COMP_POINT', str(len('py.test ' + arg)))
210282
result = testdir.run('bash', str(script), arg)
211283
result.stdout.fnmatch_lines(["test_argcomplete", "test_argcomplete.d/"])
212-
# restore environment
284+

0 commit comments

Comments
 (0)