You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

108 lines
3.5 KiB

  1. '''
  2. Created on Feb 8, 2014
  3. @author: Chris
  4. '''
  5. import types
  6. from parser_exceptions import ArgumentError
  7. from argparse import ArgumentParser
  8. from argparse import RawDescriptionHelpFormatter
  9. class MonkeyParser(object):
  10. '''
  11. Builds a parser instance from the code
  12. extracted from the client module.
  13. The instance is stored as a private variable in the
  14. class and all methods are delagted to it so that the
  15. user of the class can treat it just as a normal argparse
  16. instance.
  17. '''
  18. def __init__(self, source_code):
  19. self._parser_instance = self._build_argparser_from_client_source(source_code)
  20. # Monkey patch parser's `error` method so that it raises an error
  21. # rather than silently exiting
  22. self._parser_instance.error = types.MethodType(
  23. self._ErrorAsString,
  24. self._parser_instance
  25. )
  26. def _build_argparser_from_client_source(self, source_code):
  27. '''
  28. runs the client code by evaling each line.
  29. Example input Code:
  30. parser = ArgumentParser(description=program_license, formatter_class=RawDescriptionHelpFormatter)
  31. parser.add_argument("-r", "--recursive", dest="recurse", action="store_true", help="recurse into subfolders [default: %(default)s]")
  32. parser.add_argument("-v", "--verbose", dest="verbose", action="count", help="set verbosity level [default: %(default)s]")
  33. Method extracts the instance name (e.g. parser) from the first line,
  34. and instantiates it in a local variable by evaling the rest of the lines.
  35. Each subsequent line updates the local variable in turn.
  36. '''
  37. new_source_code = self._format_source_with_new_varname('clients_parser', source_code)
  38. # variable of the same name as the one passed into the format_source method.
  39. # Used to hold the eval'd statements
  40. first_line = new_source_code.pop(0)
  41. clients_parser, assignment = self._split_line(first_line)
  42. clients_parser = eval(assignment)
  43. for line in new_source_code:
  44. eval(line)
  45. return clients_parser
  46. def _format_source_with_new_varname(self, variable_name, source):
  47. '''
  48. 'injects' the client code with a known variable name so that it
  49. can be `eval`d and assigned to a variable in the local code.
  50. For example, if the client code was:
  51. parser = ArgumentParser(descrip...)
  52. parser.add_argument("-r", "--re...)
  53. parser.add_argument("-v", "--ve...)
  54. The variable "parser" would be overwritten with a custom name. e.g.
  55. my_parser = ArgumentParser(descrip...)
  56. my_parser.add_argument("-r", "--re...)
  57. '''
  58. source_code = source[:]
  59. first_line = source_code[0]
  60. parser_variable, statement = self._split_line(first_line)
  61. parser_variable = parser_variable.strip()
  62. for index, line in enumerate(source_code):
  63. source_code[index] = line.replace(parser_variable, variable_name)
  64. return source_code
  65. def _split_line(self, line):
  66. # Splits line at the first = sign,
  67. # joins everything after the first =
  68. # to account for additional = signs in
  69. # parameters
  70. components = line.split('=')
  71. var = components.pop(0)
  72. return var, '='.join(components)
  73. def __getattr__(self, attr):
  74. '''
  75. Auto-delegates everything to the ArgumentParser instance'''
  76. return getattr(self._parser_instance, attr)
  77. @staticmethod
  78. def _ErrorAsString(self, msg):
  79. '''
  80. Monkey patch for parser.error
  81. Raises an error rather than
  82. printing and silently exiting.
  83. '''
  84. raise ArgumentError(msg)
  85. if __name__ == '__main__':
  86. pass