diff options
author | Anthon van der Neut <anthon@mnt.org> | 2014-06-30 14:32:45 +0200 |
---|---|---|
committer | Anthon van der Neut <anthon@mnt.org> | 2014-06-30 14:32:45 +0200 |
commit | 4b51266bb19f7df8c8708485ea97869327d87a06 (patch) | |
tree | 0d93777f64b23843a7c71a06ea3a8227a0ca677a | |
parent | 9918adcba6cda48e9d51b04196ff632bdd5c74ac (diff) | |
download | ruamel.std.argparse-4b51266bb19f7df8c8708485ea97869327d87a06.tar.gz |
- python 3.4
- keep order of arguments
- extended tests for ordering
-rw-r--r-- | __init__.py | 281 | ||||
-rw-r--r-- | _action/count.py | 2 | ||||
-rw-r--r-- | _action/splitappend.py | 2 | ||||
-rw-r--r-- | test/test_argparse.py | 12 | ||||
-rw-r--r-- | test/test_program.py | 74 | ||||
-rw-r--r-- | tox.ini | 2 |
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() @@ -1,5 +1,5 @@ [tox] -envlist = py27 +envlist = py27,py34 [testenv] commands = py.test test |