- finished the moving of all Tcl Shell stuff out of the FlatCAAMApp class to flatcamTools.ToolShell class

- updated the requirements.txt file to request that the Shapely package needs to be at least version 1.7.0 as it is needed in the latest versions of FlatCAM beta
- some TOOD cleanups
- minor changes
This commit is contained in:
Marius Stanciu
2020-04-27 06:55:08 +03:00
committed by Marius
parent c7bd395368
commit 61020e3624
9 changed files with 268 additions and 386 deletions

View File

@@ -13,8 +13,10 @@ from PyQt5.QtWidgets import QVBoxLayout, QWidget
from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
import html
import sys
import traceback
import tkinter as tk
import tclCommands
import gettext
import FlatCAMTranslation as fcTranslate
@@ -110,7 +112,7 @@ class TermWidget(QWidget):
elif style == 'err':
text = '<span style="font-weight: bold; color: red;">%s</span>'\
'<span style="font-weight: bold;">%s</span>'\
%(mtype, body)
% (mtype, body)
elif style == 'warning':
# text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' % text
text = '<span style="font-weight: bold; color: #f4b642;">%s</span>' \
@@ -253,15 +255,90 @@ class TermWidget(QWidget):
class FCShell(TermWidget):
def __init__(self, sysShell, version, *args):
def __init__(self, app, version, *args):
"""
Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
:param sysShell: When instantiated the sysShell will be actually the FlatCAMApp.App() class
:param app: When instantiated the sysShell will be actually the FlatCAMApp.App() class
:param version: FlatCAM version string
:param args: Parameters passed to the TermWidget parent class
"""
TermWidget.__init__(self, version, *args, app=sysShell)
self._sysShell = sysShell
TermWidget.__init__(self, version, *args, app=app)
self.app = app
self.tcl_commands_storage = {}
if hasattr(self, 'tcl') and self.tcl is not None:
# self.tcl = None
# new object cannot be used here as it will not remember values created for next passes,
# because tcl was executed in old instance of TCL
pass
else:
self.tcl = tk.Tcl()
self.setup_shell()
self._edit.set_model_data(self.app.myKeywords)
self.setWindowIcon(self.app.ui.app_icon)
self.setWindowTitle("FlatCAM Shell")
self.resize(*self.app.defaults["global_shell_shape"])
self._append_to_browser('in', "FlatCAM %s - " % version)
self.append_output('%s\n\n' % _("Type >help< to get started"))
def setup_shell(self):
"""
Creates shell functions. Runs once at startup.
:return: None
"""
'''
How to implement TCL shell commands:
All parameters passed to command should be possible to set as None and test it afterwards.
This is because we need to see error caused in tcl,
if None value as default parameter is not allowed TCL will return empty error.
Use:
def mycommand(name=None,...):
Test it like this:
if name is None:
self.raise_tcl_error('Argument name is missing.')
When error occurred, always use raise_tcl_error, never return "some text" on error,
otherwise we will miss it and processing will silently continue.
Method raise_tcl_error pass error into TCL interpreter, then raise python exception,
which is caught in exec_command and displayed in TCL shell console with red background.
Error in console is displayed with TCL trace.
This behavior works only within main thread,
errors with promissed tasks can be catched and detected only with log.
TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for
TCL shell.
Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
'''
# Import/overwrite tcl commands as objects of TclCommand descendants
# This modifies the variable 'self.tcl_commands_storage'.
tclCommands.register_all_commands(self.app, self.tcl_commands_storage)
# Add commands to the tcl interpreter
for cmd in self.tcl_commands_storage:
self.tcl.createcommand(cmd, self.tcl_commands_storage[cmd]['fcn'])
# Make the tcl puts function return instead of print to stdout
self.tcl.eval('''
rename puts original_puts
proc puts {args} {
if {[llength $args] == 1} {
return "[lindex $args 0]"
} else {
eval original_puts $args
}
}
''')
def is_command_complete(self, text):
def skipQuotes(txt):
@@ -293,7 +370,7 @@ class FCShell(TermWidget):
:return: output if there was any
"""
self._sysShell.report_usage('exec_command')
self.app.report_usage('exec_command')
return self.exec_command_test(text, False, no_echo=no_echo)
@@ -315,15 +392,15 @@ class FCShell(TermWidget):
if no_echo is False:
self.open_processing() # Disables input box.
result = self._sysShell.tcl.eval(str(tcl_command_string))
result = self.tcl.eval(str(tcl_command_string))
if result != 'None' and no_echo is False:
self.append_output(result + '\n')
except tk.TclError as e:
# This will display more precise answer if something in TCL shell fails
result = self._sysShell.tcl.eval("set errorInfo")
self._sysShell.log.error("Exec command Exception: %s" % (result + '\n'))
result = self.tcl.eval("set errorInfo")
self.app.log.error("Exec command Exception: %s" % (result + '\n'))
if no_echo is False:
self.append_error('ERROR: ' + result + '\n')
# Show error in console and just return or in test raise exception
@@ -335,39 +412,101 @@ class FCShell(TermWidget):
pass
return result
# """
# Code below is unsused. Saved for later.
# """
def raise_tcl_unknown_error(self, unknownException):
"""
Raise exception if is different type than TclErrorException
this is here mainly to show unknown errors inside TCL shell console.
# parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
# parts = [p.replace('\n', '').replace('"', '') for p in parts]
# self.log.debug(parts)
# try:
# if parts[0] not in commands:
# self.shell.append_error("Unknown command\n")
# return
#
# #import inspect
# #inspect.getargspec(someMethod)
# if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
# (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
# self.shell.append_error(
# "Command %s takes %d arguments. %d given.\n" %
# (parts[0], commands[parts[0]]["params"], len(parts)-1)
# )
# return
#
# cmdfcn = commands[parts[0]]["fcn"]
# cmdconv = commands[parts[0]]["converters"]
# if len(parts) - 1 > 0:
# retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)])
# else:
# retval = cmdfcn()
# retfcn = commands[parts[0]]["retfcn"]
# if retval and retfcn(retval):
# self.shell.append_output(retfcn(retval) + "\n")
#
# except Exception as e:
# #self.shell.append_error(''.join(traceback.format_exc()))
# #self.shell.append_error("?\n")
# self.shell.append_error(str(e) + "\n")
:param unknownException:
:return:
"""
if not isinstance(unknownException, self.TclErrorException):
self.raise_tcl_error("Unknown error: %s" % str(unknownException))
else:
raise unknownException
def display_tcl_error(self, error, error_info=None):
"""
Escape bracket [ with '\' otherwise there is error
"ERROR: missing close-bracket" instead of real error
:param error: it may be text or exception
:param error_info: Some informations about the error
:return: None
"""
if isinstance(error, Exception):
exc_type, exc_value, exc_traceback = error_info
if not isinstance(error, self.TclErrorException):
show_trace = 1
else:
show_trace = int(self.app.defaults['global_verbose_error_level'])
if show_trace > 0:
trc = traceback.format_list(traceback.extract_tb(exc_traceback))
trc_formated = []
for a in reversed(trc):
trc_formated.append(a.replace(" ", " > ").replace("\n", ""))
text = "%s\nPython traceback: %s\n%s" % (exc_value, exc_type, "\n".join(trc_formated))
else:
text = "%s" % error
else:
text = error
text = text.replace('[', '\\[').replace('"', '\\"')
self.tcl.eval('return -code error "%s"' % text)
def raise_tcl_error(self, text):
"""
This method pass exception from python into TCL as error, so we get stacktrace and reason
:param text: text of error
:return: raise exception
"""
self.display_tcl_error(text)
raise self.TclErrorException(text)
class TclErrorException(Exception):
"""
this exception is defined here, to be able catch it if we successfully handle all errors from shell command
"""
pass
# """
# Code below is unsused. Saved for later.
# """
# parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
# parts = [p.replace('\n', '').replace('"', '') for p in parts]
# self.log.debug(parts)
# try:
# if parts[0] not in commands:
# self.shell.append_error("Unknown command\n")
# return
#
# #import inspect
# #inspect.getargspec(someMethod)
# if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
# (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
# self.shell.append_error(
# "Command %s takes %d arguments. %d given.\n" %
# (parts[0], commands[parts[0]]["params"], len(parts)-1)
# )
# return
#
# cmdfcn = commands[parts[0]]["fcn"]
# cmdconv = commands[parts[0]]["converters"]
# if len(parts) - 1 > 0:
# retval = cmdfcn(*[cmdconv[i](parts[i + 1]) for i in range(len(parts)-1)])
# else:
# retval = cmdfcn()
# retfcn = commands[parts[0]]["retfcn"]
# if retval and retfcn(retval):
# self.shell.append_output(retfcn(retval) + "\n")
#
# except Exception as e:
# #self.shell.append_error(''.join(traceback.format_exc()))
# #self.shell.append_error("?\n")
# self.shell.append_error(str(e) + "\n")