TclCommand.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444
  1. import sys
  2. import re
  3. import FlatCAMApp
  4. import abc
  5. import collections
  6. from PyQt5 import QtCore, QtGui
  7. from PyQt5.QtCore import Qt
  8. from contextlib import contextmanager
  9. class TclCommand(object):
  10. # FlatCAMApp
  11. app = None
  12. # Logger
  13. log = None
  14. # List of all command aliases, to be able use old names
  15. # for backward compatibility (add_poly, add_polygon)
  16. aliases = []
  17. # Dictionary of types from Tcl command, needs to be ordered
  18. # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
  19. arg_names = collections.OrderedDict([
  20. ('name', str)
  21. ])
  22. # dictionary of types from Tcl command, needs to be ordered.
  23. # This is for options like -optionname value.
  24. # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
  25. option_types = collections.OrderedDict()
  26. # List of mandatory options for current Tcl command: required = {'name','outname'}
  27. required = ['name']
  28. # Structured help for current command, args needs to be ordered
  29. # OrderedDict should be like collections.OrderedDict([(key,value),(key2,value2)])
  30. help = {
  31. 'main': "undefined help.",
  32. 'args': collections.OrderedDict([
  33. ('argumentname', 'undefined help.'),
  34. ('optionname', 'undefined help.')
  35. ]),
  36. 'examples': []
  37. }
  38. # Original incoming arguments into command
  39. original_args = None
  40. def __init__(self, app):
  41. self.app = app
  42. if self.app is None:
  43. raise TypeError('Expected app to be FlatCAMApp instance.')
  44. if not isinstance(self.app, FlatCAMApp.App):
  45. raise TypeError('Expected FlatCAMApp, got %s.' % type(app))
  46. self.log = self.app.log
  47. self.error_info = None
  48. self.error = None
  49. def raise_tcl_error(self, text):
  50. """
  51. This method pass exception from python into TCL as error
  52. so we get stacktrace and reason.
  53. This is only redirect to self.app.raise_tcl_error
  54. :param text: text of error
  55. :return: none
  56. """
  57. self.app.raise_tcl_error(text)
  58. def get_current_command(self):
  59. """
  60. Get current command, we are not able to get it from TCL we have to reconstruct it.
  61. :return: current command
  62. """
  63. command_string = []
  64. command_string.append(self.aliases[0])
  65. if self.original_args is not None:
  66. for arg in self.original_args:
  67. command_string.append(arg)
  68. return " ".join(command_string)
  69. def get_decorated_help(self):
  70. """
  71. Decorate help for TCL console output.
  72. :return: decorated help from structure
  73. """
  74. def get_decorated_command(alias_name):
  75. command_string = []
  76. for arg_key, arg_type in list(self.help['args'].items()):
  77. command_string.append(get_decorated_argument(arg_key, arg_type, True))
  78. return "> " + alias_name + " " + " ".join(command_string)
  79. def get_decorated_argument(help_key, help_text, in_command=False):
  80. """
  81. :param help_key: Name of the argument.
  82. :param help_text:
  83. :param in_command:
  84. :return:
  85. """
  86. option_symbol = ''
  87. if help_key in self.arg_names:
  88. arg_type = self.arg_names[help_key]
  89. type_name = str(arg_type.__name__)
  90. # in_command_name = help_key + "<" + type_name + ">"
  91. in_command_name = help_key
  92. elif help_key in self.option_types:
  93. option_symbol = '-'
  94. arg_type = self.option_types[help_key]
  95. type_name = str(arg_type.__name__)
  96. in_command_name = option_symbol + help_key + " <" + type_name + ">"
  97. else:
  98. option_symbol = ''
  99. type_name = '?'
  100. in_command_name = option_symbol + help_key + " <" + type_name + ">"
  101. if in_command:
  102. if help_key in self.required:
  103. return in_command_name
  104. else:
  105. return '[' + in_command_name + "]"
  106. else:
  107. if help_key in self.required:
  108. return "\t" + option_symbol + help_key + " <" + type_name + ">: " + help_text
  109. else:
  110. return "\t[" + option_symbol + help_key + " <" + type_name + ">: " + help_text + "]"
  111. def get_decorated_example(example_item):
  112. return "> " + example_item
  113. help_string = [self.help['main']]
  114. for alias in self.aliases:
  115. help_string.append(get_decorated_command(alias))
  116. for key, value in list(self.help['args'].items()):
  117. help_string.append(get_decorated_argument(key, value))
  118. # timeout is unique for signaled commands (this is not best oop practice, but much easier for now)
  119. if isinstance(self, TclCommandSignaled):
  120. help_string.append("\t[-timeout <int>: Max wait for job timeout before error.]")
  121. for example in self.help['examples']:
  122. help_string.append(get_decorated_example(example))
  123. return "\n".join(help_string)
  124. @staticmethod
  125. def parse_arguments(args):
  126. """
  127. Pre-processes arguments to detect '-keyword value' pairs into dictionary
  128. and standalone parameters into list.
  129. This is copy from FlatCAMApp.setup_shell().h() just for accessibility,
  130. original should be removed after all commands will be converted
  131. :param args: arguments from tcl to parse
  132. :return: arguments, options
  133. """
  134. options = {}
  135. arguments = []
  136. n = len(args)
  137. option_name = None
  138. for i in range(n):
  139. match = re.search(r'^-([a-zA-Z].*)', args[i])
  140. if match:
  141. # assert option_name is None
  142. if option_name is not None:
  143. options[option_name] = None
  144. option_name = match.group(1)
  145. continue
  146. if option_name is None:
  147. arguments.append(args[i])
  148. else:
  149. options[option_name] = args[i]
  150. option_name = None
  151. if option_name is not None:
  152. options[option_name] = None
  153. return arguments, options
  154. def check_args(self, args):
  155. """
  156. Check arguments and options for right types
  157. :param args: arguments from tcl to check
  158. :return: named_args, unnamed_args
  159. """
  160. arguments, options = self.parse_arguments(args)
  161. named_args = {}
  162. unnamed_args = []
  163. # check arguments
  164. idx = 0
  165. arg_names_items = list(self.arg_names.items())
  166. for argument in arguments:
  167. if len(self.arg_names) > idx:
  168. key, arg_type = arg_names_items[idx]
  169. try:
  170. named_args[key] = arg_type(argument)
  171. except Exception as e:
  172. self.raise_tcl_error("Cannot cast named argument '%s' to type %s with exception '%s'."
  173. % (key, arg_type, str(e)))
  174. else:
  175. unnamed_args.append(argument)
  176. idx += 1
  177. # check options
  178. for key in options:
  179. if key not in self.option_types and key != 'timeout':
  180. self.raise_tcl_error('Unknown parameter: %s' % key)
  181. try:
  182. if key != 'timeout':
  183. # None options are allowed; if None then the defaults are used
  184. # - must be implemented in the Tcl commands
  185. if options[key] is not None:
  186. named_args[key] = self.option_types[key](options[key])
  187. else:
  188. named_args[key] = options[key]
  189. else:
  190. named_args[key] = int(options[key])
  191. except Exception as e:
  192. self.raise_tcl_error("Cannot cast argument '-%s' to type '%s' with exception '%s'."
  193. % (key, self.option_types[key], str(e)))
  194. # check required arguments
  195. for key in self.required:
  196. if key not in named_args:
  197. self.raise_tcl_error("Missing required argument '%s'." % key)
  198. return named_args, unnamed_args
  199. def raise_tcl_unknown_error(self, unknown_exception):
  200. """
  201. raise Exception if is different type than TclErrorException
  202. this is here mainly to show unknown errors inside TCL shell console
  203. :param unknown_exception:
  204. :return:
  205. """
  206. raise unknown_exception
  207. def raise_tcl_error(self, text):
  208. """
  209. this method pass exception from python into TCL as error, so we get stacktrace and reason
  210. :param text: text of error
  211. :return: raise exception
  212. """
  213. # because of signaling we cannot call error to TCL from here but when task
  214. # is finished also non-signaled are handled here to better exception
  215. # handling and displayed after command is finished
  216. raise self.app.TclErrorException(text)
  217. def execute_wrapper(self, *args):
  218. """
  219. Command which is called by tcl console when current commands aliases are hit.
  220. Main catch(except) is implemented here.
  221. This method should be reimplemented only when initial checking sequence differs
  222. :param args: arguments passed from tcl command console
  223. :return: None, output text or exception
  224. """
  225. # self.worker_task.emit({'fcn': self.exec_command_test, 'params': [text, False]})
  226. try:
  227. self.log.debug("TCL command '%s' executed." % str(self.__class__))
  228. self.original_args = args
  229. args, unnamed_args = self.check_args(args)
  230. return self.execute(args, unnamed_args)
  231. except Exception as unknown:
  232. error_info = sys.exc_info()
  233. self.log.error("TCL command '%s' failed. Error text: %s" % (str(self), str(unknown)))
  234. self.app.display_tcl_error(unknown, error_info)
  235. self.raise_tcl_unknown_error(unknown)
  236. @abc.abstractmethod
  237. def execute(self, args, unnamed_args):
  238. """
  239. Direct execute of command, this method should be implemented in each descendant.
  240. No main catch should be implemented here.
  241. :param args: array of known named arguments and options
  242. :param unnamed_args: array of other values which were passed into command
  243. without -somename and we do not have them in known arg_names
  244. :return: None, output text or exception
  245. """
  246. raise NotImplementedError("Please Implement this method")
  247. class TclCommandSignaled(TclCommand):
  248. """
  249. !!! I left it here only for demonstration !!!
  250. Go to TclCommandCncjob and into class definition put
  251. class TclCommandCncjob(TclCommandSignaled):
  252. also change
  253. obj.generatecncjob(use_thread = False, **args)
  254. to
  255. obj.generatecncjob(use_thread = True, **args)
  256. This class is child of TclCommand and is used for commands which create new objects
  257. it handles all necessary stuff about blocking and passing exceptions
  258. """
  259. @abc.abstractmethod
  260. def execute(self, args, unnamed_args):
  261. raise NotImplementedError("Please Implement this method")
  262. output = None
  263. def execute_call(self, args, unnamed_args):
  264. try:
  265. self.output = None
  266. self.error = None
  267. self.error_info = None
  268. self.output = self.execute(args, unnamed_args)
  269. except Exception as unknown:
  270. self.error_info = sys.exc_info()
  271. self.error = unknown
  272. finally:
  273. self.app.shell_command_finished.emit(self)
  274. def execute_wrapper(self, *args):
  275. """
  276. Command which is called by tcl console when current commands aliases are hit.
  277. Main catch(except) is implemented here.
  278. This method should be reimplemented only when initial checking sequence differs
  279. :param args: arguments passed from tcl command console
  280. :return: None, output text or exception
  281. """
  282. @contextmanager
  283. def wait_signal(signal, timeout=300000):
  284. """Block loop until signal emitted, or timeout (ms) elapses."""
  285. loop = QtCore.QEventLoop()
  286. # Normal termination
  287. signal.connect(loop.quit)
  288. # Termination by exception in thread
  289. self.app.thread_exception.connect(loop.quit)
  290. status = {'timed_out': False}
  291. def report_quit():
  292. status['timed_out'] = True
  293. loop.quit()
  294. yield
  295. # Temporarily change how exceptions are managed.
  296. oeh = sys.excepthook
  297. ex = []
  298. def except_hook(type_, value, traceback_):
  299. ex.append(value)
  300. oeh(type_, value, traceback_)
  301. sys.excepthook = except_hook
  302. # Terminate on timeout
  303. if timeout is not None:
  304. time_val = int(timeout)
  305. QtCore.QTimer.singleShot(time_val, report_quit)
  306. # Block
  307. loop.exec_()
  308. # Restore exception management
  309. sys.excepthook = oeh
  310. if ex:
  311. raise ex[0]
  312. if status['timed_out']:
  313. self.app.raise_tcl_unknown_error("Operation timed outed! Consider increasing option "
  314. "'-timeout <miliseconds>' for command or "
  315. "'set_sys global_background_timeout <miliseconds>'.")
  316. try:
  317. self.log.debug("TCL command '%s' executed." % str(self.__class__))
  318. self.original_args = args
  319. args, unnamed_args = self.check_args(args)
  320. if 'timeout' in args:
  321. passed_timeout = args['timeout']
  322. del args['timeout']
  323. else:
  324. passed_timeout = self.app.defaults['global_background_timeout']
  325. # set detail for processing, it will be there until next open or close
  326. self.app.shell.open_processing(self.get_current_command())
  327. def handle_finished(obj):
  328. self.app.shell_command_finished.disconnect(handle_finished)
  329. if self.error is not None:
  330. self.raise_tcl_unknown_error(self.error)
  331. self.app.shell_command_finished.connect(handle_finished)
  332. with wait_signal(self.app.shell_command_finished, passed_timeout):
  333. # every TclCommandNewObject ancestor support timeout as parameter,
  334. # but it does not mean anything for child itself
  335. # when operation will be really long is good to set it higher then defqault 30s
  336. self.app.worker_task.emit({'fcn': self.execute_call, 'params': [args, unnamed_args]})
  337. return self.output
  338. except Exception as unknown:
  339. # if error happens inside thread execution, then pass correct error_info to display
  340. if self.error_info is not None:
  341. error_info = self.error_info
  342. else:
  343. error_info = sys.exc_info()
  344. self.log.error("TCL command '%s' failed." % str(self))
  345. self.app.display_tcl_error(unknown, error_info)
  346. self.raise_tcl_unknown_error(unknown)