From cd6700152c0b83f3b909583695d6aa2f0b55c911 Mon Sep 17 00:00:00 2001 From: Kamil Sopko Date: Wed, 16 Mar 2016 18:57:43 +0100 Subject: [PATCH] draft for reimplementation of tcl commands to separated files/modules --- FlatCAMApp.py | 7 +- tclCommands/TclCommand.py | 180 +++++++++++++++++++++++++++++ tclCommands/TclCommandExteriors.py | 64 ++++++++++ tclCommands/TclCommandInteriors.py | 63 ++++++++++ tclCommands/__init__.py | 46 ++++++++ 5 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 tclCommands/TclCommand.py create mode 100644 tclCommands/TclCommandExteriors.py create mode 100644 tclCommands/TclCommandInteriors.py create mode 100644 tclCommands/__init__.py diff --git a/FlatCAMApp.py b/FlatCAMApp.py index 9f2fb815..5764f33d 100644 --- a/FlatCAMApp.py +++ b/FlatCAMApp.py @@ -25,7 +25,7 @@ from FlatCAMDraw import FlatCAMDraw from FlatCAMProcess import * from MeasurementTool import Measurement from DblSidedTool import DblSidedTool - +import tclCommands ######################################## ## App ## @@ -660,6 +660,8 @@ class App(QtCore.QObject): if not isinstance(unknownException, self.TclErrorException): self.raiseTclError("Unknown error: %s" % str(unknownException)) + else: + raise unknownException def raiseTclError(self, text): """ @@ -3547,6 +3549,9 @@ class App(QtCore.QObject): } } + #import/overwrite tcl commands as objects of TclCommand descendants + tclCommands.register_all_commands(self, commands) + # Add commands to the tcl interpreter for cmd in commands: self.tcl.createcommand(cmd, commands[cmd]['fcn']) diff --git a/tclCommands/TclCommand.py b/tclCommands/TclCommand.py new file mode 100644 index 00000000..ccc29eb2 --- /dev/null +++ b/tclCommands/TclCommand.py @@ -0,0 +1,180 @@ +import sys, inspect, pkgutil +import re +import FlatCAMApp + + +class TclCommand(object): + + app=None + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = None + + # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname + arg_names = {'name': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "undefined help.", + 'args': { + 'argumentname': 'undefined help.', + 'optionname': 'undefined help.' + }, + 'examples' : [] + } + + def __init__(self, app): + self.app=app + + def get_decorated_help(self): + """ + Decorate help for TCL console output. + + :return: decorated help from structue + """ + + def get_decorated_command(alias): + command_string = [] + for key, value in reversed(self.help['args'].items()): + command_string.append(get_decorated_argument(key, value, True)) + return "> " + alias + " " + " ".join(command_string) + + def get_decorated_argument(key, value, in_command=False): + option_symbol = '' + if key in self.arg_names: + type=self.arg_names[key] + in_command_name = "<" + str(type.__name__) + ">" + else: + option_symbol = '-' + type=self.option_types[key] + in_command_name = option_symbol + key + " <" + str(type.__name__) + ">" + if in_command: + if key in self.required: + return in_command_name + else: + return '[' + in_command_name + "]" + else: + if key in self.required: + return "\t" + option_symbol + key + " <" + str(type.__name__) + ">: " + value + else: + return "\t[" + option_symbol + key + " <" + str(type.__name__) + ">: " + value+"]" + + def get_decorated_example(example): + example_string = '' + return "todo" + example_string + + help_string=[self.help['main']] + for alias in self.aliases: + help_string.append(get_decorated_command(alias)) + + for key, value in reversed(self.help['args'].items()): + help_string.append(get_decorated_argument(key, value)) + + for example in self.help['examples']: + help_string.append(get_decorated_example(example)) + + return "\n".join(help_string) + + def parse_arguments(self, args): + """ + Pre-processes arguments to detect '-keyword value' pairs into dictionary + and standalone parameters into list. + + This is copy from FlatCAMApp.setup_shell().h() just for accesibility, original should be removed after all commands will be converted + """ + + options = {} + arguments = [] + 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: + arguments.append(args[i]) + else: + options[name] = args[i] + name = None + + return arguments, options + + def check_args(self, args): + """ + Check arguments and options for right types + + :param args: arguments from tcl to check + :return: + """ + + arguments, options = self.parse_arguments(args) + + named_args={} + unnamed_args=[] + + # check arguments + idx=0 + arg_names_reversed=self.arg_names.items() + for argument in arguments: + if len(self.arg_names) > idx: + key, type = arg_names_reversed[len(self.arg_names)-idx-1] + try: + named_args[key] = type(argument) + except Exception, e: + self.app.raiseTclError("Cannot cast named argument '%s' to type %s." % (key, type)) + else: + unnamed_args.append(argument) + idx += 1 + + # check otions + for key in options: + if key not in self.option_types: + self.app.raiseTclError('Unknown parameter: %s' % key) + try: + named_args[key] = self.option_types[key](options[key]) + except Exception, e: + self.app.raiseTclError("Cannot cast argument '-%s' to type %s." % (key, self.option_types[key])) + + # check required arguments + for key in self.required: + if key not in named_args: + self.app.raiseTclError("Missing required argument '%s'." % (key)) + + return named_args, unnamed_args + + def execute_wrapper(self, *args): + """ + Command which is called by tcl console when current commands aliases are hit. + Main catch(except) is implemented here. + This method should be reimplemented only when initial checking sequence differs + + :param args: arguments passed from tcl command console + :return: None, output text or exception + """ + try: + args, unnamed_args = self.check_args(args) + return self.execute(args, unnamed_args) + except Exception as unknown: + self.app.raiseTclUnknownError(unknown) + + def execute(self, args, unnamed_args): + """ + Direct execute of command, this method should be implemented in each descendant. + No main catch should be implemented here. + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None, output text or exception + """ + + raise NotImplementedError("Please Implement this method") diff --git a/tclCommands/TclCommandExteriors.py b/tclCommands/TclCommandExteriors.py new file mode 100644 index 00000000..aad0fa2a --- /dev/null +++ b/tclCommands/TclCommandExteriors.py @@ -0,0 +1,64 @@ +from ObjectCollection import * +import TclCommand + + +class TclCommandExteriors(TclCommand.TclCommand): + """ + Tcl shell command to get exteriors of polygons + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['exteriors','ext'] + + # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname + arg_names = {'name': str,'name2': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {'outname': str} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "Get exteriors of polygons.", + 'args': { + 'name': 'Name of the source Geometry object.', + 'outname': 'Name of the resulting Geometry object.' + }, + 'examples':[] + } + + def execute(self, args, unnamed_args): + """ + execute current TCL shell command + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None or exception + """ + + name = args['name'] + + if 'outname' in args: + outname = args['outname'] + else: + outname = name + "_exteriors" + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + + def geo_init(geo_obj, app_obj): + geo_obj.solid_geometry = obj_exteriors + + obj_exteriors = obj.get_exteriors() + self.app.new_object('geometry', outname, geo_init) \ No newline at end of file diff --git a/tclCommands/TclCommandInteriors.py b/tclCommands/TclCommandInteriors.py new file mode 100644 index 00000000..f1c65590 --- /dev/null +++ b/tclCommands/TclCommandInteriors.py @@ -0,0 +1,63 @@ +from ObjectCollection import * +import TclCommand + +class TclCommandInteriors(TclCommand.TclCommand): + """ + Tcl shell command to get interiors of polygons + """ + + # array of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon) + aliases = ['interiors'] + + # dictionary of types from Tcl command: args = {'name': str}, this is for value without optionname + arg_names = {'name': str} + + # dictionary of types from Tcl command: types = {'outname': str} , this is for options like -optionname value + option_types = {'outname': str} + + # array of mandatory options for current Tcl command: required = {'name','outname'} + required = ['name'] + + # structured help for current command + help = { + 'main': "Get interiors of polygons.", + 'args': { + 'name': 'Name of the source Geometry object.', + 'outname': 'Name of the resulting Geometry object.' + }, + 'examples':[] + } + + def execute(self, args, unnamed_args): + """ + execute current TCL shell command + + :param args: array of known named arguments and options + :param unnamed_args: array of other values which were passed into command + without -somename and we do not have them in known arg_names + :return: None or exception + """ + + name = args['name'] + + if 'outname' in args: + outname = args['outname'] + else: + outname = name + "_interiors" + + try: + obj = self.app.collection.get_by_name(name) + except: + self.app.raiseTclError("Could not retrieve object: %s" % name) + + if obj is None: + self.app.raiseTclError("Object not found: %s" % name) + + if not isinstance(obj, Geometry): + self.app.raiseTclError('Expected Geometry, got %s %s.' % (name, type(obj))) + + def geo_init(geo_obj, app_obj): + geo_obj.solid_geometry = obj_exteriors + + obj_exteriors = obj.get_interiors() + self.app.new_object('geometry', outname, geo_init) \ No newline at end of file diff --git a/tclCommands/__init__.py b/tclCommands/__init__.py new file mode 100644 index 00000000..43cb0a6b --- /dev/null +++ b/tclCommands/__init__.py @@ -0,0 +1,46 @@ +import pkgutil +import inspect +import sys + +# allowed command modules +import tclCommands.TclCommandExteriors +import tclCommands.TclCommandInteriors + + +__all__=[] + +for loader, name, is_pkg in pkgutil.walk_packages(__path__): + module = loader.find_module(name).load_module(name) + __all__.append(name) + + +def register_all_commands(app, commands): + """ + Static method which register all known commands. + + Command should be for now in directory tclCommands and module should start with TCLCommand + Class have to follow same name as module. + + we need import all modules in top section: + import tclCommands.TclCommandExteriors + at this stage we can include only wanted commands with this, autoloading may be implemented in future + I have no enought knowledge about python's anatomy. Would be nice to include all classes which are descendant etc. + + :param app: FlatCAMApp + :param commands: array of commands which should be modified + :return: None + """ + + tcl_modules = {k: v for k, v in sys.modules.items() if k.startswith('tclCommands.TclCommand')} + + for key, module in tcl_modules.items(): + if key != 'tclCommands.TclCommand': + classname = key.split('.')[1] + class_ = getattr(module, classname) + commandInstance=class_(app) + + for alias in commandInstance.aliases: + commands[alias]={ + 'fcn': commandInstance.execute_wrapper, + 'help': commandInstance.get_decorated_help() + } \ No newline at end of file