""" method that can be added to argparse.ArgumentParser that allows a free intermix of positional and optional argument strings Its API is the same as for parse_known_args Here it is a unbound function to facilitate testing """ from argparse import ArgumentParser, PARSER, REMAINDER, ArgumentError from gettext import gettext as _, ngettext import traceback # from warnings import warn import logging logging.basicConfig(format='%(levelname)s: Intermix: %(message)s') def warn(message): logging.warning(message) def parse_intermixed_args(self, args=None, namespace=None): args, argv = self.parse_known_intermixed_args(args, namespace) if argv: msg = _('unrecognized arguments: %s') self.error(msg % ' '.join(argv)) return args def parse_known_intermixed_args(self, args=None, namespace=None, _fallback=None): # self - argparse parser # args, namespace - as used by parse_known_args # _fallback - action to take if it can't handle this parser's arguments # (default raises an error) # returns a namespace and list of extras # positional can be freely intermixed with optionals # optionals are first parsed with all positional arguments deactivated # the 'extras' are then parsed # positionals 'deactivated' by setting nargs=0 positionals = self._get_positional_actions() a = [action for action in positionals if action.nargs in [PARSER, REMAINDER]] if a: if _fallback is None: a = a[0] err = ArgumentError(a, 'parse_intermixed_args: positional arg with nargs=%s'%a.nargs) self.error(str(err)) else: return _fallback(args, namespace) if [action.dest for group in self._mutually_exclusive_groups for action in group._group_actions if action in positionals]: if _fallback is None: self.error('parse_intermixed_args: positional in mutuallyExclusiveGroup') else: return _fallback(args, namespace) save_usage = self.usage try: if self.usage is None: # capture the full usage for use in error messages self.usage = self.format_usage()[7:] for action in positionals: # deactivate positionals action.save_nargs = action.nargs action.nargs = 0 try: namespace, remaining_args = self.parse_known_args(args, namespace) for action in positionals: # remove the empty positional values from namespace if hasattr(namespace, action.dest): delattr(namespace, action.dest) except SystemExit: warn('error from 1st parse_known_args') raise finally: # restore nargs and usage before exiting for action in positionals: action.nargs = action.save_nargs #self.usage = save_usage logging.info('1st: %s,%s'%(namespace, remaining_args)) # parse positionals # optionals aren't normally required, but just in case, turn that off optionals = self._get_optional_actions() for action in optionals: action.save_required = action.required action.required = False for group in self._mutually_exclusive_groups: group.save_required = group.required group.required = False try: namespace, extras = self.parse_known_args(remaining_args, namespace) except SystemExit: warn('error from 2nd parse_known_args') raise finally: # restore parser values before exiting for action in optionals: action.required = action.save_required for group in self._mutually_exclusive_groups: group.required = group.save_required finally: self.usage = save_usage return namespace, extras ArgumentParser.parse_intermixed_args = parse_intermixed_args ArgumentParser.parse_known_intermixed_args = parse_known_intermixed_args if __name__ == "__main__": def parse_fallback_args(self, args=None, namespace=None): def fallback(*args): print('fallingback to parse_known_args --') return self.parse_known_args(*args) args, argv = self.parse_known_intermixed_args(args, namespace, _fallback=fallback) if argv: msg = _('unrecognized arguments: %s') self.error(msg % ' '.join(argv)) return args ArgumentParser.parse_fallback_args = parse_fallback_args def error(self, message): usage = self.format_usage() raise Exception('%s%s'%(usage, message)) ArgumentParser.error = error #def exit(self, status=0, message=None): # import sys # raise Exception(message) #ArgumentParser.exit = exit def parse_fallback_args(self, args=None, namespace=None): # alternative fallback, using modified error try: args, argv = self.parse_known_intermixed_args(args, namespace) except Exception as e: if 'parse_intermixed_args' in str(e): print('fallingback to parse_known_args') args, argv = self.parse_known_args(args, namespace) else: raise if argv: msg = _('unrecognized arguments: %s') self.error(msg % ' '.join(argv)) return args ArgumentParser.parse_fallback_args = parse_fallback_args def header(astr): print(astr,'\n==============') parser = ArgumentParser(prog='PROG') parser.add_argument('--foo', dest='foo', required=True) parser.add_argument('--bar', dest='bar') parser.add_argument('cmd') parser.add_argument('rest', nargs='*', type=int) trials = ['cmd 1 2 3 --foo x --bar y', # Namespace(bar='y', cmd='cmd', foo='x', rest=[1, 2, 3]) '--foo x cmd 1 2 3 --bar y', '--foo x --bar y cmd 1 2 3', 'cmd 1 --foo x --bar y 2 3', 'cmd --foo x 1 --bar y 2 3', 'cmd --foo x 1 2 --bar y 3', 'cmd --foo x 1 --bar y 2 --error 3', # (Namespace(bar='y', cmd='cmd', foo='x', rest=[1, 2]), ['--error', '3']) 'cmd --foo x 1 --error 2 --bar y 3', # (Namespace(bar='y', cmd='cmd', foo='x', rest=[1]), ['--error', '2', '3']) 'cmd 1 2', # Namespace(bar=None, cmd='cmd', foo=None, rest=[1, 2]) 'cmd', '--foo 1', # error: the following arguments are required: cmd, rest '--foo', # error: argument --foo: expected one argument ''] print('') for astr in trials: args1 = None print("'%s'"%astr) try: args = parser.parse_fallback_args(astr.split()) print(args) if args1 is None: args1 = args else: assert(args1 == args) except Exception as e: print(e) if 'unrecognized' in str(e): print(parser.parse_known_intermixed_args(astr.split())) print('') else: pass assert(parser.usage is None) print('') print(parser.format_help()) # ================= header('behavior with REMAINDER') # TestNargsRemainder # REMAINDER acts after optionals have been processed # skip 2 step parse if there is a REMAINDER, so NS is same # alt is to raise error if there is a REMAINDER parser = ArgumentParser(prog='PROG') parser.add_argument('-z') parser.add_argument('x') parser.add_argument('y', nargs='...') argv ='X A B -z Z'.split() try: print(parser.parse_fallback_args(argv)) except Exception as e: print(e) # ================ header('\nsubparsers case') parser = ArgumentParser(prog='PROG') sp = parser.add_subparsers() spp = sp.add_parser('cmd') spp.add_argument('foo') print(parser.format_help()) argv ='cmd 1'.split() print(parser.parse_fallback_args(argv)) # ==================== header('\nrequired opts') # TestMessageContentError parser = ArgumentParser(prog='PROG') parser.add_argument('req_pos', type=str) parser.add_argument('-req_opt', type=int, required=True) try: print(parser.parse_args([])) # warns about req_pos and -req_opt except Exception as e: print('parse_args',e) try: print(parser.parse_intermixed_args([])) # warns only about -req_opt (in 1st parse step) except Exception as e: print('parse_intermixed_args',e) # ================= header('\nmutually exclusive case') parser = ArgumentParser(prog='PROG') group = parser.add_mutually_exclusive_group(required=True) group.add_argument('--bar', help='bar help') group.add_argument('--baz', nargs='?', const='Z', help='baz help') argv = '--bar X'.split(); print(argv) print(parser.parse_args(argv)) print(parser.parse_fallback_args(argv)) assert(group.required) header('\nmutually exclusive case, both') # TestMutuallyExclusiveOptionalAndPositional parser = ArgumentParser(prog='PROG') parser.add_argument('baz', nargs=2) group = parser.add_mutually_exclusive_group(required=True) group.add_argument('--foo', action='store_true', help='FOO') group.add_argument('--spam', help='SPAM') print(parser.parse_fallback_args('baz --spam 1 baz'.split())) print('') group.add_argument('badger', nargs='?', default='X', help='BADGER') argv = 'baz baz'.split(); print(argv) try: print(parser.parse_args(argv)) # PROG: error: one of the arguments --foo --spam badger is required except Exception as e: print(e) try: print(parser.parse_intermixed_args(argv)) # parse_intermixed_args: positional in mutuallyExclusiveGroup except Exception as e: print(e) assert(parser.usage is None) argv = 'baz baz badger --foo'.split(); print(argv) try: print(parser.parse_fallback_args(argv)) except Exception as e: print(e) # error: argument badger: not allowed with argument --foo # badger with nargs=0 matches 'nothing' in the 1st parse # switch to single parse_known to avoid this problem import sys sys.exit() # ================ print('\noptparse comparison') import optparse def oParse(argv=None): # optparse equivalent parser = optparse.OptionParser() parser.add_option('--foo') parser.add_option('--bar') opts, args = parser.parse_args(argv) opts.cmd = args[0] opts.rest = args[1:] return opts print('optparse') for astr in trials: try: print(oParse(astr.split())) except: print(sys.exc_info()) # {'bar': '1', 'cmd': 'a', 'foo': 'x', 'rest': ['b', 'c', 'd']} print('') oParse(['-h']) """ when parse_intermixed_args is used parse_args, test_argparse.py gives errors in : TestActionUserDefined fail in: TestMessageContentError from gettext import gettext as _, ngettext def parse_fudge_args(self, args=None, namespace=None): args, argv = self.parse_known_intermixed_args(args, namespace, _fallback=self.parse_known_args) if argv: msg = _('unrecognized arguments: %s') self.error(msg % ' '.join(argv)) return args argparse.ArgumentParser.parse_args = parse_fudge_args """