''' Created on Feb 8, 2014 @author: Chris ''' import types from parser_exceptions import ArgumentError from argparse import ArgumentParser from argparse import RawDescriptionHelpFormatter import code_prep class MonkeyParser(object): ''' Builds a parser instance from the code extracted from the client module. The instance is stored as a private variable in the class and all methods are delagted to it so that the user of the class can treat it just as a normal argparse instance. ''' def __init__(self, source_code): self._parser_instance = self._build_argparser_from_client_source(source_code) # Monkey patch parser's `error` method so that it raises an error # rather than silently exiting self._parser_instance.error = types.MethodType( self._ErrorAsString, self._parser_instance ) def _build_argparser_from_client_source(self, source_code): ''' runs the client code by evaling each line. Example input Code: parser = ArgumentParser(description=program_license, formatter_class=RawDescriptionHelpFormatter) parser.add_argument("-r", "--recursive", dest="recurse", action="store_true", help="recurse into subfolders [default: %(default)s]") parser.add_argument("-v", "--verbose", dest="verbose", action="count", help="set verbosity level [default: %(default)s]") Method extracts the instance name (e.g. parser) from the first line, and instantiates it in a local variable by evaling the rest of the lines. Each subsequent line updates the local variable in turn. ''' imports = filter(lambda x: 'gooey' not in x, code_prep.take_imports(source_code)) arg_code = code_prep.drop_imports(source_code) updated_source_code = code_prep.update_parser_varname('clients_parser', arg_code) for _import in imports: exec(_import) first_line = updated_source_code.pop(0) clients_parser, assignment = code_prep.split_line(first_line) clients_parser = eval(assignment) for line in updated_source_code: eval(line) return clients_parser def _format_source_with_new_varname(self, new_variable_name, source): ''' 'injects' the client code with a known variable name so that it can be `eval`d and assigned to a variable in the local code. For example, if the client code was: parser = ArgumentParser(descrip...) parser.add_argument("-r", "--re...) parser.add_argument("-v", "--ve...) The variable "parser" would be overwritten with a custom name. e.g. my_parser = ArgumentParser(descrip...) my_parser.add_argument("-r", "--re...) ''' source_code = source[:] first_line = source_code[0] client_parser_variable, statement = self._split_line(first_line) client_parser_variable = client_parser_variable.strip() for index, line in enumerate(source_code): source_code[index] = line.replace(client_parser_variable, new_variable_name) source_code.append('{0}.parse_args()'.format(new_variable_name)) return source_code def _split_line(self, line): # Splits line at the first = sign, # joins everything after the first = # to account for additional = signs in # parameters components = line.split('=') var = components.pop(0) return var, '='.join(components) def __getattr__(self, attr): ''' Auto-delegates everything to the ArgumentParser instance''' return getattr(self._parser_instance, attr) @staticmethod def _ErrorAsString(self, msg): ''' Monkey patch for parser.error Raises an error rather than printing and silently exiting. ''' raise ArgumentError(msg) if __name__ == '__main__': pass