summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnthon van der Neut <anthon@mnt.org>2014-06-30 14:32:45 +0200
committerAnthon van der Neut <anthon@mnt.org>2014-06-30 14:32:45 +0200
commit4b51266bb19f7df8c8708485ea97869327d87a06 (patch)
tree0d93777f64b23843a7c71a06ea3a8227a0ca677a
parent9918adcba6cda48e9d51b04196ff632bdd5c74ac (diff)
downloadruamel.std.argparse-4b51266bb19f7df8c8708485ea97869327d87a06.tar.gz
- python 3.4
- keep order of arguments - extended tests for ordering
-rw-r--r--__init__.py281
-rw-r--r--_action/count.py2
-rw-r--r--_action/splitappend.py2
-rw-r--r--test/test_argparse.py12
-rw-r--r--test/test_program.py74
-rw-r--r--tox.ini2
6 files changed, 293 insertions, 80 deletions
diff --git a/__init__.py b/__init__.py
index af658b3..06e9803 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,5 +1,15 @@
# coding: utf-8
+from __future__ import print_function
+
+# from six
+import sys
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+ string_types = str,
+else:
+ string_types = basestring,
# < from ruamel.util.new import _convert_version
def _convert_version(tup):
@@ -61,9 +71,9 @@ class SubParsersAction(argparse._SubParsersAction):
return parser
-from _action.checksinglestore import CheckSingleStoreAction
-from _action.count import CountAction
-from _action.splitappend import SplitAppendAction
+from ._action.checksinglestore import CheckSingleStoreAction
+from ._action.count import CountAction
+from ._action.splitappend import SplitAppendAction
class SmartFormatter(argparse.HelpFormatter):
@@ -74,25 +84,28 @@ class SmartFormatter(argparse.HelpFormatter):
The SmartFormatter has sensible defaults (RawDescriptionFormatter) and
the individual help text can be marked ( help="R|" ) for
variations in formatting.
+ version string is formatted using _split_lines and preserves any
+ line breaks in the version string.
"""
def __init__(self, *args, **kw):
- self._add_defaults = False
+ self._add_defaults = None
super(SmartFormatter, self).__init__(*args, **kw)
def _fill_text(self, text, width, indent):
return ''.join([indent + line for line in text.splitlines(True)])
def _split_lines(self, text, width):
- # print 'TEXT', text
if text.startswith('D|'):
self._add_defaults = True
text = text[2:]
+ elif text.startswith('*|'):
+ text = text[2:]
if text.startswith('R|'):
return text[2:].splitlines()
return argparse.HelpFormatter._split_lines(self, text, width)
def _get_help_string(self, action):
- if not self._add_defaults:
+ if self._add_defaults is None:
return argparse.HelpFormatter._get_help_string(self, action)
help = action.help
if '%(default)' not in action.help:
@@ -102,7 +115,21 @@ class SmartFormatter(argparse.HelpFormatter):
help += ' (default: %(default)s)'
return help
-
+ def _expand_help(self, action):
+ """mark a password help with '*|' at the start, so that
+ when global default adding is activated (e.g. through a helpstring
+ starting with 'D|') no password is show by default.
+ Orginal marking used in repo cannot be used because of decorators.
+ """
+ hs = self._get_help_string(action)
+ if hs.startswith('*|'):
+ params = dict(vars(action), prog=self._prog)
+ if params.get('default') is not None:
+ # you can update params, this will change the default, but we
+ # are printing help only
+ params['default'] = '*' * len(params['default'])
+ return self._get_help_string(action) % params
+ return super(SmartFormatter, self)._expand_help(action)
class ProgramBase(object):
@@ -110,66 +137,46 @@ class ProgramBase(object):
ToDo:
- grouping
- mutual exclusion
+ - Original order/sorted (by kw)
+
"""
+ _methods_with_sub_parsers = []
+
def __init__(self, *args, **kw):
self._verbose = kw.pop('verbose', 0)
self._parser = argparse.ArgumentParser(*args, **kw)
cls = self
self._sub_parsers = None
methods_with_sub_parsers = [] # list to process, multilevel
- for x in dir(cls):
- if x.startswith('_'):
- continue
- method = getattr(self, x)
- if hasattr(method, "_sub_parser"):
- if self._sub_parsers is None:
- # create the top level subparsers
- self._sub_parsers = self._parser.add_subparsers(
- dest="subparser_level_0", help=None)
- methods_with_sub_parsers.append(method)
- max_depth = 10
- level = 0
- all_methods_with_sub_parsers = methods_with_sub_parsers[:]
- while methods_with_sub_parsers:
- level += 1
- if level > max_depth:
- raise NotImplementedError
- for method in methods_with_sub_parsers:
- parent = method._sub_parser['kw'].get('_parent', None)
- sub_parsers = self._sub_parsers
- if parent is None:
- method._sub_parser['level'] = 0
- # parent sub parser
- elif 'level' not in parent._sub_parser:
- print 'skipping', parent.__name__, method.__name__
- continue
- else: # have a parent
- # make sure _parent is no longer in kw
+ all_methods_with_sub_parsers = []
+
+ def add_subparsers(method_name_list, parser, level=0):
+ if not method_name_list:
+ return None
+ ssp = parser.add_subparsers(
+ dest="subparser_level_{0}".format(level),)
+ for method_name in method_name_list:
+ #print('method', ' ' * level, method_name)
+ method = getattr(self, method_name)
+ all_methods_with_sub_parsers.append(method)
+ info = method._sub_parser
+ info['level'] = level
+ if level > 0:
method._sub_parser['parent'] = \
method._sub_parser['kw'].pop('_parent')
- level = parent._sub_parser['level'] + 1
- method._sub_parser['level'] = level
- print 'level', level
- ssp = parent._sub_parser.get('sp')
- if ssp is None:
- pparser = parent._sub_parser['parser']
- ssp = pparser.add_subparsers(
- dest="subparser_level_{}".format(level),
- )
- parent._sub_parser['sp'] = ssp
- sub_parsers = ssp
arg = method._sub_parser['args']
- if not arg or not isinstance(arg[0], basestring):
+ if not arg or not isinstance(arg[0], string_types):
arg = list(arg)
arg.insert(0, method.__name__)
- sp = sub_parsers.add_parser(*arg,
- **method._sub_parser['kw'])
- # add parser primarily for being able to add subparsers
- method._sub_parser['parser'] = sp
- sp.set_defaults(func=method)
-
- # print x, method._sub_parser
- for o in method._sub_parser['options']:
+ parser = ssp.add_parser(*arg, **method._sub_parser['kw'])
+ info['parser'] = parser
+ res = add_subparsers(info.get('ordering', []),
+ parser, level=level+1)
+ if res is None:
+ # only set default if there are no subparsers, otherwise
+ # defaults override
+ parser.set_defaults(func=method)
+ for o in info['options']:
arg = list(o['args'])
fun_name = o.get('fun')
if arg:
@@ -186,11 +193,32 @@ class ProgramBase(object):
else:
# add long option based on function name
arg.insert(0, '--' + fun_name)
- sp.add_argument(*arg, **o['kw'])
- #print 'removing', method.__name__
- methods_with_sub_parsers.remove(method)
+ parser.add_argument(*arg, **o['kw'])
+ return ssp
+
+ def dump(method_name_list, level=0):
+ if not method_name_list:
+ return None
+ for method_name in method_name_list:
+ print('method', ' ' * level, method_name)
+ method = getattr(self, method_name)
+ info = method._sub_parser
+ for k in sorted(info):
+ if k == 'parser':
+ v = 'ArgumentParser()'
+ elif k == 'sp':
+ v = '_SubParserAction()'
+ else:
+ v = info[k]
+ print(' ' + ' ' * level, k, '->', v)
+ dump(info.get('ordering', []), level=level+1)
+
+ self._sub_parsers = add_subparsers(
+ ProgramBase._methods_with_sub_parsers, self._parser)
+
+ # this only does toplevel and global options
for x in dir(self):
- if x.startswith('_') and not x in ['__init__', '_pb_init']:
+ if x.startswith('_') and x not in ['__init__', '_pb_init']:
continue
method = getattr(self, x)
if hasattr(method, "_options"): # not transfered to sub_parser
@@ -203,14 +231,116 @@ class ProgramBase(object):
except TypeError:
print('args, kw', arg, kw)
if global_option:
+ #print('global option', arg, len(all_methods_with_sub_parsers))
for m in all_methods_with_sub_parsers:
sp = m._sub_parser['parser']
sp.add_argument(*arg, **kw)
- def _parse_args(self, *args):
- self._args = self._parser.parse_args(*args)
+ #print('-------------------')
+ #dump(ProgramBase._methods_with_sub_parsers)
+ if False:
+ for x in dir(cls):
+ #for x in ProgramBase._methods_with_sub_parsers:
+ if x.startswith('_'):
+ continue
+ method = getattr(self, x)
+ if hasattr(method, "_sub_parser"):
+ if self._sub_parsers is None:
+ # create the top level subparsers
+ self._sub_parsers = self._parser.add_subparsers(
+ dest="subparser_level_0", help=None)
+ methods_with_sub_parsers.append(method)
+ max_depth = 10
+ level = 0
+ all_methods_with_sub_parsers = methods_with_sub_parsers[:]
+ while methods_with_sub_parsers:
+ level += 1
+ if level > max_depth:
+ raise NotImplementedError
+ for method in all_methods_with_sub_parsers:
+ if not method in methods_with_sub_parsers:
+ continue
+ parent = method._sub_parser['kw'].get('_parent', None)
+ sub_parsers = self._sub_parsers
+ if parent is None:
+ method._sub_parser['level'] = 0
+ # parent sub parser
+ elif 'level' not in parent._sub_parser:
+ #print('skipping', parent.__name__, method.__name__)
+ continue
+ else: # have a parent
+ # make sure _parent is no longer in kw
+ method._sub_parser['parent'] = \
+ method._sub_parser['kw'].pop('_parent')
+ level = parent._sub_parser['level'] + 1
+ method._sub_parser['level'] = level
+ ssp = parent._sub_parser.get('sp')
+ if ssp is None:
+ pparser = parent._sub_parser['parser']
+ ssp = pparser.add_subparsers(
+ dest="subparser_level_{0}".format(level),
+ )
+ parent._sub_parser['sp'] = ssp
+ sub_parsers = ssp
+ arg = method._sub_parser['args']
+ if not arg or not isinstance(arg[0], basestring):
+ arg = list(arg)
+ arg.insert(0, method.__name__)
+ sp = sub_parsers.add_parser(*arg,
+ **method._sub_parser['kw'])
+ # add parser primarily for being able to add subparsers
+ method._sub_parser['parser'] = sp
+ # and make self._args.func callable
+ sp.set_defaults(func=method)
+
+ # print(x, method._sub_parser)
+ for o in method._sub_parser['options']:
+ arg = list(o['args'])
+ fun_name = o.get('fun')
+ if arg:
+ # short option name only, add long option name
+ # based on function name
+ if len(arg[0]) == 2 and arg[0][0] == '-':
+ if (fun_name):
+ arg.insert(0, '--' + fun_name)
+ else:
+ # no option name
+ if o['kw'].get('nargs') == '+ ':
+ # file names etc, no leading dashes
+ arg.insert(0, fun_name)
+ else:
+ # add long option based on function name
+ arg.insert(0, '--' + fun_name)
+ sp.add_argument(*arg, **o['kw'])
+ methods_with_sub_parsers.remove(method)
+ for x in dir(self):
+ if x.startswith('_') and x not in ['__init__', '_pb_init']:
+ continue
+ method = getattr(self, x)
+ if hasattr(method, "_options"): # not transfered to sub_parser
+ for o in method._options:
+ arg = o['args']
+ kw = o['kw']
+ global_option = kw.pop('global_option', False)
+ try:
+ self._parser.add_argument(*arg, **kw)
+ except TypeError:
+ print('args, kw', arg, kw)
+ if global_option:
+ #print('global option', arg, len(all_methods_with_sub_parsers))
+ for m in all_methods_with_sub_parsers:
+ sp = m._sub_parser['parser']
+ sp.add_argument(*arg, **kw)
+
+ def _parse_args(self, *args, **kw):
+ self._args = self._parser.parse_args(*args, **kw)
return self._args
+ #def _parse_known_args(self, *args, **kw):
+ # self._args, self._unknown_args = \
+ # self._parser.parse_known_args(*args, **kw)
+ # return self._args
+
@staticmethod
def _pb_option(*args, **kw):
def decorator(target):
@@ -236,6 +366,11 @@ class ProgramBase(object):
a = self._parent[1]
k = self._parent[2].copy()
k['_parent'] = self._parent[0]
+ pi = self._parent[0]._sub_parser
+ ordering = pi.setdefault('ordering', [])
+ else:
+ ordering = ProgramBase._methods_with_sub_parsers
+ ordering.append(target.__name__)
# move options to sub_parser
o = getattr(target, '_options', [])
if o:
@@ -255,8 +390,7 @@ class ProgramBase(object):
if arguments is not given the name will be the method name
"""
decorator = Decorator()
- print '>>>>', self.target.__name__, self
- decorator._parent = (self.target, a, k)
+ decorator._parent = (self.target, a, k, [])
return decorator
decorator = Decorator()
@@ -265,8 +399,23 @@ class ProgramBase(object):
# decorators
-def option(*args, **kw):
- return ProgramBase._pb_option(*args, **kw)
+def option(*args, **keywords):
+ """\
+args:
+ name or flags - Either a name or a list of option strings, e.g. foo or -f, --foo.
+keywords:
+ action - The basic type of action to be taken when this argument is encountered at the command line.
+ nargs - The number of command-line arguments that should be consumed.
+ const - A constant value required by some action and nargs selections.
+ default - The value produced if the argument is absent from the command line.
+ type - The type to which the command-line argument should be converted.
+ choices - A container of the allowable values for the argument.
+ required - Whether or not the command-line option may be omitted (optionals only).
+ help - A brief description of what the argument does.
+ metavar - A name for the argument in usage messages.
+ dest - The name of the attribute to be added to the object returned by parse_args().
+ """
+ return ProgramBase._pb_option(*args, **keywords)
def sub_parser(*args, **kw):
@@ -275,4 +424,4 @@ def sub_parser(*args, **kw):
def version(version_string):
return ProgramBase._pb_option(
- '--version', action='version', version=version_string) \ No newline at end of file
+ '--version', action='version', version=version_string)
diff --git a/_action/count.py b/_action/count.py
index 4130e4f..1362907 100644
--- a/_action/count.py
+++ b/_action/count.py
@@ -1,5 +1,7 @@
# coding: utf-8
+from __future__ import print_function
+
import argparse
diff --git a/_action/splitappend.py b/_action/splitappend.py
index 77008e2..fade386 100644
--- a/_action/splitappend.py
+++ b/_action/splitappend.py
@@ -1,5 +1,7 @@
# coding: utf-8
+from __future__ import print_function
+
import argparse
diff --git a/test/test_argparse.py b/test/test_argparse.py
index 1fab404..926b685 100644
--- a/test/test_argparse.py
+++ b/test/test_argparse.py
@@ -41,11 +41,11 @@ def test_argparse(capsys):
full_help = dedent("""\
usage: py.test [-h] [--verbose] [--list LIST] [--oneline]
- {}
+ {0}
optional arguments:
-h, --help show this help message and exit
- --verbose {}
- --list LIST {}
+ --verbose {1}
+ --list LIST {2}
--oneline one line help
""").format(
desc, help_verbose,
@@ -87,11 +87,11 @@ def test_argparse_default(capsys):
full_help = dedent("""\
usage: py.test [-h] [--verbose] [--list LIST] [--oneline]
- {}
+ {0}
optional arguments:
-h, --help show this help message and exit
- --verbose {} (default: False)
- --list LIST {}
+ --verbose {1} (default: False)
+ --list LIST {2}
(default: None)
--oneline one line help (default: False)
""").format(
diff --git a/test/test_program.py b/test/test_program.py
index d5fa015..e4bb947 100644
--- a/test/test_program.py
+++ b/test/test_program.py
@@ -1,12 +1,24 @@
+# coding: utf-8
+from __future__ import print_function
+
+import sys
import pytest
from ruamel.std.argparse import ProgramBase, option, sub_parser, version
class Program(ProgramBase):
def __init__(self):
+ #super(Program, self).__init__(
+ # formatter_class=SmartFormatter
+ #)
ProgramBase.__init__(self)
+ def run(self):
+ print('here', self._args.func)
+ if self._args.func:
+ return self._args.func()
+
# you can put these options on __init__, but if Program is going
# to be subclassed, there will be another __init__ scanned
# in ProgramBase.__init__ than the one decorated here
@@ -31,14 +43,26 @@ class Program(ProgramBase):
def check(self):
pass
+ @check.sub_parser(help='check something')
+ def lablab(self):
+ pass
+
+ @check.sub_parser(help='check something')
+ def k(self):
+ print('doing k')
+
+ @check.sub_parser(help='check something')
+ def m(self):
+ pass
+
@sub_parser(help="call git")
def git(self):
- pass
+ print( 'doing git')
@git.sub_parser('abc')
@option('--extra')
def just_some_name(self):
- pass
+ print( 'doing just_some_name/abc')
@git.sub_parser('hihi', help='helphelp')
def hki(self):
@@ -46,8 +70,32 @@ class Program(ProgramBase):
@hki.sub_parser('oops')
def oops(self):
+ print( 'doing oops')
+
+ @sub_parser(help="call a")
+ def a(self):
+ pass
+
+ @sub_parser(help="call b")
+ def b(self):
+ pass
+
+ @sub_parser(help="call c")
+ def c(self):
pass
+ @sub_parser(help="call d")
+ def d(self):
+ pass
+
+ # on purpose not in "right" order
+ @sub_parser(help="call f")
+ def f(self):
+ print( 'doing f')
+
+ @sub_parser(help="call e")
+ def e(self):
+ pass
#@sub_parser('svn')
#def subversion(self):
@@ -61,8 +109,8 @@ class ParseHelpOutput:
self(o)
def __call__(self, out):
- print out
- print '+++++'
+ print(out)
+ print('+++++')
self._chunks = {}
chunk = None
for line in out.splitlines():
@@ -79,14 +127,24 @@ class ParseHelpOutput:
if chunk is None or not line.strip():
continue
chunk.append(line)
- print self._chunks
+ print('chunks', self._chunks)
+ if not self._chunks:
+ print('stderr', err)
def start(self, chunk, s, strip=True):
+ """check if a stripped line in the chunk text starts with s"""
for l in self._chunks[chunk]:
if l.lstrip().startswith(s):
return True
return False
+ def somewhere(self, chunk, s, strip=True):
+ """check if s is somewhere in the chunk"""
+ for l in self._chunks[chunk]:
+ if s in l:
+ return True
+ return False
+
@pytest.fixture(scope='class')
def program():
@@ -99,6 +157,7 @@ class TestProgram:
program._parse_args('-h'.split())
pho = ParseHelpOutput(capsys)
assert pho.start('positional arguments', 'hg')
+ assert pho.somewhere('usage', 'c,d,f,e')
assert pho.start('optional arguments', '--verbose')
def test_help_sub_parser(self, capsys, program):
@@ -142,9 +201,10 @@ class TestProgram:
def test_version(self, capsys, program):
with pytest.raises(SystemExit):
program._parse_args('--version'.split())
- pho = ParseHelpOutput(capsys, error=True)
+ pho = ParseHelpOutput(capsys, error=sys.version_info < (3,4))
assert pho.start('version', '42')
if __name__ == '__main__':
p = Program()
- p._parse_args() \ No newline at end of file
+ p._parse_args()
+ p.run()
diff --git a/tox.ini b/tox.ini
index 72827a8..3e3f27e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist = py27
+envlist = py27,py34
[testenv]
commands = py.test test