""" 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 # 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): # self - argparse parser # args, namespace - as used by parse_known_args # 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 if self.usage is None: # capture the full usage (could restore to None at end) self.usage = self.format_usage()[7:] positionals = self._get_positional_actions() if any([action.nargs in [PARSER, REMAINDER] for action in positionals]): warn('PARSER or REMAINDER in positionals nargs; \n\tusing parse_known') # these nargs don't play nicely with intermixed # fall back on the default parsing return self.parse_known_args(args, namespace) if [action.dest for group in self._mutually_exclusive_groups for action in group._group_actions if action in positionals]: warn('positional in mutuallyExclusiveGroup; \n\tusing parse_known') # intermixed does not handle MXG with positionals well # fall back on the default parsing return self.parse_known_args(args, namespace) for action in positionals: action.save_nargs = action.nargs action.nargs = 0 try: namespace, remaining_args = self.parse_known_args(args, namespace) for action in positionals: if hasattr(namespace, action.dest): delattr(namespace, action.dest) # remove [] values from namespace except SystemExit: warn('error from 1st parse_known') raise finally: for action in positionals: action.nargs = action.save_nargs 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: raise finally: for action in optionals: action.required = action.save_required for group in self._mutually_exclusive_groups: group.required = group.save_required return namespace, extras ArgumentParser.parse_intermixed_args = parse_intermixed_args if __name__ == "__main__": import sys parserInt = ArgumentParser() parserInt.add_argument('--foo', dest='foo') parserInt.add_argument('--bar', dest='bar') parserInt.add_argument('cmd') parserInt.add_argument('rest', nargs='*') trials = ['a b c d --foo x --bar 1', '--foo x a b c d --bar 1', '--foo x --bar 1 a b c d', 'a b --foo x --bar 1 c d', 'a --foo x b --bar 1 c d', 'a --foo x b c --bar 1 d', 'a --foo x b --bar 1 c --error d', 'a --foo x b --error d --bar 1 c', 'a b c', 'a', '--foo 1', # error: the following arguments are required: cmd, rest '--foo', # error: argument --foo: expected one argument ''] for astr in trials: print(astr) print('') for astr in trials: try: args, extras = parserInt.parse_intermixed_args(astr.split()) print(args, extras) except: #print(sys.exc_info()) print('argv:', astr) print('') print('') print(parserInt.format_help()) # ================= print('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 parserInt = ArgumentParser() parserInt.add_argument('-z') parserInt.add_argument('x') parserInt.add_argument('y', nargs='...') print(parserInt.parse_known_args('X A B -z Z'.split())) print(parserInt.parse_intermixed_args('X A B -z Z'.split())) # ================ print('\nsubparsers case') # skip the 2 step p = ArgumentParser() sp = p.add_subparsers() spp = sp.add_parser('cmd') spp.add_argument('foo') print(p.format_help()) print(p.parse_known_args(['cmd','1'])) print(p.parse_intermixed_args(['cmd','1'])) # ==================== print('\nrequired opts') # TestMessageContentError p = ArgumentParser() p.add_argument('req_pos', type=str) p.add_argument('-req_opt', type=int, required=True) try: print(p.parse_known_args([])) # warns about req_pos and -req_opt except SystemExit: pass try: print(p.parse_intermixed_args([])) # warns only about -req_opt (in 1st parse step) except SystemExit: pass # ================= print('\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') print(parser.parse_known_args('--bar X'.split())) print(parser.parse_intermixed_args('--bar X'.split())) print('\nmutually exclusive case, both') # TestMutuallyExclusiveOptionalAndPositional parser = ArgumentParser(prog='PROG') group = parser.add_mutually_exclusive_group(required=True) group.add_argument('--foo', action='store_true', help='FOO') group.add_argument('--spam', help='SPAM') group.add_argument('badger', nargs='*', default='X', help='BADGER') try: print(parser.parse_known_args(''.split())) # PROG: error: one of the arguments --foo --spam badger is required except SystemExit: pass try: print(parser.parse_intermixed_args(''.split())) # (Namespace(badger='X', foo=False, spam=None), []) # switch to single parse_known to avoid this problem except SystemExit: pass print(parser.parse_known_args('--foo'.split())) print(parser.parse_intermixed_args('--spam 1'.split())) # 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 sys.exit() # ================ print('\noptparse comparison') import optparse def oParse(argv=None): # optparse equivalent p = optparse.OptionParser() p.add_option('--foo') p.add_option('--bar') opts, args = p.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 """