- refactored some of the code in the App class and created a new Tcl Command named Help
This commit is contained in:
parent
66d9ddd402
commit
a1499158c2
|
@ -12,6 +12,7 @@ CHANGELOG for FlatCAM beta
|
|||
- added a new feature, project auto-saving controlled from Edit -> Preferences -> General -> APP. Preferences -> Enable Auto Save checkbox
|
||||
- fixed some bugs in the Tcl Commands
|
||||
- modified the Tcl Commands to be able to use as boolean values keywords with lower case like 'false' instead of expected 'False'
|
||||
- refactored some of the code in the App class and created a new Tcl Command named Help
|
||||
|
||||
20.04.2020
|
||||
|
||||
|
|
737
FlatCAMApp.py
737
FlatCAMApp.py
|
@ -2556,6 +2556,9 @@ class App(QtCore.QObject):
|
|||
# this will hold the TCL instance
|
||||
self.tcl = None
|
||||
|
||||
# the actual variable will be redeclared in setup_tcl()
|
||||
self.tcl_commands_storage = None
|
||||
|
||||
self.init_tcl()
|
||||
|
||||
self.shell = FCShell(self, version=self.version)
|
||||
|
@ -2566,14 +2569,7 @@ class App(QtCore.QObject):
|
|||
self.shell.append_output("FlatCAM %s - " % self.version)
|
||||
self.shell.append_output(_("Type >help< to get started\n\n"))
|
||||
|
||||
self.ui.shell_dock = QtWidgets.QDockWidget("FlatCAM TCL Shell")
|
||||
self.ui.shell_dock.setObjectName('Shell_DockWidget')
|
||||
self.ui.shell_dock.setWidget(self.shell)
|
||||
self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas)
|
||||
self.ui.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
|
||||
QtWidgets.QDockWidget.DockWidgetFloatable |
|
||||
QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock)
|
||||
|
||||
# show TCL shell at start-up based on the Menu -? Edit -> Preferences setting.
|
||||
if self.defaults["global_shell_at_startup"]:
|
||||
|
@ -2883,7 +2879,7 @@ class App(QtCore.QObject):
|
|||
# if there are Windows paths then replace the path separator with a Unix like one
|
||||
if sys.platform == 'win32':
|
||||
command_tcl_formatted = command_tcl_formatted.replace('\\', '/')
|
||||
self.shell._sysShell.exec_command(command_tcl_formatted, no_echo=True)
|
||||
self.shell.exec_command(command_tcl_formatted, no_echo=True)
|
||||
except Exception as ext:
|
||||
print("ERROR: ", ext)
|
||||
sys.exit(2)
|
||||
|
@ -2903,9 +2899,9 @@ class App(QtCore.QObject):
|
|||
# color=QtGui.QColor("gray"))
|
||||
cmd_line_shellfile_text = myfile.read()
|
||||
if self.cmd_line_headless != 1:
|
||||
self.shell._sysShell.exec_command(cmd_line_shellfile_text)
|
||||
self.shell.exec_command(cmd_line_shellfile_text)
|
||||
else:
|
||||
self.shell._sysShell.exec_command(cmd_line_shellfile_text, no_echo=True)
|
||||
self.shell.exec_command(cmd_line_shellfile_text, no_echo=True)
|
||||
|
||||
except Exception as ext:
|
||||
print("ERROR: ", ext)
|
||||
|
@ -3703,209 +3699,6 @@ class App(QtCore.QObject):
|
|||
else:
|
||||
self.defaults['global_stats'][resource] = 1
|
||||
|
||||
def init_tcl(self):
|
||||
"""
|
||||
Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
|
||||
:return: None
|
||||
"""
|
||||
if hasattr(self, 'tcl') and self.tcl is not None:
|
||||
# self.tcl = None
|
||||
# TODO we need to clean non default variables and procedures here
|
||||
# new object cannot be used here as it will not remember values created for next passes,
|
||||
# because tcl was execudted in old instance of TCL
|
||||
pass
|
||||
else:
|
||||
self.tcl = tk.Tcl()
|
||||
self.setup_shell()
|
||||
self.log.debug("TCL Shell has been initialized.")
|
||||
|
||||
# TODO: This shouldn't be here.
|
||||
class TclErrorException(Exception):
|
||||
"""
|
||||
this exception is defined here, to be able catch it if we successfully handle all errors from shell command
|
||||
"""
|
||||
pass
|
||||
|
||||
def shell_message(self, msg, show=False, error=False, warning=False, success=False, selected=False):
|
||||
"""
|
||||
Shows a message on the FlatCAM Shell
|
||||
|
||||
:param msg: Message to display.
|
||||
:param show: Opens the shell.
|
||||
:param error: Shows the message as an error.
|
||||
:param warning: Shows the message as an warning.
|
||||
:param success: Shows the message as an success.
|
||||
:param selected: Indicate that something was selected on canvas
|
||||
:return: None
|
||||
"""
|
||||
if show:
|
||||
self.ui.shell_dock.show()
|
||||
try:
|
||||
if error:
|
||||
self.shell.append_error(msg + "\n")
|
||||
elif warning:
|
||||
self.shell.append_warning(msg + "\n")
|
||||
elif success:
|
||||
self.shell.append_success(msg + "\n")
|
||||
elif selected:
|
||||
self.shell.append_selected(msg + "\n")
|
||||
else:
|
||||
self.shell.append_output(msg + "\n")
|
||||
except AttributeError:
|
||||
log.debug("shell_message() is called before Shell Class is instantiated. The message is: %s", str(msg))
|
||||
|
||||
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.
|
||||
|
||||
: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.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)
|
||||
|
||||
def exec_command(self, text, no_echo=False):
|
||||
"""
|
||||
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
|
||||
Also handles execution in separated threads
|
||||
|
||||
:param text: FlatCAM TclCommand with parameters
|
||||
:param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
|
||||
will create crashes of the _Expandable_Edit widget
|
||||
:return: output if there was any
|
||||
"""
|
||||
|
||||
self.report_usage('exec_command')
|
||||
|
||||
result = self.exec_command_test(text, False, no_echo=no_echo)
|
||||
|
||||
# MS: added this method call so the geometry is updated once the TCL command is executed
|
||||
# if no_plot is None:
|
||||
# self.plot_all()
|
||||
|
||||
return result
|
||||
|
||||
def exec_command_test(self, text, reraise=True, no_echo=False):
|
||||
"""
|
||||
Same as exec_command(...) with additional control over exceptions.
|
||||
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
|
||||
|
||||
:param text: Input command
|
||||
:param reraise: Re-raise TclError exceptions in Python (mostly for unittests).
|
||||
:param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
|
||||
will create crashes of the _Expandable_Edit widget
|
||||
:return: Output from the command
|
||||
"""
|
||||
|
||||
tcl_command_string = str(text)
|
||||
|
||||
try:
|
||||
if no_echo is False:
|
||||
self.shell.open_processing() # Disables input box.
|
||||
|
||||
result = self.tcl.eval(str(tcl_command_string))
|
||||
if result != 'None' and no_echo is False:
|
||||
self.shell.append_output(result + '\n')
|
||||
|
||||
except tk.TclError as e:
|
||||
|
||||
# This will display more precise answer if something in TCL shell fails
|
||||
result = self.tcl.eval("set errorInfo")
|
||||
self.log.error("Exec command Exception: %s" % (result + '\n'))
|
||||
if no_echo is False:
|
||||
self.shell.append_error('ERROR: ' + result + '\n')
|
||||
# Show error in console and just return or in test raise exception
|
||||
if reraise:
|
||||
raise e
|
||||
finally:
|
||||
if no_echo is False:
|
||||
self.shell.close_processing()
|
||||
pass
|
||||
return result
|
||||
|
||||
# """
|
||||
# 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")
|
||||
|
||||
def info(self, msg):
|
||||
"""
|
||||
Informs the user. Normally on the status bar, optionally
|
||||
|
@ -10464,9 +10257,9 @@ class App(QtCore.QObject):
|
|||
with open(filename, "r") as tcl_script:
|
||||
cmd_line_shellfile_content = tcl_script.read()
|
||||
if self.cmd_line_headless != 1:
|
||||
self.shell._sysShell.exec_command(cmd_line_shellfile_content)
|
||||
self.shell.exec_command(cmd_line_shellfile_content)
|
||||
else:
|
||||
self.shell._sysShell.exec_command(cmd_line_shellfile_content, no_echo=True)
|
||||
self.shell.exec_command(cmd_line_shellfile_content, no_echo=True)
|
||||
|
||||
if silent is False:
|
||||
self.inform.emit('[success] %s' % _("TCL script file opened in Code Editor and executed."))
|
||||
|
@ -11865,231 +11658,6 @@ class App(QtCore.QObject):
|
|||
"""
|
||||
self.ui.progress_bar.setValue(int(percentage))
|
||||
|
||||
def setup_shell(self):
|
||||
"""
|
||||
Creates shell functions. Runs once at startup.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
|
||||
self.log.debug("setup_shell()")
|
||||
|
||||
def shelp(p=None):
|
||||
if not p:
|
||||
cmd_enum = _("Available commands:\n")
|
||||
|
||||
displayed_text = []
|
||||
try:
|
||||
# find the maximum length of a command name
|
||||
max_len = 0
|
||||
for cmd_name in commands:
|
||||
curr_len = len(cmd_name)
|
||||
if curr_len > max_len:
|
||||
max_len = curr_len
|
||||
max_tabs = math.ceil(max_len / 8)
|
||||
|
||||
for cmd_name in sorted(commands):
|
||||
cmd_description = commands[cmd_name]['description']
|
||||
|
||||
curr_len = len(cmd_name)
|
||||
tabs = '\t'
|
||||
|
||||
# make sure to add the right number of tabs (1 tab = 8 spaces) so all the commands
|
||||
# descriptions are aligned
|
||||
if curr_len == max_len:
|
||||
cmd_line_txt = ' %s%s%s' % (str(cmd_name), tabs, cmd_description)
|
||||
else:
|
||||
nr_tabs = 0
|
||||
|
||||
for x in range(max_tabs):
|
||||
if curr_len <= (x*8):
|
||||
nr_tabs += 1
|
||||
|
||||
# nr_tabs = 2 if curr_len <= 8 else 1
|
||||
cmd_line_txt = ' %s%s%s' % (str(cmd_name), nr_tabs*tabs, cmd_description)
|
||||
|
||||
displayed_text.append(cmd_line_txt)
|
||||
except Exception as err:
|
||||
log.debug("App.setup_shell.shelp() when run as 'help' --> %s" % str(err))
|
||||
displayed_text = [' %s' % cmd for cmd in sorted(commands)]
|
||||
|
||||
cmd_enum += '\n'.join(displayed_text)
|
||||
cmd_enum += '\n\n%s\n%s' % (_("Type help <command_name> for usage."), _("Example: help open_gerber"))
|
||||
return cmd_enum
|
||||
|
||||
if p not in commands:
|
||||
return "Unknown command: %s" % p
|
||||
|
||||
return commands[p]["help"]
|
||||
|
||||
# --- Migrated to new architecture ---
|
||||
# def options(name):
|
||||
# ops = self.collection.get_by_name(str(name)).options
|
||||
# return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops])
|
||||
|
||||
def h(*args):
|
||||
"""
|
||||
Pre-processes arguments to detect '-keyword value' pairs into dictionary
|
||||
and standalone parameters into list.
|
||||
"""
|
||||
|
||||
kwa = {}
|
||||
a = []
|
||||
n = len(args)
|
||||
name = None
|
||||
for i in range(n):
|
||||
match = re.search(r'^-([a-zA-Z].*)', args[i])
|
||||
if match:
|
||||
assert name is None
|
||||
name = match.group(1)
|
||||
continue
|
||||
|
||||
if name is None:
|
||||
a.append(args[i])
|
||||
else:
|
||||
kwa[name] = args[i]
|
||||
name = None
|
||||
|
||||
return a, kwa
|
||||
|
||||
@contextmanager
|
||||
def wait_signal(signal, timeout=10000):
|
||||
"""
|
||||
Block loop until signal emitted, timeout (ms) elapses
|
||||
or unhandled exception happens in a thread.
|
||||
|
||||
:param timeout: time after which the loop is exited
|
||||
:param signal: Signal to wait for.
|
||||
"""
|
||||
loop = QtCore.QEventLoop()
|
||||
|
||||
# Normal termination
|
||||
signal.connect(loop.quit)
|
||||
|
||||
# Termination by exception in thread
|
||||
self.thread_exception.connect(loop.quit)
|
||||
|
||||
status = {'timed_out': False}
|
||||
|
||||
def report_quit():
|
||||
status['timed_out'] = True
|
||||
loop.quit()
|
||||
|
||||
yield
|
||||
|
||||
# Temporarily change how exceptions are managed.
|
||||
oeh = sys.excepthook
|
||||
ex = []
|
||||
|
||||
def except_hook(type_, value, traceback_):
|
||||
ex.append(value)
|
||||
oeh(type_, value, traceback_)
|
||||
sys.excepthook = except_hook
|
||||
|
||||
# Terminate on timeout
|
||||
if timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout, report_quit)
|
||||
|
||||
# # ## Block ## ##
|
||||
loop.exec_()
|
||||
|
||||
# Restore exception management
|
||||
sys.excepthook = oeh
|
||||
if ex:
|
||||
self.raiseTclError(str(ex[0]))
|
||||
|
||||
if status['timed_out']:
|
||||
raise Exception('Timed out!')
|
||||
|
||||
def make_docs():
|
||||
output = ''
|
||||
import collections
|
||||
od = collections.OrderedDict(sorted(commands.items()))
|
||||
for cmd_, val in od.items():
|
||||
output += cmd_ + ' \n' + ''.join(['~'] * len(cmd_)) + '\n'
|
||||
|
||||
t = val['help']
|
||||
usage_i = t.find('>')
|
||||
if usage_i < 0:
|
||||
expl = t
|
||||
output += expl + '\n\n'
|
||||
continue
|
||||
|
||||
expl = t[:usage_i - 1]
|
||||
output += expl + '\n\n'
|
||||
|
||||
end_usage_i = t[usage_i:].find('\n')
|
||||
|
||||
if end_usage_i < 0:
|
||||
end_usage_i = len(t[usage_i:])
|
||||
output += ' ' + t[usage_i:] + '\n No parameters.\n'
|
||||
else:
|
||||
extras = t[usage_i+end_usage_i+1:]
|
||||
parts = [s.strip() for s in extras.split('\n')]
|
||||
|
||||
output += ' ' + t[usage_i:usage_i+end_usage_i] + '\n'
|
||||
for p in parts:
|
||||
output += ' ' + p + '\n\n'
|
||||
|
||||
return output
|
||||
|
||||
'''
|
||||
Howto 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 ocurre, always use raise_tcl_error, never return "sometext" 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 catched 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.
|
||||
|
||||
'''
|
||||
|
||||
commands = {
|
||||
'help': {
|
||||
'fcn': shelp,
|
||||
'help': _("Shows list of commands."),
|
||||
'description': ''
|
||||
},
|
||||
}
|
||||
|
||||
# Import/overwrite tcl commands as objects of TclCommand descendants
|
||||
# This modifies the variable 'commands'.
|
||||
tclCommands.register_all_commands(self, commands)
|
||||
|
||||
# Add commands to the tcl interpreter
|
||||
for cmd in commands:
|
||||
self.tcl.createcommand(cmd, commands[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 setup_recent_items(self):
|
||||
"""
|
||||
Setup a dictionary with the recent files accessed, organized by type
|
||||
|
@ -13040,6 +12608,295 @@ class App(QtCore.QObject):
|
|||
self.options.update(self.defaults)
|
||||
# self.options_write_form()
|
||||
|
||||
def init_tcl(self):
|
||||
"""
|
||||
Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
|
||||
:return: None
|
||||
"""
|
||||
if hasattr(self, 'tcl') and self.tcl is not None:
|
||||
# self.tcl = None
|
||||
# TODO we need to clean non default variables and procedures here
|
||||
# new object cannot be used here as it will not remember values created for next passes,
|
||||
# because tcl was execudted in old instance of TCL
|
||||
pass
|
||||
else:
|
||||
self.tcl = tk.Tcl()
|
||||
self.setup_shell()
|
||||
self.log.debug("TCL Shell has been initialized.")
|
||||
|
||||
def setup_shell(self):
|
||||
"""
|
||||
Creates shell functions. Runs once at startup.
|
||||
|
||||
:return: None
|
||||
"""
|
||||
|
||||
self.log.debug("setup_shell()")
|
||||
|
||||
def shelp(p=None):
|
||||
pass
|
||||
|
||||
# --- Migrated to new architecture ---
|
||||
# def options(name):
|
||||
# ops = self.collection.get_by_name(str(name)).options
|
||||
# return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops])
|
||||
|
||||
def h(*args):
|
||||
"""
|
||||
Pre-processes arguments to detect '-keyword value' pairs into dictionary
|
||||
and standalone parameters into list.
|
||||
"""
|
||||
|
||||
kwa = {}
|
||||
a = []
|
||||
n = len(args)
|
||||
name = None
|
||||
for i in range(n):
|
||||
match = re.search(r'^-([a-zA-Z].*)', args[i])
|
||||
if match:
|
||||
assert name is None
|
||||
name = match.group(1)
|
||||
continue
|
||||
|
||||
if name is None:
|
||||
a.append(args[i])
|
||||
else:
|
||||
kwa[name] = args[i]
|
||||
name = None
|
||||
|
||||
return a, kwa
|
||||
|
||||
@contextmanager
|
||||
def wait_signal(signal, timeout=10000):
|
||||
"""
|
||||
Block loop until signal emitted, timeout (ms) elapses
|
||||
or unhandled exception happens in a thread.
|
||||
|
||||
:param timeout: time after which the loop is exited
|
||||
:param signal: Signal to wait for.
|
||||
"""
|
||||
loop = QtCore.QEventLoop()
|
||||
|
||||
# Normal termination
|
||||
signal.connect(loop.quit)
|
||||
|
||||
# Termination by exception in thread
|
||||
self.thread_exception.connect(loop.quit)
|
||||
|
||||
status = {'timed_out': False}
|
||||
|
||||
def report_quit():
|
||||
status['timed_out'] = True
|
||||
loop.quit()
|
||||
|
||||
yield
|
||||
|
||||
# Temporarily change how exceptions are managed.
|
||||
oeh = sys.excepthook
|
||||
ex = []
|
||||
|
||||
def except_hook(type_, value, traceback_):
|
||||
ex.append(value)
|
||||
oeh(type_, value, traceback_)
|
||||
|
||||
sys.excepthook = except_hook
|
||||
|
||||
# Terminate on timeout
|
||||
if timeout is not None:
|
||||
QtCore.QTimer.singleShot(timeout, report_quit)
|
||||
|
||||
# # ## Block ## ##
|
||||
loop.exec_()
|
||||
|
||||
# Restore exception management
|
||||
sys.excepthook = oeh
|
||||
if ex:
|
||||
self.raise_tcl_error(str(ex[0]))
|
||||
|
||||
if status['timed_out']:
|
||||
raise Exception('Timed out!')
|
||||
|
||||
def make_docs():
|
||||
output = ''
|
||||
import collections
|
||||
od = collections.OrderedDict(sorted(self.tcl_commands_storage.items()))
|
||||
for cmd_, val in od.items():
|
||||
output += cmd_ + ' \n' + ''.join(['~'] * len(cmd_)) + '\n'
|
||||
|
||||
t = val['help']
|
||||
usage_i = t.find('>')
|
||||
if usage_i < 0:
|
||||
expl = t
|
||||
output += expl + '\n\n'
|
||||
continue
|
||||
|
||||
expl = t[:usage_i - 1]
|
||||
output += expl + '\n\n'
|
||||
|
||||
end_usage_i = t[usage_i:].find('\n')
|
||||
|
||||
if end_usage_i < 0:
|
||||
end_usage_i = len(t[usage_i:])
|
||||
output += ' ' + t[usage_i:] + '\n No parameters.\n'
|
||||
else:
|
||||
extras = t[usage_i + end_usage_i + 1:]
|
||||
parts = [s.strip() for s in extras.split('\n')]
|
||||
|
||||
output += ' ' + t[usage_i:usage_i + end_usage_i] + '\n'
|
||||
for p in parts:
|
||||
output += ' ' + p + '\n\n'
|
||||
|
||||
return output
|
||||
|
||||
'''
|
||||
Howto 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 ocurre, always use raise_tcl_error, never return "sometext" 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 catched 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.
|
||||
|
||||
'''
|
||||
|
||||
self.tcl_commands_storage = {}
|
||||
# commands = {
|
||||
# 'help': {
|
||||
# 'fcn': shelp,
|
||||
# 'help': _("Shows list of commands."),
|
||||
# 'description': ''
|
||||
# },
|
||||
# }
|
||||
|
||||
# Import/overwrite tcl commands as objects of TclCommand descendants
|
||||
# This modifies the variable 'commands'.
|
||||
tclCommands.register_all_commands(self, 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
|
||||
}
|
||||
}
|
||||
''')
|
||||
|
||||
# TODO: This shouldn't be here.
|
||||
class TclErrorException(Exception):
|
||||
"""
|
||||
this exception is defined here, to be able catch it if we successfully handle all errors from shell command
|
||||
"""
|
||||
pass
|
||||
|
||||
def shell_message(self, msg, show=False, error=False, warning=False, success=False, selected=False):
|
||||
"""
|
||||
Shows a message on the FlatCAM Shell
|
||||
|
||||
:param msg: Message to display.
|
||||
:param show: Opens the shell.
|
||||
:param error: Shows the message as an error.
|
||||
:param warning: Shows the message as an warning.
|
||||
:param success: Shows the message as an success.
|
||||
:param selected: Indicate that something was selected on canvas
|
||||
:return: None
|
||||
"""
|
||||
if show:
|
||||
self.ui.shell_dock.show()
|
||||
try:
|
||||
if error:
|
||||
self.shell.append_error(msg + "\n")
|
||||
elif warning:
|
||||
self.shell.append_warning(msg + "\n")
|
||||
elif success:
|
||||
self.shell.append_success(msg + "\n")
|
||||
elif selected:
|
||||
self.shell.append_selected(msg + "\n")
|
||||
else:
|
||||
self.shell.append_output(msg + "\n")
|
||||
except AttributeError:
|
||||
log.debug("shell_message() is called before Shell Class is instantiated. The message is: %s", str(msg))
|
||||
|
||||
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.
|
||||
|
||||
: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.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 ArgsThread(QtCore.QObject):
|
||||
open_signal = pyqtSignal(list)
|
||||
|
|
|
@ -252,3 +252,11 @@ class FlatCAMTool(QtWidgets.QWidget):
|
|||
(_("Edited value is out of range"), minval, maxval))
|
||||
else:
|
||||
self.app.inform.emit('[success] %s' % _("Edited value is within limits."))
|
||||
|
||||
def sizeHint(self):
|
||||
"""
|
||||
I've overloaded this just in case I will need to make changes in the future to enforce dimensions
|
||||
:return:
|
||||
"""
|
||||
default_hint_size = super(FlatCAMTool, self).sizeHint()
|
||||
return QtCore.QSize(default_hint_size.width(), default_hint_size.height())
|
||||
|
|
|
@ -42,13 +42,24 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
|
|||
|
||||
# Divine icon pack by Ipapun @ finicons.com
|
||||
|
||||
# ################################## ##
|
||||
# ## BUILDING THE GUI IS DONE HERE # ##
|
||||
# ################################## ##
|
||||
# #######################################################################
|
||||
# ############ BUILDING THE GUI IS EXECUTED HERE ########################
|
||||
# #######################################################################
|
||||
|
||||
# ######### ##
|
||||
# ## Menu # ##
|
||||
# ######### ##
|
||||
# #######################################################################
|
||||
# ####################### TCL Shell DOCK ################################
|
||||
# #######################################################################
|
||||
self.shell_dock = QtWidgets.QDockWidget("FlatCAM TCL Shell")
|
||||
self.shell_dock.setObjectName('Shell_DockWidget')
|
||||
self.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas)
|
||||
self.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
|
||||
QtWidgets.QDockWidget.DockWidgetFloatable |
|
||||
QtWidgets.QDockWidget.DockWidgetClosable)
|
||||
self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.shell_dock)
|
||||
|
||||
# #######################################################################
|
||||
# ###################### Menu BUILDING ##################################
|
||||
# #######################################################################
|
||||
self.menu = self.menuBar()
|
||||
|
||||
self.menu_toggle_nb = QtWidgets.QAction(
|
||||
|
@ -735,7 +746,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
|
|||
# ########################################################################
|
||||
|
||||
# IMPORTANT #
|
||||
# The order: SPITTER -> NOTEBOOK -> SNAP TOOLBAR is important and without it the GUI will not be initialized as
|
||||
# The order: SPLITTER -> NOTEBOOK -> SNAP TOOLBAR is important and without it the GUI will not be initialized as
|
||||
# desired.
|
||||
self.splitter = QtWidgets.QSplitter()
|
||||
self.setCentralWidget(self.splitter)
|
||||
|
|
|
@ -159,7 +159,7 @@ class DblSidedTool(FlatCAMTool):
|
|||
self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Parameters"))
|
||||
self.param_label.setToolTip('%s.' % _("Parameters for the mirror operation"))
|
||||
|
||||
grid_lay1.addWidget(self.param_label, 0, 0, 1, 3)
|
||||
grid_lay1.addWidget(self.param_label, 0, 0, 1, 2)
|
||||
|
||||
# ## Axis
|
||||
self.mirax_label = QtWidgets.QLabel('%s:' % _("Mirror Axis"))
|
||||
|
|
|
@ -14,6 +14,8 @@ from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
|
|||
import html
|
||||
import sys
|
||||
|
||||
import tkinter as tk
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
@ -227,6 +229,12 @@ class TermWidget(QWidget):
|
|||
|
||||
class FCShell(TermWidget):
|
||||
def __init__(self, sysShell, version, *args):
|
||||
"""
|
||||
|
||||
:param sysShell: 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)
|
||||
self._sysShell = sysShell
|
||||
|
||||
|
@ -246,4 +254,94 @@ class FCShell(TermWidget):
|
|||
return True
|
||||
|
||||
def child_exec_command(self, text):
|
||||
self._sysShell.exec_command(text)
|
||||
self.exec_command(text)
|
||||
|
||||
def exec_command(self, text, no_echo=False):
|
||||
"""
|
||||
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
|
||||
Also handles execution in separated threads
|
||||
|
||||
:param text: FlatCAM TclCommand with parameters
|
||||
:param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
|
||||
will create crashes of the _Expandable_Edit widget
|
||||
:return: output if there was any
|
||||
"""
|
||||
|
||||
self._sysShell.report_usage('exec_command')
|
||||
|
||||
return self.exec_command_test(text, False, no_echo=no_echo)
|
||||
|
||||
def exec_command_test(self, text, reraise=True, no_echo=False):
|
||||
"""
|
||||
Same as exec_command(...) with additional control over exceptions.
|
||||
Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
|
||||
|
||||
:param text: Input command
|
||||
:param reraise: Re-raise TclError exceptions in Python (mostly for unittests).
|
||||
:param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
|
||||
will create crashes of the _Expandable_Edit widget
|
||||
:return: Output from the command
|
||||
"""
|
||||
|
||||
tcl_command_string = str(text)
|
||||
|
||||
try:
|
||||
if no_echo is False:
|
||||
self.open_processing() # Disables input box.
|
||||
|
||||
result = self._sysShell.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'))
|
||||
if no_echo is False:
|
||||
self.append_error('ERROR: ' + result + '\n')
|
||||
# Show error in console and just return or in test raise exception
|
||||
if reraise:
|
||||
raise e
|
||||
finally:
|
||||
if no_echo is False:
|
||||
self.close_processing()
|
||||
pass
|
||||
return result
|
||||
|
||||
# """
|
||||
# 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")
|
||||
|
|
|
@ -33,8 +33,6 @@ class ToolTransform(FlatCAMTool):
|
|||
FlatCAMTool.__init__(self, app)
|
||||
self.decimals = self.app.decimals
|
||||
|
||||
self.transform_lay = QtWidgets.QVBoxLayout()
|
||||
self.layout.addLayout(self.transform_lay)
|
||||
# ## Title
|
||||
title_label = QtWidgets.QLabel("%s" % self.toolName)
|
||||
title_label.setStyleSheet("""
|
||||
|
@ -44,12 +42,12 @@ class ToolTransform(FlatCAMTool):
|
|||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.transform_lay.addWidget(title_label)
|
||||
self.transform_lay.addWidget(QtWidgets.QLabel(''))
|
||||
self.layout.addWidget(title_label)
|
||||
self.layout.addWidget(QtWidgets.QLabel(''))
|
||||
|
||||
# ## Layout
|
||||
grid0 = QtWidgets.QGridLayout()
|
||||
self.transform_lay.addLayout(grid0)
|
||||
self.layout.addLayout(grid0)
|
||||
grid0.setColumnStretch(0, 0)
|
||||
grid0.setColumnStretch(1, 1)
|
||||
grid0.setColumnStretch(2, 0)
|
||||
|
@ -206,7 +204,7 @@ class ToolTransform(FlatCAMTool):
|
|||
self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False)
|
||||
|
||||
grid0.addWidget(self.scale_link_cb, 10, 0)
|
||||
grid0.addWidget(self.scale_zero_ref_cb, 10, 1)
|
||||
grid0.addWidget(self.scale_zero_ref_cb, 10, 1, 1, 2)
|
||||
|
||||
separator_line = QtWidgets.QFrame()
|
||||
separator_line.setFrameShape(QtWidgets.QFrame.HLine)
|
||||
|
@ -395,7 +393,7 @@ class ToolTransform(FlatCAMTool):
|
|||
|
||||
grid0.addWidget(QtWidgets.QLabel(''), 26, 0, 1, 3)
|
||||
|
||||
self.transform_lay.addStretch()
|
||||
self.layout.addStretch()
|
||||
|
||||
# ## Reset Tool
|
||||
self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
|
||||
|
@ -408,7 +406,7 @@ class ToolTransform(FlatCAMTool):
|
|||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.transform_lay.addWidget(self.reset_button)
|
||||
self.layout.addWidget(self.reset_button)
|
||||
|
||||
# ## Signals
|
||||
self.rotate_button.clicked.connect(self.on_rotate)
|
||||
|
|
|
@ -3,8 +3,7 @@ import re
|
|||
import FlatCAMApp
|
||||
import abc
|
||||
import collections
|
||||
from PyQt5 import QtCore, QtGui
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5 import QtCore
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
# ##########################################################
|
||||
# FlatCAM: 2D Post-processing for Manufacturing #
|
||||
# File Author: Marius Adrian Stanciu (c) #
|
||||
# Content was borrowed from FlatCAM proper #
|
||||
# Date: 4/22/2020 #
|
||||
# MIT Licence #
|
||||
# ##########################################################
|
||||
|
||||
from tclCommands.TclCommand import TclCommand
|
||||
|
||||
import collections
|
||||
import math
|
||||
|
||||
import gettext
|
||||
import FlatCAMTranslation as fcTranslate
|
||||
import builtins
|
||||
|
||||
fcTranslate.apply_language('strings')
|
||||
if '_' not in builtins.__dict__:
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
class TclCommandHelp(TclCommand):
|
||||
"""
|
||||
Tcl shell command to get the value of a system variable
|
||||
|
||||
example:
|
||||
get_sys excellon_zeros
|
||||
"""
|
||||
|
||||
# List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
|
||||
aliases = ['help']
|
||||
|
||||
description = '%s %s' % ("--", "PRINTS to TCL the HELP.")
|
||||
|
||||
# Dictionary of types from Tcl command, needs to be ordered
|
||||
arg_names = collections.OrderedDict([
|
||||
('name', str)
|
||||
])
|
||||
|
||||
# Dictionary of types from Tcl command, needs to be ordered , this is for options like -optionname value
|
||||
option_types = collections.OrderedDict([
|
||||
|
||||
])
|
||||
|
||||
# array of mandatory options for current Tcl command: required = {'name','outname'}
|
||||
required = []
|
||||
|
||||
# structured help for current command, args needs to be ordered
|
||||
help = {
|
||||
'main': "Returns to TCL the value for the entered system variable.",
|
||||
'args': collections.OrderedDict([
|
||||
('name', 'Name of a Tcl Command for which to display the Help.'),
|
||||
]),
|
||||
'examples': ['help add_circle']
|
||||
}
|
||||
|
||||
def execute(self, args, unnamed_args):
|
||||
"""
|
||||
|
||||
:param args:
|
||||
:param unnamed_args:
|
||||
:return:
|
||||
"""
|
||||
print(self.app.tcl_commands_storage)
|
||||
|
||||
if 'name' in args:
|
||||
name = args['name']
|
||||
if name not in self.app.tcl_commands_storage:
|
||||
return "Unknown command: %s" % name
|
||||
|
||||
print(self.app.tcl_commands_storage[name]["help"])
|
||||
else:
|
||||
if args is None:
|
||||
cmd_enum = _("Available commands:\n")
|
||||
|
||||
displayed_text = []
|
||||
try:
|
||||
# find the maximum length of a command name
|
||||
max_len = 0
|
||||
for cmd_name in self.app.tcl_commands_storage:
|
||||
curr_len = len(cmd_name)
|
||||
if curr_len > max_len:
|
||||
max_len = curr_len
|
||||
max_tabs = math.ceil(max_len / 8)
|
||||
|
||||
for cmd_name in sorted(self.app.tcl_commands_storage):
|
||||
cmd_description = self.app.tcl_commands_storage[cmd_name]['description']
|
||||
|
||||
curr_len = len(cmd_name)
|
||||
tabs = '\t'
|
||||
|
||||
cmd_name_colored = "<span style=\" color:#ff0000;\" >"
|
||||
cmd_name_colored += str(cmd_name)
|
||||
cmd_name_colored += "</span>"
|
||||
|
||||
# make sure to add the right number of tabs (1 tab = 8 spaces) so all the commands
|
||||
# descriptions are aligned
|
||||
if curr_len == max_len:
|
||||
cmd_line_txt = ' %s%s%s' % (cmd_name_colored, tabs, cmd_description)
|
||||
else:
|
||||
nr_tabs = 0
|
||||
|
||||
for x in range(max_tabs):
|
||||
if curr_len <= (x * 8):
|
||||
nr_tabs += 1
|
||||
|
||||
# nr_tabs = 2 if curr_len <= 8 else 1
|
||||
cmd_line_txt = ' %s%s%s' % (cmd_name_colored, nr_tabs * tabs, cmd_description)
|
||||
|
||||
displayed_text.append(cmd_line_txt)
|
||||
except Exception as err:
|
||||
self.app.log.debug("App.setup_shell.shelp() when run as 'help' --> %s" % str(err))
|
||||
displayed_text = [' %s' % cmd for cmd in sorted(self.app.tcl_commands_storage)]
|
||||
|
||||
cmd_enum += '\n'.join(displayed_text)
|
||||
cmd_enum += '\n\n%s\n%s' % (_("Type help <command_name> for usage."), _("Example: help open_gerber"))
|
||||
|
||||
print(cmd_enum)
|
|
@ -93,6 +93,7 @@ def register_all_commands(app, commands):
|
|||
tcl_modules = {k: v for k, v in list(sys.modules.items()) if k.startswith('tclCommands.TclCommand')}
|
||||
|
||||
for key, mod in list(tcl_modules.items()):
|
||||
print(key)
|
||||
if key != 'tclCommands.TclCommand':
|
||||
class_name = key.split('.')[1]
|
||||
class_type = getattr(mod, class_name)
|
||||
|
|
Loading…
Reference in New Issue