TclCommand.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import re
  2. import FlatCAMApp
  3. import abc
  4. import collections
  5. from PyQt4 import QtCore
  6. from contextlib import contextmanager
  7. class TclCommand(object):
  8. # FlatCAMApp
  9. app = None
  10. # logger
  11. log = None
  12. # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
  13. aliases = []
  14. # dictionary of types from Tcl command, needs to be ordered
  15. # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
  16. arg_names = collections.OrderedDict([
  17. ('name', str)
  18. ])
  19. # dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
  20. # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
  21. option_types = collections.OrderedDict()
  22. # array of mandatory options for current Tcl command: required = {'name','outname'}
  23. required = ['name']
  24. # structured help for current command, args needs to be ordered
  25. # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
  26. help = {
  27. 'main': "undefined help.",
  28. 'args': collections.OrderedDict([
  29. ('argumentname', 'undefined help.'),
  30. ('optionname', 'undefined help.')
  31. ]),
  32. 'examples': []
  33. }
  34. def __init__(self, app):
  35. self.app = app
  36. if self.app is None:
  37. raise TypeError('Expected app to be FlatCAMApp instance.')
  38. if not isinstance(self.app, FlatCAMApp.App):
  39. raise TypeError('Expected FlatCAMApp, got %s.' % type(app))
  40. self.log = self.app.log
  41. def raise_tcl_error(self, text):
  42. """
  43. this method pass exception from python into TCL as error, so we get stacktrace and reason
  44. this is only redirect to self.app.raise_tcl_error
  45. :param text: text of error
  46. :return: none
  47. """
  48. self.app.raise_tcl_error(text)
  49. def get_decorated_help(self):
  50. """
  51. Decorate help for TCL console output.
  52. :return: decorated help from structure
  53. """
  54. def get_decorated_command(alias_name):
  55. command_string = []
  56. for arg_key, arg_type in self.help['args'].items():
  57. command_string.append(get_decorated_argument(arg_key, arg_type, True))
  58. return "> " + alias_name + " " + " ".join(command_string)
  59. def get_decorated_argument(help_key, help_text, in_command=False):
  60. option_symbol = ''
  61. if help_key in self.arg_names:
  62. arg_type = self.arg_names[help_key]
  63. type_name = str(arg_type.__name__)
  64. in_command_name = "<" + type_name + ">"
  65. elif help_key in self.option_types:
  66. option_symbol = '-'
  67. arg_type = self.option_types[help_key]
  68. type_name = str(arg_type.__name__)
  69. in_command_name = option_symbol + help_key + " <" + type_name + ">"
  70. else:
  71. option_symbol = ''
  72. type_name = '?'
  73. in_command_name = option_symbol + help_key + " <" + type_name + ">"
  74. if in_command:
  75. if help_key in self.required:
  76. return in_command_name
  77. else:
  78. return '[' + in_command_name + "]"
  79. else:
  80. if help_key in self.required:
  81. return "\t" + option_symbol + help_key + " <" + type_name + ">: " + help_text
  82. else:
  83. return "\t[" + option_symbol + help_key + " <" + type_name + ">: " + help_text + "]"
  84. def get_decorated_example(example_item):
  85. return "> "+example_item
  86. help_string = [self.help['main']]
  87. for alias in self.aliases:
  88. help_string.append(get_decorated_command(alias))
  89. for key, value in self.help['args'].items():
  90. help_string.append(get_decorated_argument(key, value))
  91. for example in self.help['examples']:
  92. help_string.append(get_decorated_example(example))
  93. return "\n".join(help_string)
  94. @staticmethod
  95. def parse_arguments(args):
  96. """
  97. Pre-processes arguments to detect '-keyword value' pairs into dictionary
  98. and standalone parameters into list.
  99. This is copy from FlatCAMApp.setup_shell().h() just for accessibility,
  100. original should be removed after all commands will be converted
  101. :param args: arguments from tcl to parse
  102. :return: arguments, options
  103. """
  104. options = {}
  105. arguments = []
  106. n = len(args)
  107. name = None
  108. for i in range(n):
  109. match = re.search(r'^-([a-zA-Z].*)', args[i])
  110. if match:
  111. assert name is None
  112. name = match.group(1)
  113. continue
  114. if name is None:
  115. arguments.append(args[i])
  116. else:
  117. options[name] = args[i]
  118. name = None
  119. return arguments, options
  120. def check_args(self, args):
  121. """
  122. Check arguments and options for right types
  123. :param args: arguments from tcl to check
  124. :return: named_args, unnamed_args
  125. """
  126. arguments, options = self.parse_arguments(args)
  127. named_args = {}
  128. unnamed_args = []
  129. # check arguments
  130. idx = 0
  131. arg_names_items = self.arg_names.items()
  132. for argument in arguments:
  133. if len(self.arg_names) > idx:
  134. key, arg_type = arg_names_items[idx]
  135. try:
  136. named_args[key] = arg_type(argument)
  137. except Exception, e:
  138. self.raise_tcl_error("Cannot cast named argument '%s' to type %s with exception '%s'."
  139. % (key, arg_type, str(e)))
  140. else:
  141. unnamed_args.append(argument)
  142. idx += 1
  143. # check options
  144. for key in options:
  145. if key not in self.option_types:
  146. self.raise_tcl_error('Unknown parameter: %s' % key)
  147. try:
  148. named_args[key] = self.option_types[key](options[key])
  149. except Exception, e:
  150. self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'."
  151. % (key, self.option_types[key], str(e)))
  152. # check required arguments
  153. for key in self.required:
  154. if key not in named_args:
  155. self.raise_tcl_error("Missing required argument '%s'." % key)
  156. return named_args, unnamed_args
  157. def execute_wrapper(self, *args):
  158. """
  159. Command which is called by tcl console when current commands aliases are hit.
  160. Main catch(except) is implemented here.
  161. This method should be reimplemented only when initial checking sequence differs
  162. :param args: arguments passed from tcl command console
  163. :return: None, output text or exception
  164. """
  165. try:
  166. self.log.debug("TCL command '%s' executed." % str(self.__class__))
  167. args, unnamed_args = self.check_args(args)
  168. return self.execute(args, unnamed_args)
  169. except Exception as unknown:
  170. self.log.error("TCL command '%s' failed." % str(self))
  171. self.app.raise_tcl_unknown_error(unknown)
  172. @abc.abstractmethod
  173. def execute(self, args, unnamed_args):
  174. """
  175. Direct execute of command, this method should be implemented in each descendant.
  176. No main catch should be implemented here.
  177. :param args: array of known named arguments and options
  178. :param unnamed_args: array of other values which were passed into command
  179. without -somename and we do not have them in known arg_names
  180. :return: None, output text or exception
  181. """
  182. raise NotImplementedError("Please Implement this method")
  183. class TclCommandSignaled(TclCommand):
  184. """
  185. !!! I left it here only for demonstration !!!
  186. Go to TclCommandCncjob and into class definition put
  187. class TclCommandCncjob(TclCommand.TclCommandSignaled):
  188. also change
  189. obj.generatecncjob(use_thread = False, **args)
  190. to
  191. obj.generatecncjob(use_thread = True, **args)
  192. This class is child of TclCommand and is used for commands which create new objects
  193. it handles all neccessary stuff about blocking and passing exeptions
  194. """
  195. # default timeout for operation is 30 sec, but it can be much more
  196. default_timeout = 30000
  197. def execute_wrapper(self, *args):
  198. """
  199. Command which is called by tcl console when current commands aliases are hit.
  200. Main catch(except) is implemented here.
  201. This method should be reimplemented only when initial checking sequence differs
  202. :param args: arguments passed from tcl command console
  203. :return: None, output text or exception
  204. """
  205. @contextmanager
  206. def wait_signal(signal, timeout=30000):
  207. """Block loop until signal emitted, or timeout (ms) elapses."""
  208. loop = QtCore.QEventLoop()
  209. signal.connect(loop.quit)
  210. status = {'timed_out': False}
  211. def report_quit():
  212. status['timed_out'] = True
  213. loop.quit()
  214. yield
  215. if timeout is not None:
  216. QtCore.QTimer.singleShot(timeout, report_quit)
  217. loop.exec_()
  218. if status['timed_out']:
  219. self.app.raise_tcl_unknown_error('Operation timed out!')
  220. try:
  221. self.log.debug("TCL command '%s' executed." % str(self.__class__))
  222. args, unnamed_args = self.check_args(args)
  223. if 'timeout' in args:
  224. passed_timeout=args['timeout']
  225. del args['timeout']
  226. else:
  227. passed_timeout=self.default_timeout
  228. with wait_signal(self.app.new_object_available, passed_timeout):
  229. # every TclCommandNewObject ancestor support timeout as parameter,
  230. # but it does not mean anything for child itself
  231. # when operation will be really long is good to set it higher then defqault 30s
  232. return self.execute(args, unnamed_args)
  233. except Exception as unknown:
  234. self.log.error("TCL command '%s' failed." % str(self))
  235. self.app.raise_tcl_unknown_error(unknown)