TclCommand.py 12 KB

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