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.

206 lines
7.5 KiB

2 years ago
  1. """
  2. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  3. !!!!!!!!!!!DEBUGGING NOTE!!!!!!!!!!!
  4. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  5. PyCharm will inject addition params into stdout when starting
  6. a new process. This can make debugging VERY VERY CONFUSING as
  7. the thing being injected starts complaining about unknown
  8. arguments...
  9. TL;DR: disable the "Attaach to subprocess automatically" option
  10. in the Debugger settings, and the world will be sane again.
  11. See: https://youtrack.jetbrains.com/issue/PY-24929
  12. and: https://www.jetbrains.com/help/pycharm/2017.1/python-debugger.html
  13. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  14. !!!!!!!!!!!DEBUGGING NOTE!!!!!!!!!!!
  15. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  16. """
  17. import json
  18. import os
  19. import sys
  20. import traceback
  21. from argparse import ArgumentParser
  22. from copy import deepcopy
  23. from typing import List, Dict
  24. from gooey.python_bindings.dynamics import monkey_patch_for_form_validation
  25. from gooey.python_bindings.dynamics import patch_argument, collect_errors
  26. from gooey.python_bindings.types import GooeyParams
  27. from gooey.python_bindings.coms import serialize_outbound, decode_payload
  28. from gooey.python_bindings.types import PublicGooeyState
  29. from . import cmd_args
  30. from . import config_generator
  31. def noop(*args, **kwargs):
  32. """
  33. No-op for dev/null-ing handlers which
  34. haven't been specified by the user.
  35. """
  36. return None
  37. def bypass_gooey(params):
  38. """
  39. Bypasses all the Gooey machinery and runs the user's code directly.
  40. """
  41. def parse_args(self: ArgumentParser, args=None, namespace=None):
  42. # We previously mutated sys.argv directly to remove
  43. # the --ignore-gooey flag. But this caused lots of issues
  44. # See: https://github.com/chriskiehl/Gooey/issues/686
  45. # So, we instead modify the parser to transparently
  46. # consume the extra token.
  47. patched_parser = patch_argument(self, '--ignore-gooey', action='store_true')
  48. args = patched_parser.original_parse_args(args, namespace) # type: ignore
  49. # removed from the arg object so the user doesn't have
  50. # to deal with it or be confused by it
  51. del args.ignore_gooey
  52. return args
  53. return parse_args
  54. def boostrap_gooey(params: GooeyParams):
  55. """Bootstraps the Gooey UI."""
  56. def parse_args(self: ArgumentParser, args=None, namespace=None):
  57. # This import is delayed so it is not in the --ignore-gooey codepath.
  58. from gooey.gui import bootstrap
  59. source_path = sys.argv[0]
  60. build_spec = None
  61. if params['load_build_config']:
  62. try:
  63. exec_dir = os.path.dirname(sys.argv[0])
  64. open_path = os.path.join(exec_dir, params['load_build_config']) # type: ignore
  65. build_spec = json.load(open(open_path, "r"))
  66. except Exception as e:
  67. print('Exception loading Build Config from {0}: {1}'.format(params['load_build_config'], e))
  68. sys.exit(1)
  69. if not build_spec:
  70. if params['use_cmd_args']:
  71. cmd_args.parse_cmd_args(self, args)
  72. build_spec = config_generator.create_from_parser(
  73. self,
  74. source_path,
  75. **params)
  76. if params['dump_build_config']:
  77. config_path = os.path.join(os.path.dirname(sys.argv[0]), 'gooey_config.json')
  78. print('Writing Build Config to: {}'.format(config_path))
  79. with open(config_path, 'w') as f:
  80. f.write(json.dumps(build_spec, indent=2))
  81. bootstrap.run(build_spec)
  82. return parse_args
  83. def validate_form(params: GooeyParams, write=print, exit=sys.exit):
  84. """
  85. Validates the user's current form.
  86. """
  87. def merge_errors(state: PublicGooeyState, errors: Dict[str, str]) -> PublicGooeyState:
  88. changes = deepcopy(state['active_form'])
  89. for item in changes:
  90. if item['type'] == 'RadioGroup':
  91. for subitem in item['options']: # type: ignore
  92. subitem['error'] = errors.get(subitem['id'], None)
  93. item['error'] = any(x['error'] for x in item['options']) # type: ignore
  94. else:
  95. item['error'] = errors.get(item['id'], None) # type: ignore
  96. return PublicGooeyState(active_form=changes)
  97. def parse_args(self: ArgumentParser, args=None, namespace=None):
  98. error_registry: Dict[str, Exception] = {}
  99. patched_parser = monkey_patch_for_form_validation(error_registry, self)
  100. try:
  101. args = patched_parser.original_parse_args(args, namespace) # type: ignore
  102. state = args.gooey_state
  103. next_state = merge_errors(state, collect_errors(patched_parser, error_registry, vars(args)))
  104. write(serialize_outbound(next_state))
  105. exit(0)
  106. except Exception as e:
  107. write(e)
  108. exit(1)
  109. return parse_args
  110. def validate_field(params):
  111. def parse_args(self: ArgumentParser, args=None, namespace=None):
  112. raise NotImplementedError
  113. return parse_args
  114. def handle_completed_run(params, write=print, exit=sys.exit):
  115. def parse_args(self: ArgumentParser, args=None, namespace=None):
  116. # because we're running under the context of a successful
  117. # invocation having just completed, the arguments supplied to
  118. # the parser to trigger it are thus, by definition, safe to parse.
  119. # So, we don't need any error patching monkey business and just need
  120. # to attach our specific arg to parse the extra option Gooey passes
  121. patch_argument(self, '--gooey-state', action='store', type=decode_payload)
  122. patch_argument(self, '--gooey-run-is-success', default=False, action='store_true')
  123. patch_argument(self, '--gooey-run-is-failure', default=False, action='store_true')
  124. try:
  125. args = self.original_parse_args(args, namespace) # type: ignore
  126. form_state = args.gooey_state
  127. was_success = args.gooey_run_is_success
  128. # removing the injected gooey value so as not
  129. # to clutter the user's object
  130. del args.gooey_state
  131. del args.gooey_run_is_success
  132. del args.gooey_run_is_failure
  133. if was_success:
  134. next_state = getattr(self, 'on_gooey_success', noop)(args, form_state) # type: ignore
  135. else:
  136. next_state = getattr(self, 'on_gooey_error', noop)(args, form_state) # type: ignore
  137. write(serialize_outbound(next_state))
  138. exit(0)
  139. except Exception as e:
  140. write(''.join(traceback.format_stack()))
  141. write(e)
  142. exit(1)
  143. return parse_args
  144. def handle_error(params):
  145. def parse_args(self: ArgumentParser, args=None, namespace=None):
  146. raise NotImplementedError
  147. return parse_args
  148. def handle_field_update(params):
  149. def parse_args(self: ArgumentParser, args=None, namespace=None):
  150. raise NotImplementedError
  151. return parse_args
  152. def handle_submit(params):
  153. def parse_args(self: ArgumentParser, args=None, namespace=None):
  154. raise NotImplementedError
  155. return parse_args
  156. def choose_hander(params: GooeyParams, cliargs: List[str]):
  157. """
  158. Dispatches to the appropriate handler based on values
  159. found in the CLI arguments
  160. """
  161. with open('tmp.txt', 'w') as f:
  162. f.write(str(sys.argv))
  163. if '--gooey-validate-form' in cliargs:
  164. return validate_form(params)
  165. elif '--gooey-run-is-success' in cliargs or '--gooey-run-is-failure' in cliargs:
  166. return handle_completed_run(params)
  167. elif '--ignore-gooey' in cliargs:
  168. return bypass_gooey(params)
  169. else:
  170. return boostrap_gooey(params)