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.

237 lines
6.7 KiB

  1. """
  2. Converts argparse parser actions into json "Build Specs"
  3. """
  4. import argparse
  5. import os
  6. from argparse import (
  7. _CountAction,
  8. _HelpAction,
  9. _StoreConstAction,
  10. _StoreFalseAction,
  11. _StoreTrueAction,
  12. ArgumentParser,
  13. _SubParsersAction)
  14. from collections import OrderedDict
  15. from functools import partial
  16. from itertools import chain
  17. import sys
  18. VALID_WIDGETS = (
  19. 'FileChooser',
  20. 'MultiFileChooser',
  21. 'FileSaver',
  22. 'DirChooser',
  23. 'DateChooser',
  24. 'TextField',
  25. 'Dropdown',
  26. 'Counter',
  27. 'RadioGroup',
  28. 'CheckBox',
  29. 'MultiDirChooser',
  30. 'Textarea',
  31. 'PasswordField',
  32. 'Listbox'
  33. )
  34. class UnknownWidgetType(Exception):
  35. pass
  36. class UnsupportedConfiguration(Exception):
  37. pass
  38. {
  39. 'siege': {
  40. 'command': 'siege',
  41. 'display_name': 'Siege',
  42. 'contents': []
  43. }
  44. }
  45. def convert(parser):
  46. widget_dict = getattr(parser, 'widgets', {})
  47. actions = parser._actions
  48. if has_subparsers(actions):
  49. if has_required(actions):
  50. raise UnsupportedConfiguration("Gooey doesn't currently support required arguments when subparsers are present.")
  51. layout_type = 'column'
  52. layout_data = OrderedDict(
  53. (choose_name(name, sub_parser), {
  54. 'command': name,
  55. 'contents': process(sub_parser, getattr(sub_parser, 'widgets', {}))
  56. }) for name, sub_parser in get_subparser(actions).choices.items())
  57. else:
  58. layout_type = 'standard'
  59. layout_data = OrderedDict([
  60. ('primary', {
  61. 'command': None,
  62. 'contents': process(parser, widget_dict)
  63. })
  64. ])
  65. return {
  66. 'layout_type': layout_type,
  67. 'widgets': layout_data
  68. }
  69. def process(parser, widget_dict):
  70. mutually_exclusive_groups = [
  71. [mutex_action for mutex_action in group_actions._group_actions]
  72. for group_actions in parser._mutually_exclusive_groups]
  73. group_options = list(chain(*mutually_exclusive_groups))
  74. base_actions = [action for action in parser._actions
  75. if action not in group_options
  76. and action.dest != 'help']
  77. required_actions = filter(is_required, base_actions)
  78. optional_actions = filter(is_optional, base_actions)
  79. return list(categorize(required_actions, widget_dict, required=True)) + \
  80. list(categorize(optional_actions, widget_dict)) + \
  81. list(map(build_radio_group, mutually_exclusive_groups))
  82. def categorize(actions, widget_dict, required=False):
  83. _get_widget = partial(get_widget, widgets=widget_dict)
  84. for action in actions:
  85. if is_standard(action):
  86. yield as_json(action, _get_widget(action) or 'TextField', required)
  87. elif is_choice(action):
  88. yield as_json(action, _get_widget(action) or 'Dropdown', required)
  89. elif is_flag(action):
  90. yield as_json(action, _get_widget(action) or 'CheckBox', required)
  91. elif is_counter(action):
  92. _json = as_json(action, _get_widget(action) or 'Counter', required)
  93. # pre-fill the 'counter' dropdown
  94. _json['data']['choices'] = list(map(str, range(1, 11)))
  95. yield _json
  96. else:
  97. raise UnknownWidgetType(action)
  98. def get_widget(action, widgets):
  99. supplied_widget = widgets.get(action.dest, None)
  100. type_arg_widget = 'FileChooser' if action.type == argparse.FileType else None
  101. return supplied_widget or type_arg_widget or None
  102. def is_required(action):
  103. '''
  104. _actions possessing the `required` flag and not implicitly optional
  105. through `nargs` being '*' or '?'
  106. '''
  107. return not isinstance(action, _SubParsersAction) and (action.required == True and action.nargs not in ['*', '?'])
  108. def has_required(actions):
  109. return list(filter(None, list(filter(is_required, actions))))
  110. def is_subparser(action):
  111. return isinstance(action,_SubParsersAction)
  112. def has_subparsers(actions):
  113. return list(filter(is_subparser, actions))
  114. def get_subparser(actions):
  115. return list(filter(is_subparser, actions))[0]
  116. def is_optional(action):
  117. '''
  118. _actions either not possessing the `required` flag or implicitly optional through `nargs` being '*' or '?'
  119. '''
  120. return (not action.required) or action.nargs in ['*', '?']
  121. def is_choice(action):
  122. ''' action with choices supplied '''
  123. return action.choices
  124. def is_standard(action):
  125. """ actions which are general "store" instructions.
  126. e.g. anything which has an argument style like:
  127. $ script.py -f myfilename.txt
  128. """
  129. boolean_actions = (
  130. _StoreConstAction, _StoreFalseAction,
  131. _StoreTrueAction
  132. )
  133. return (not action.choices
  134. and not isinstance(action, _CountAction)
  135. and not isinstance(action, _HelpAction)
  136. and type(action) not in boolean_actions)
  137. def is_flag(action):
  138. """ _actions which are either storeconst, store_bool, etc.. """
  139. action_types = [_StoreTrueAction, _StoreFalseAction, _StoreConstAction]
  140. return any(list(map(lambda Action: isinstance(action, Action), action_types)))
  141. def is_counter(action):
  142. """ _actions which are of type _CountAction """
  143. return isinstance(action, _CountAction)
  144. def is_default_progname(name, subparser):
  145. return subparser.prog == '{} {}'.format(os.path.split(sys.argv[0])[-1], name)
  146. def choose_name(name, subparser):
  147. return name if is_default_progname(name, subparser) else subparser.prog
  148. def build_radio_group(mutex_group):
  149. if not mutex_group:
  150. return []
  151. options = [
  152. {
  153. 'display_name': mutex_arg.metavar or mutex_arg.dest,
  154. 'help': mutex_arg.help,
  155. 'nargs': mutex_arg.nargs or '',
  156. 'commands': mutex_arg.option_strings,
  157. 'choices': mutex_arg.choices,
  158. } for mutex_arg in mutex_group
  159. ]
  160. return {
  161. 'type': 'RadioGroup',
  162. 'group_name': 'Choose Option',
  163. 'required': False,
  164. 'data': options
  165. }
  166. def as_json(action, widget, required):
  167. if widget not in VALID_WIDGETS:
  168. raise UnknownWidgetType('Widget Type {0} is unrecognized'.format(widget))
  169. return {
  170. 'type': widget,
  171. 'required': required,
  172. 'data': {
  173. 'display_name': action.metavar or action.dest,
  174. 'help': action.help,
  175. 'nargs': action.nargs or '',
  176. 'commands': action.option_strings,
  177. 'choices': action.choices or [],
  178. 'default': clean_default(widget, action.default)
  179. }
  180. }
  181. def clean_default(widget_type, default):
  182. '''
  183. Attemps to safely coalesce the default value down to
  184. a valid JSON type.
  185. See: Issue #147.
  186. function references supplied as arguments to the
  187. `default` parameter in Argparse cause errors in Gooey.
  188. '''
  189. if widget_type != 'CheckBox':
  190. return default.__name__ if callable(default) else default
  191. # checkboxes must be handled differently, as they
  192. # must be forced down to a boolean value
  193. return default if isinstance(default, bool) else False