From 4d8fc0515727dc1303507cdcfab4eafc1dab3bbf Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 2 Sep 2010 11:39:01 +0000 Subject: [PATCH] Cleaned up the batch processors (both cmd- and code processor) and homogenized their interfaces and APIs. Also test-ran the example codes and fixed some bugs. --- game/gamesrc/commands/default/batchprocess.py | 174 ++++--- game/gamesrc/commands/default/objmanip.py | 19 +- game/gamesrc/world/examples/batch_code.py | 47 +- src/objects/models.py | 1 + src/objects/object_search_funcs.py | 7 +- src/utils/batchprocessors.py | 441 +++++++++--------- 6 files changed, 373 insertions(+), 316 deletions(-) diff --git a/game/gamesrc/commands/default/batchprocess.py b/game/gamesrc/commands/default/batchprocess.py index 8450525073..d5722f586e 100644 --- a/game/gamesrc/commands/default/batchprocess.py +++ b/game/gamesrc/commands/default/batchprocess.py @@ -23,23 +23,46 @@ Example batch-code file: game/gamesrc/commands/examples/example_batch_code.py """ from traceback import format_exc from django.conf import settings -from src.utils import batchprocessors +from src.utils.batchprocessors import BATCHCMD, BATCHCODE from game.gamesrc.commands.default.muxcommand import MuxCommand from src.commands.cmdset import CmdSet + +HEADER_WIDTH = 70 + #global defines for storage -CWHITE = r"%cn%ch%cw" -CRED = r"%cn%ch%cr" -CGREEN = r"%cn%ci%cg" -CYELLOW = r"%cn%ch%cy" -CNORM = r"%cn" - - #------------------------------------------------------------ # Helper functions #------------------------------------------------------------ +def format_header(caller, entry): + """ + Formats a header + """ + width = HEADER_WIDTH - 10 + entry = entry.strip() + header = entry[:min(width, min(len(entry), entry.find('\n')))] + if len(entry) > width: + header = "%s[...]" % header + ptr = caller.ndb.batch_stackptr + 1 + stacklen = len(caller.ndb.batch_stack) + header = "{w%02i/%02i{G: %s{n" % (ptr, stacklen, header) + # add extra space to the side for padding. + header = "%s%s" % (header, " "*(width-len(header))) + header = header.replace('\n', '\\n') + + return header + +def format_code(entry): + """ + Formats the viewing of code and errors + """ + code = "" + for line in entry.split('\n'): + code += "\n{G>>>{n %s" % line + return code.strip() + def batch_cmd_exec(caller): """ Helper function for executing a single batch-command entry @@ -47,19 +70,14 @@ def batch_cmd_exec(caller): ptr = caller.ndb.batch_stackptr stack = caller.ndb.batch_stack command = stack[ptr] - cmdname = command[:command.find(" ")] - caller.msg("%s %02i/%02i: %s %s%s" % (CGREEN, ptr+1, - len(stack), - cmdname, - CGREEN, " "*(50-len(cmdname)))) + caller.msg(format_header(caller, command)) try: caller.execute_cmd(command) except Exception: - caller.msg(format_exc()) + caller.msg(format_code(format_exc())) return False return True - def batch_code_exec(caller): """ Helper function for executing a single batch-code entry @@ -67,20 +85,16 @@ def batch_code_exec(caller): ptr = caller.ndb.batch_stackptr stack = caller.ndb.batch_stack debug = caller.ndb.batch_debug - codedict = stack[ptr] - caller.msg("%s %02i/%02i: %s %s%s" % (CGREEN, ptr + 1, - len(stack), - codedict["firstline"], - CGREEN, " "*(50-len(codedict["firstline"])))) - err = batchprocessors.batch_code_exec(codedict, - extra_environ={"caller":caller}, debug=debug) + + caller.msg(format_header(caller, codedict['code'])) + err = BATCHCODE.code_exec(codedict, + extra_environ={"caller":caller}, debug=debug) if err: - caller.msg(err) + caller.msg(format_code(err)) return False return True - def step_pointer(caller, step=1): """ Step in stack, returning the item located. @@ -93,13 +107,15 @@ def step_pointer(caller, step=1): stack = caller.ndb.batch_stack nstack = len(stack) if ptr + step <= 0: - caller.msg("Beginning of batch file.") + caller.msg("{RBeginning of batch file.") if ptr + step >= nstack: - caller.msg("End of batch file.") + caller.msg("{REnd of batch file.") caller.ndb.batch_stackptr = max(0, min(nstack-1, ptr + step)) def show_curr(caller, showall=False): - "Show the current position in stack." + """ + Show the current position in stack + """ stackptr = caller.ndb.batch_stackptr stack = caller.ndb.batch_stack @@ -107,25 +123,39 @@ def show_curr(caller, showall=False): caller.ndb.batch_stackptr = len(stack) - 1 show_curr(caller, showall) return + entry = stack[stackptr] + if type(entry) == dict: - # we first try the batch-code syntax - firstline = entry['code'][:min(35, len(entry['code'])-1)] - codeall = entry['code'] + # this is a batch-code entry + string = format_header(caller, entry['code']) + codeall = entry['code'].strip() else: - # we try the batch-cmd syntax instead - firstline = entry[:min(35, len(entry)-1)] - codeall = entry - string = "%s %02i/%02i: %s %s %s %s%s" % (CGREEN, - stackptr+1, len(stack), - firstline, CGREEN, - "(hh for help)", - " "*(35-len(firstline.strip())), - CNORM) + # this is a batch-cmd entry + string = format_header(caller, entry) + codeall = entry.strip() + string += "{G(hh for help)" if showall: - string += "\n%s" % codeall + for line in codeall.split('\n'): + string += "\n{n>>> %s" % line caller.msg(string) +def purge_processor(caller): + """ + This purges all effects running + on the caller. + """ + try: + del caller.ndb.batch_stack + del caller.ndb.batch_stackptr + del caller.ndb.batch_pythonpath + del caller.ndb.batch_batchmode + except: + pass + # clear everything but the default cmdset. + caller.cmdset.delete(BatchSafeCmdSet) + caller.cmdset.clear() + caller.scripts.validate() # this will purge interactive mode #------------------------------------------------------------ # main access commands @@ -164,7 +194,7 @@ class CmdBatchCommands(MuxCommand): #parse indata file - commands = batchprocessors.parse_batchcommand_file(python_path) + commands = BATCHCMD.parse_file(python_path) if not commands: string = "'%s' not found.\nYou have to supply the python path " string += "of the file relative to \nyour batch-file directory (%s)." @@ -202,7 +232,8 @@ class CmdBatchCommands(MuxCommand): del caller.ndb.batch_pythonpath del caller.ndb.batch_batchmode string = " Batchfile '%s' applied." % python_path - caller.msg("%s%s%s" % (CGREEN, string, " "*(60-len(string)))) + caller.msg("{G%s" % string) + purge_processor(caller) class CmdBatchCode(MuxCommand): """ @@ -240,7 +271,7 @@ class CmdBatchCode(MuxCommand): python_path = self.args #parse indata file - codes = batchprocessors.parse_batchcode_file(python_path) + codes = BATCHCODE.parse_file(python_path) if not codes: string = "'%s' not found.\nYou have to supply the python path " string += "of the file relative to \nyour batch-file directory (%s)." @@ -283,7 +314,8 @@ class CmdBatchCode(MuxCommand): del caller.ndb.batch_pythonpath del caller.ndb.batch_batchmode string = " Batchfile '%s' applied." % python_path - caller.msg("%s%s%s" % (CGREEN, string, " "*(60-len(string)))) + caller.msg("{G%s" % string) + purge_processor(caller) #------------------------------------------------------------ # State-commands for the interactive batch processor modes @@ -294,23 +326,17 @@ class CmdStateAbort(MuxCommand): """ @abort - Exits back the default cmdset, regardless of what state - we are currently in. + This is a safety feature. It force-ejects us out of the processor and to + the default cmdset, regardless of what current cmdset the processor might + have put us in (e.g. when testing buggy scripts etc). """ key = "@abort" help_category = "BatchProcess" def func(self): "Exit back to default." - caller = self.caller - del caller.ndb.batch_stack - del caller.ndb.batch_stackptr - del caller.ndb.batch_pythonpath - del caller.ndb.batch_batchmode - # clear everything but the default cmdset. - caller.cmdset.delete(BatchSafeCmdSet) - caller.cmdset.clear() - caller.msg("Exit: Cleared back to default state.") + purge_processor(self.caller) + self.caller.msg("Exited processor and reset out active cmdset back to the default one.") class CmdStateLL(MuxCommand): """ @@ -358,10 +384,10 @@ class CmdStateRR(MuxCommand): def func(self): caller = self.caller if caller.ndb.batch_batchmode == "batch_code": - batchprocessors.read_batchcommand_file(caller.ndb.batch_pythonpath) + BATCHCODE.read_file(caller.ndb.batch_pythonpath) else: - batchprocessors.read_batchcode_file(caller.ndb.batch_pythonpath) - caller.msg("\nFile reloaded. Staying on same command.\n") + BATHCMD.read_file(caller.ndb.batch_pythonpath) + caller.msg(format_code("File reloaded. Staying on same command.")) show_curr(caller) class CmdStateRRR(MuxCommand): @@ -377,11 +403,11 @@ class CmdStateRRR(MuxCommand): def func(self): caller = self.caller if caller.ndb.batch_batchmode == "batch_code": - batchprocessors.read_batchcommand_file(caller.ndb.batch_pythonpath) + BATCHCODE.read_file(caller.ndb.batch_pythonpath) else: - batchprocessors.read_batchcode_file(caller.ndb.batch_pythonpath) + BATCHCMD.read_file(caller.ndb.batch_pythonpath) caller.ndb.batch_stackptr = 0 - caller.msg("\nFile reloaded. Restarting from top.\n") + caller.msg(format_code("File reloaded. Restarting from top.")) show_curr(caller) class CmdStateNN(MuxCommand): @@ -545,7 +571,7 @@ class CmdStateCC(MuxCommand): del caller.ndb.batch_stackptr del caller.ndb.batch_pythonpath del caller.ndb.batch_batchmode - caller.msg("Finished processing batch file.") + caller.msg(format_code("Finished processing batch file.")) class CmdStateJJ(MuxCommand): """ @@ -562,7 +588,7 @@ class CmdStateJJ(MuxCommand): if arg and arg.isdigit(): number = int(self.args)-1 else: - caller.msg("You must give a number index.") + caller.msg(format_code("You must give a number index.")) return ptr = caller.ndb.batch_stackptr step = number - ptr @@ -584,7 +610,7 @@ class CmdStateJL(MuxCommand): if arg and arg.isdigit(): number = int(self.args)-1 else: - caller.msg("You must give a number index.") + caller.msg(format_code("You must give a number index.")) return ptr = caller.ndb.batch_stackptr step = number - ptr @@ -601,26 +627,19 @@ class CmdStateQQ(MuxCommand): help_category = "BatchProcess" def func(self): - caller = self.caller - del caller.ndb.batch_stack - del caller.ndb.batch_stackptr - del caller.ndb.batch_pythonpath - del caller.ndb.batch_batchmode - caller.cmdset.delete(BatchSafeCmdSet) - caller.cmdset.delete(BatchInteractiveCmdSet) - caller.scripts.validate() # this will clear interactive mode. - caller.msg("Aborted interactive batch mode.") + purge_processor(self.caller) + self.caller.msg("Aborted interactive batch mode.") class CmdStateHH(MuxCommand): "Help command" - key = "help" - aliases = "hh" + key = "hh" help_category = "BatchProcess" def func(self): string = """ Interactive batch processing commands: + nn [steps] - next command (no processing) nl [steps] - next & look bb [steps] - back to previous command (no processing) @@ -637,6 +656,11 @@ class CmdStateHH(MuxCommand): cc - continue processing to end, then quit. qq - quit (abort all remaining commands) + + @abort - this is a safety command that always is available + regardless of what cmdsets gets added to us during + batch-command processing. It immediately shuts down + the processor and returns us to the default cmdset. """ self.caller.msg(string) diff --git a/game/gamesrc/commands/default/objmanip.py b/game/gamesrc/commands/default/objmanip.py index 252e7b06c3..43e533c665 100644 --- a/game/gamesrc/commands/default/objmanip.py +++ b/game/gamesrc/commands/default/objmanip.py @@ -603,13 +603,13 @@ class CmdCreate(ObjManipCommand): obj = create.create_object(typeclass, name, caller, home=caller, aliases=aliases) if not obj: - string += "\nError when creating object." + string = "Error when creating object." continue if aliases: - string += "\nYou create a new %s: %s (aliases: %s)." + string = "You create a new %s: %s (aliases: %s)." string = string % (obj.typeclass, obj.name, ", ".join(aliases)) else: - string += "\nYou create a new %s: %s." + string = "You create a new %s: %s." string = string % (obj.typeclass, obj.name) if 'drop' in self.switches: if caller.location: @@ -1189,21 +1189,22 @@ class CmdDestroy(MuxCommand): for objname in self.lhslist: obj = caller.search(objname) if not obj: - continue + continue objname = obj.name if obj.player and not 'override' in self.switches: - string += "\n\rObject %s is a player object. Use /override to delete anyway." % objname + string = "Object %s is a player object. Use /override to delete anyway." % objname continue if not has_perm(caller, obj, 'create'): - string += "\n\rYou don't have permission to delete %s." % objname + string = "You don't have permission to delete %s." % objname continue # do the deletion okay = obj.delete() if not okay: - string += "\n\rERROR: %s NOT deleted, probably because at_obj_delete() returned False." % objname + string = "ERROR: %s NOT deleted, probably because at_obj_delete() returned False." % objname else: - string += "\n\r%s was deleted." % objname - caller.msg(string.strip('\n')) + string = "%s was deleted." % objname + if string: + caller.msg(string.strip()) #NOT VALID IN NEW SYSTEM! diff --git a/game/gamesrc/world/examples/batch_code.py b/game/gamesrc/world/examples/batch_code.py index 401661fc05..aabf07154e 100644 --- a/game/gamesrc/world/examples/batch_code.py +++ b/game/gamesrc/world/examples/batch_code.py @@ -17,37 +17,43 @@ # automatically be made available for each block. Observe # that changes to these variables made in one block is not # preserved between blocks!) -# #CODE [objname, objname, ...] - This designates a code block that will be executed like a +# #CODE (infotext) [objname, objname, ...] - This designates a code block that will be executed like a # stand-alone piece of code together with any #HEADER -# defined. s mark the (variable-)names of objects created in the code, +# defined. +# infotext is a describing text about what goes in in this block. It will be +# shown by the batchprocessing command. +# s mark the (variable-)names of objects created in the code, # and which may be auto-deleted by the processor if desired (such as when # debugging the script). E.g., if the code contains the command # myobj = create.create_object(...), you could put 'myobj' in the #CODE header # regardless of what the created object is actually called in-game. -# The following variables are automatically made available for the script: +# The following variable is automatically made available for the script: # caller - the object executing the script # -# + #HEADER -# everything in this block will be imported to all CODE blocks when -# they are executed. +# everything in this block will be appended to the beginning of +# all other #CODE blocks when they are executed. from src.utils import create, search -from game.gamesrc.typeclasses.examples import red_button -from game.gamesrc.typeclasses import basetypes +from game.gamesrc.objects.examples import red_button +from game.gamesrc.objects import baseobjects -#CODE +limbo = search.objects(caller, 'Limbo', global_search=True)[0] + + +#CODE (create red button) # This is the first code block. Within each block, python -# code works as normal. +# code works as normal. Note how we make use if imports and +# 'limbo' defined in the #HEADER block. This block's header +# offers no information about red_button variable, so it +# won't be able to be deleted in debug mode. -# get the limbo room. -limbo = search.objects(caller, 'Limbo', global_search=True)[0] -caller.msg(limbo) # create a red button in limbo red_button = create.create_object(red_button.RedButton, key="Red button", location=limbo, aliases=["button"]) @@ -55,17 +61,22 @@ red_button = create.create_object(red_button.RedButton, key="Red button", # we take a look at what we created caller.msg("A %s was created." % red_button.key) -#CODE table, chair + +#CODE (create table and chair) table, chair # this code block has 'table' and 'chair' set as deletable # objects. This means that when the batchcode processor runs in # testing mode, objects created in these variables will be deleted -# again (so as to avoid duplicate objects when testing the script). +# again (so as to avoid duplicate objects when testing the script many +# times). -limbo = search.objects(caller, 'Limbo', global_search=True)[0] caller.msg(limbo.key) -table = create.create_object(basetypes.Object, key="Table", location=limbo) -chair = create.create_object(basetypes.Object, key="Chair", location=limbo) + +# the python variables we assign to must match the ones given in the +# header for the system to be able to delete them afterwards during a +# debugging run. +table = create.create_object(baseobjects.Object, key="Table", location=limbo) +chair = create.create_object(baseobjects.Object, key="Chair", location=limbo) string = "A %s and %s were created. If debug was active, they were deleted again." caller.msg(string % (table, chair)) diff --git a/src/objects/models.py b/src/objects/models.py index 4ee9db583c..f0ce5fb93e 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -504,6 +504,7 @@ class ObjectDB(TypedObject): """ # This is an important function that must always work. # we use a different __getattribute__ to avoid recursive loops. + if from_obj: try: from_obj.at_msg_send(message, self) diff --git a/src/objects/object_search_funcs.py b/src/objects/object_search_funcs.py index 2752d8f0d0..f8c738ebc0 100644 --- a/src/objects/object_search_funcs.py +++ b/src/objects/object_search_funcs.py @@ -27,7 +27,7 @@ def handle_search_errors(emit_to_obj, ostring, results, global_search=False): dbrefs instead of only numbers) """ if not results: - emit_to_obj.emit_to("Could not find '%s'." % ostring) + emit_to_obj.msg("Could not find '%s'." % ostring) return None if len(results) > 1: # we have more than one match. We will display a @@ -47,8 +47,8 @@ def handle_search_errors(emit_to_obj, ostring, results, global_search=False): if show_dbref: dbreftext = "(#%i)" % result.id string += "\n %i-%s%s%s" % (num+1, result.name, - dbreftext, invtext) - emit_to_obj.emit_to(string) + dbreftext, invtext) + emit_to_obj.msg(string.strip()) return None else: return results[0] @@ -101,4 +101,3 @@ def object_multimatch_parser(ostring): return (None, ostring) except IndexError: return (None, ostring) - diff --git a/src/utils/batchprocessors.py b/src/utils/batchprocessors.py index 46369729ee..d16e94b657 100644 --- a/src/utils/batchprocessors.py +++ b/src/utils/batchprocessors.py @@ -147,13 +147,6 @@ from game import settings as settings_module from django.core.management import setup_environ from traceback import format_exc -# colours - -WHITE = r"%cn%ch%cw" -RED = r"%cn%ch%cr" -GREEN = r"%cn%ci%cg" -YELLOW = r"%cn%ch%cy" -NORM = r"%cn" #------------------------------------------------------------ # @@ -161,90 +154,96 @@ NORM = r"%cn" # #------------------------------------------------------------ -def read_batchcommand_file(pythonpath): +class BatchCommandProcessor(object): """ - This reads the contents of a batch-command file. - Filename is considered to be the name of the batch file - relative the directory specified in settings.py + This class implements a batch-command processor. + """ - if pythonpath and not (pythonpath.startswith('src.') or - pythonpath.startswith('game.')): - pythonpath = "%s.%s" % (settings.BASE_BATCHPROCESS_PATH, - pythonpath) - abspath = utils.pypath_to_realpath(pythonpath, 'ev') - try: - fobj = open(abspath) - except IOError: - logger.log_errmsg("Could not open path '%s'." % pythonpath) - return None - lines = fobj.readlines() - fobj.close() - return lines - -def parse_batchcommand_file(pythonpath): - """ - This parses the lines of a batchfile according to the following - rules: - 1) # at the beginning of a line marks the end of the command before it. - It is also a comment and any number of # can exist on subsequent - lines (but not inside comments). - 2) Commands are placed alone at the beginning of a line and their - arguments are considered to be everything following (on any - number of lines) until the next comment line beginning with #. - 3) Newlines are ignored in command definitions - 4) A completely empty line in a command line definition is condered - a newline (so two empty lines is a paragraph). - 5) Excess spaces and indents inside arguments are stripped. - - """ - - #helper function - def identify_line(line): - """ - Identifies the line type (comment, commanddef or empty) + def read_file(self, pythonpath): """ + This reads the contents of a batch-command file. + Filename is considered to be the name of the batch file + relative the directory specified in settings.py + """ + + if pythonpath and not (pythonpath.startswith('src.') or + pythonpath.startswith('game.')): + pythonpath = "%s.%s" % (settings.BASE_BATCHPROCESS_PATH, + pythonpath) + abspath = utils.pypath_to_realpath(pythonpath, 'ev') try: - if line.strip()[0] == '#': - return "comment" - else: - return "commanddef" - except IndexError: - return "empty" + fobj = open(abspath) + except IOError: + logger.log_errmsg("Could not open path '%s'." % abspath) + return None + lines = fobj.readlines() + fobj.close() + return lines - #read the indata, if possible. - lines = read_batchcommand_file(pythonpath) - if not lines: - return None + def parse_file(self, pythonpath): + """ + This parses the lines of a batchfile according to the following + rules: + 1) # at the beginning of a line marks the end of the command before it. + It is also a comment and any number of # can exist on subsequent + lines (but not inside comments). + 2) Commands are placed alone at the beginning of a line and their + arguments are considered to be everything following (on any + number of lines) until the next comment line beginning with #. + 3) Newlines are ignored in command definitions + 4) A completely empty line in a command line definition is condered + a newline (so two empty lines is a paragraph). + 5) Excess spaces and indents inside arguments are stripped. - commands = [] - curr_cmd = "" + """ - #purge all superfluous whitespace and newlines from lines - reg1 = re.compile(r"\s+") - lines = [reg1.sub(" ", l) for l in lines] + #helper function + def identify_line(line): + """ + Identifies the line type (comment, commanddef or empty) + """ + try: + if line.strip()[0] == '#': + return "comment" + else: + return "commanddef" + except IndexError: + return "empty" - #parse all command definitions into a list. - for line in lines: - typ = identify_line(line) - if typ == "commanddef": - curr_cmd += line - elif typ == "empty" and curr_cmd: - curr_cmd += "\r\n" - else: #comment - if curr_cmd: - commands.append(curr_cmd.strip()) - curr_cmd = "" - if curr_cmd: - commands.append(curr_cmd.strip()) + #read the indata, if possible. + lines = self.read_file(pythonpath) + if not lines: + return None - #second round to clean up now merged line edges etc. - reg2 = re.compile(r"[ \t\f\v]+") - commands = [reg2.sub(" ", c) for c in commands] + commands = [] + curr_cmd = "" - #remove eventual newline at the end of commands - commands = [c.strip('\r\n') for c in commands] - return commands + #purge all superfluous whitespace and newlines from lines + reg1 = re.compile(r"\s+") + lines = [reg1.sub(" ", l) for l in lines] + + #parse all command definitions into a list. + for line in lines: + typ = identify_line(line) + if typ == "commanddef": + curr_cmd += line + elif typ == "empty" and curr_cmd: + curr_cmd += "\r\n" + else: #comment + if curr_cmd: + commands.append(curr_cmd.strip()) + curr_cmd = "" + if curr_cmd: + commands.append(curr_cmd.strip()) + + #second round to clean up now merged line edges etc. + reg2 = re.compile(r"[ \t\f\v]+") + commands = [reg2.sub(" ", c) for c in commands] + + #remove eventual newline at the end of commands + commands = [c.strip('\r\n') for c in commands] + return commands #------------------------------------------------------------ # @@ -252,148 +251,170 @@ def parse_batchcommand_file(pythonpath): # #------------------------------------------------------------ -def read_batchcode_file(pythonpath): + +class BatchCodeProcessor(object): """ - This reads the contents of batchfile. - Filename is considered to be the name of the batch file - relative the directory specified in settings.py - """ - - if pythonpath and not (pythonpath.startswith('src.') or - pythonpath.startswith('game.')): - pythonpath = "%s.%s" % (settings.BASE_BATCHPROCESS_PATH, - pythonpath) - abspath = utils.pypath_to_realpath(pythonpath) - try: - fobj = open(abspath) - except IOError: - logger.log_errmsg("Could not open path '%s'." % pythonpath) - return None - lines = fobj.readlines() - fobj.close() - return lines - - -def parse_batchcode_file(pythonpath): - """ - This parses the lines of a batchfile according to the following - rules: - - 1) Lines starting with #HEADER starts a header block (ends other blocks) - 2) Lines starting with #CODE begins a code block (ends other blocks) - 3) All lines outside blocks are stripped. - 4) All excess whitespace beginning/ending a block is stripped. - + This implements a batch-code processor + """ - # helper function - def parse_line(line): + def read_file(self, pythonpath): """ - Identifies the line type: block command, comment, empty or normal code. - + This reads the contents of batchfile. + Filename is considered to be the name of the batch file + relative the directory specified in settings.py """ - line = line.strip() - - if line.startswith("#HEADER"): - return "header", "" - elif line.startswith("#CODE"): - # parse code command - line = line.lstrip("#CODE").strip() - objs = [] - if line: - objs = [obj.strip() for obj in line.split(',')] - return "code", objs - elif line.startswith("#"): - return "comment", "" - else: - #normal line - return it with a line break. - return None, "\n%s" % line - # read indata + if pythonpath and not (pythonpath.startswith('src.') or + pythonpath.startswith('game.')): + pythonpath = "%s.%s" % (settings.BASE_BATCHPROCESS_PATH, + pythonpath) + abspath = utils.pypath_to_realpath(pythonpath, 'py') + try: + fobj = open(abspath) + except IOError: + logger.log_errmsg("Could not open path '%s'." % abspath) + return None + lines = fobj.readlines() + fobj.close() + return lines - lines = read_batchcode_file(pythonpath) - if not lines: - return None - - # parse file into blocks - - header = "" - codes = [] - - in_header = False - in_code = False - for line in lines: - # parse line - mode, line = parse_line(line) - # try: - # print "::", in_header, in_code, mode, line.strip() - # except: - # print "::", in_header, in_code, mode, line - if mode == 'comment': - continue - elif mode == 'header': - in_header = True - in_code = False - elif mode == 'code': - in_header = False - in_code = True - # the line is a list of object variable names - # (or an empty list) at this point. - codedict = {'objs':line, - 'code':""} - codes.append(codedict) - else: - # another type of line (empty or code) - if in_header: - header += line - elif in_code: - codes[-1]['code'] += line + def parse_file(self, pythonpath): + """ + This parses the lines of a batchfile according to the following + rules: + + 1) Lines starting with #HEADER starts a header block (ends other blocks) + 2) Lines starting with #CODE begins a code block (ends other blocks) + 3) #CODE headers may be of the following form: #CODE (info) objname, objname2, ... + 3) All lines outside blocks are stripped. + 4) All excess whitespace beginning/ending a block is stripped. + + """ + + # helper function + def parse_line(line): + """ + Identifies the line type: block command, comment, empty or normal code. + + """ + line = line.strip() + + if line.startswith("#HEADER"): + return ("header", "", "") + elif line.startswith("#CODE"): + # parse code command + line = line.lstrip("#CODE").strip() + objs = [] + info = "" + if line and '(' in line and ')' in line: + # a code description + lp = line.find('(') + rp = line.find(')') + info = line[lp:rp+1] + line = line[rp+1:] + if line: + objs = [obj.strip() for obj in line.split(',')] + return ("codeheader", info, objs) + elif line.startswith('#'): + return ('comment', "", "\n%s" % line) else: - # not in a block (e.g. first in file). Ignore. + #normal line - return it with a line break. + return ('line', "", "\n%s" % line) + + # read indata + + lines = self.read_file(pythonpath) + if not lines: + return None + + # parse file into blocks + + header = "" + codes = [] + + in_header = False + in_code = False + + for line in lines: + # parse line + mode, info, line = parse_line(line) + # try: + # print "::", in_header, in_code, mode, line.strip() + # except: + # print "::", in_header, in_code, mode, line + if mode == 'header': + in_header = True + in_code = False + elif mode == 'codeheader': + in_header = False + in_code = True + # the line is a list of object variable names + # (or an empty list) at this point. + codedict = {'objs':line, + 'info':info, + 'code':""} + codes.append(codedict) + elif mode == 'comment' and in_header: continue - # last, we merge the headers with all codes. - for codedict in codes: - codedict["firstline"] = codedict["code"].strip()[:min(35, len(codedict['code'].strip())-1)] - codedict["code"] = "%s\n%s" % (header, codedict["code"]) - return codes + else: + # another type of line (empty, comment or code) + if line and in_header: + header += line + elif line and in_code: + codes[-1]['code'] += line + else: + # not in a block (e.g. first in file). Ignore. + continue -def batch_code_exec(codedict, extra_environ=None, debug=False): - """ - Execute a single code block, including imports and appending global vars + # last, we merge the headers with all codes. + for codedict in codes: + codedict["code"] = "#CODE %s %s\n%s\n\n%s" % (codedict['info'], + ", ".join(codedict["objs"]), + header.strip(), + codedict["code"].strip()) + return codes - extra_environ - dict with environment variables - """ + def code_exec(self, codedict, extra_environ=None, debug=False): + """ + Execute a single code block, including imports and appending global vars - # define the execution environment - environ = "setup_environ(settings_module)" - environdict = {"setup_environ":setup_environ, - "settings_module":settings_module} - if extra_environ: - for key, value in extra_environ.items(): - environdict[key] = value + extra_environ - dict with environment variables + """ - # merge all into one block - code = "%s\n%s" % (environ, codedict['code']) - if debug: - # try to delete marked objects - for obj in codedict['objs']: - code += "\ntry: %s.delete()\nexcept: pass" % obj - - # execute the block - try: - exec(code, environdict) - except Exception: - errlist = format_exc().split('\n') - if len(errlist) > 4: - errlist = errlist[4:] - err = "\n".join("<<< %s" % line for line in errlist if line) + # define the execution environment + environ = "setup_environ(settings_module)" + environdict = {"setup_environ":setup_environ, + "settings_module":settings_module} + if extra_environ: + for key, value in extra_environ.items(): + environdict[key] = value + + # merge all into one block + code = "%s\n%s" % (environ, codedict['code']) if debug: - # try to delete objects again. - try: - for obj in codedict['objs']: - eval("%s.delete()" % obj, environdict) - except Exception: - pass - return err - return None + # try to delete marked objects + for obj in codedict['objs']: + code += "\ntry: %s.delete()\nexcept: pass" % obj + + # execute the block + try: + exec(code, environdict) + except Exception: + errlist = format_exc().split('\n') + if len(errlist) > 4: + errlist = errlist[4:] + err = "\n".join(" %s" % line for line in errlist if line) + if debug: + # try to delete objects again. + try: + for obj in codedict['objs']: + eval("%s.delete()" % obj, environdict) + except Exception: + pass + return err + return None + +BATCHCMD = BatchCommandProcessor() +BATCHCODE = BatchCodeProcessor()