From 0dc62a5fc93b43c25113edce77a6c0b93a573e1a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 20 Apr 2014 23:06:10 +0200 Subject: [PATCH] Refactored batch processors, addressing points in #489. --- game/gamesrc/world/examples/batch_code.py | 62 +++--- src/commands/default/batchprocess.py | 23 +-- src/server/portal/telnet.py | 2 +- src/utils/batchprocessors.py | 230 +++++++--------------- 4 files changed, 111 insertions(+), 206 deletions(-) diff --git a/game/gamesrc/world/examples/batch_code.py b/game/gamesrc/world/examples/batch_code.py index fe43209a40..e23c0652b8 100644 --- a/game/gamesrc/world/examples/batch_code.py +++ b/game/gamesrc/world/examples/batch_code.py @@ -52,34 +52,34 @@ from ev import Object limbo = search_object('Limbo')[0] -#CODE (create red button) - -# This is the first code block. Within each block, python -# 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. - -# create a red button in limbo -red_button = create_object(red_button.RedButton, key="Red button", - location=limbo, aliases=["button"]) - -# we take a look at what we created -caller.msg("A %s was created." % red_button.key) - -#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 many -# times). - -# 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_object(Object, key="Table", location=limbo) -chair = create_object(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)) +##CODE (create red button) +# +## This is the first code block. Within each block, python +## 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. +# +## create a red button in limbo +#red_button = create_object(red_button.RedButton, key="Red button", +# location=limbo, aliases=["button"]) +# +## we take a look at what we created +#caller.msg("A %s was created." % red_button.key) +# +##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 many +## times). +# +## 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_object(Object, key="Table", location=limbo) +#chair = create_object(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/commands/default/batchprocess.py b/src/commands/default/batchprocess.py index fed75d9a4d..d84aa357b1 100644 --- a/src/commands/default/batchprocess.py +++ b/src/commands/default/batchprocess.py @@ -134,10 +134,10 @@ def batch_code_exec(caller): ptr = caller.ndb.batch_stackptr stack = caller.ndb.batch_stack debug = caller.ndb.batch_debug - codedict = stack[ptr] + code = stack[ptr] - caller.msg(format_header(caller, codedict['code'])) - err = BATCHCODE.code_exec(codedict, + caller.msg(format_header(caller, code)) + err = BATCHCODE.code_exec(code, extra_environ={"caller": caller}, debug=debug) if err: caller.msg(format_code(err)) @@ -177,14 +177,8 @@ def show_curr(caller, showall=False): entry = stack[stackptr] - if type(entry) == dict: - # this is a batch-code entry - string = format_header(caller, entry['code']) - codeall = entry['code'].strip() - else: - # this is a batch-cmd entry - string = format_header(caller, entry) - codeall = entry.strip() + string = format_header(caller, entry) + codeall = entry.strip() string += "{G(hh for help)" if showall: for line in codeall.split('\n'): @@ -349,10 +343,11 @@ class CmdBatchCode(MuxCommand): caller.msg("Usage: @batchcode[/interactive/debug] ") return python_path = self.args + debug = 'debug' in self.switches #parse indata file try: - codes = BATCHCODE.parse_file(python_path) + codes = BATCHCODE.parse_file(python_path, debug=debug) except UnicodeDecodeError, err: lnum = err.linenum caller.msg(_UTF8_ERROR % (python_path, lnum)) @@ -365,10 +360,6 @@ class CmdBatchCode(MuxCommand): return switches = self.switches - debug = False - if 'debug' in switches: - debug = True - # Store work data in cache caller.ndb.batch_stack = codes caller.ndb.batch_stackptr = 0 diff --git a/src/server/portal/telnet.py b/src/server/portal/telnet.py index f4294d02a0..3ff53b3bb1 100644 --- a/src/server/portal/telnet.py +++ b/src/server/portal/telnet.py @@ -89,7 +89,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): the disconnect method """ self.sessionhandler.disconnect(self) - self.transport.close() + self.transport.loseConnection() def dataReceived(self, data): """ diff --git a/src/utils/batchprocessors.py b/src/utils/batchprocessors.py index bff1a5f135..27a6b92a94 100644 --- a/src/utils/batchprocessors.py +++ b/src/utils/batchprocessors.py @@ -201,52 +201,31 @@ def read_batchfile(pythonpath, file_ending='.py'): abspaths.append(utils.pypath_to_realpath("%s.%s" % (basepath, pythonpath), file_ending)) else: abspaths = [utils.pypath_to_realpath(pythonpath, file_ending)] - fobj, lines, err = None, [], None - for file_encoding in ENCODINGS: - # try different encodings, in order - load_errors = [] - for abspath in abspaths: - # try different paths, until we get a match + text, fobj = None, None + fileerr, decoderr = [], [] + for abspath in abspaths: + # try different paths, until we get a match + # we read the file directly into unicode. + for file_encoding in ENCODINGS: + # try different encodings, in order try: - # we read the file directly into unicode. fobj = codecs.open(abspath, 'r', encoding=file_encoding) - except IOError: - load_errors.append("Could not open batchfile '%s'." % abspath) + text = fobj.read() + except IOError, e: + # could not find the file + fileerr.append(str(e)) + break + except (ValueError, UnicodeDecodeError), e: + # this means an encoding error; try another encoding + decoderr.append(str(e)) continue break - if not fobj: - continue + if not fobj: + raise IOError("\n".join(fileerr)) + if not text: + raise UnicodeDecodeError("\n".join(decoderr)) - load_errors = [] - err = None - # We have successfully found and opened the file. Now actually - # try to decode it using the given protocol. - try: - lines = fobj.readlines() - except UnicodeDecodeError: - # give the line of failure - fobj.seek(0) - try: - lnum = 0 - for lnum, line in enumerate(fobj): - pass - except UnicodeDecodeError, err: - # lnum starts from 0, so we add +1 line, - # besides the faulty line is never read - # so we add another 1 (thus +2) to get - # the actual line number seen in an editor. - err.linenum = lnum + 2 - fobj.close() - # possibly try another encoding - continue - # if we get here, the encoding worked. Stop iteration. - break - if load_errors: - logger.log_errmsg("\n".join(load_errors)) - if err: - return err - else: - return lines + return text #------------------------------------------------------------ @@ -293,6 +272,7 @@ class BatchCommandProcessor(object): #remove eventual newline at the end of commands commands = [c.strip('\r\n') for c in commands] commands = [c for c in commands if c] + return commands @@ -319,7 +299,7 @@ class BatchCodeProcessor(object): """ - def parse_file(self, pythonpath): + def parse_file(self, pythonpath, debug=False): """ This parses the lines of a batchfile according to the following rules: @@ -334,127 +314,73 @@ class BatchCodeProcessor(object): """ - # helper function - def parse_line(line): - """ - Identifies the line type: - block command, comment, empty or normal code. - """ - parseline = line.strip() + text = "".join(read_batchfile(pythonpath, file_ending='.py')) - if parseline.startswith("#HEADER"): - return ("header", "", "") - if parseline.startswith("#INSERT"): - filename = line.lstrip("#INSERT").strip() - if filename: - return ('insert', "", filename) - else: - return ('comment', "", "{r#INSERT {n") - elif parseline.startswith("#CODE"): - # parse code command - line = line.lstrip("#CODE").strip() - info = CODE_INFO_HEADER.findall(line) or "" - if info: - info = info[0] - line = line.replace(info, "") - objs = [o.strip() for o in line.split(",") if o.strip()] - return ("codeheader", info, objs) - elif parseline.startswith('#'): - return ('comment', "", "%s" % line) - else: - #normal line - return it with a line break. - return ('line', "", "%s" % line) + def clean_block(text): + text = re.sub(r"^\#.*?$|^\s*$", "", text, flags=re.MULTILINE) + return "\n".join([line for line in text.split("\n") if line]) - # read indata + def replace_insert(match): + "Map replace entries" + return "\#\n".join(self.parse_file(match.group())) - lines = read_batchfile(pythonpath, file_ending='.py') - if not lines: - return None + text = re.sub(r"^\#INSERT (.*?)", replace_insert, text, flags=re.MULTILINE) + blocks = re.split(r"(^\#CODE.*?$|^\#HEADER)$", text, flags=re.MULTILINE) + headers = [] + codes = [] # list of tuples (code, info, objtuple) + if blocks: + if blocks[0]: + # the first block is either empty or an unmarked code block + code = clean_block(blocks.pop(0)) + if code: + codes.append((code, "")) + iblock = 0 + for block in blocks[::2]: + # loop over every second component; these are the #CODE/#HEADERs + if block.startswith("#HEADER"): + headers.append(clean_block(blocks[iblock + 1])) + elif block.startswith("#CODE"): + match = re.search(r"\(.*?\)", block) + info = match.group() if match else "" + objs = [] + if debug: + # insert auto-delete lines into code + objs = block[match.end():].split(",") + objs = ["# added by Evennia's debug mode\n%s.delete()" % obj.strip() for obj in objs if obj] + # build the code block + code = "\n".join([clean_block(blocks[iblock + 1])] + objs) + if code: + codes.append((code, info)) + iblock += 2 - # 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 == 'insert': - # recursive load of inserted code files - note that we - # are not checking for cyclic imports! - in_header = False - in_code = False - inserted_codes = self.parse_file(line) or [{'objs': "", 'info': line, 'code': ""}] - for codedict in inserted_codes: - codedict["inserted"] = True - codes.extend(inserted_codes) - elif 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 - 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 - - # last, we merge the headers with all codes. - for codedict in codes: - #print "codedict:", codedict - if codedict and "inserted" in codedict: - # we don't need to merge code+header in this case - # since that was already added in the recursion. We - # just check for errors. - if not codedict['code']: - codedict['code'] = "{r#INSERT ERROR: %s{n" % codedict['info'] - else: - objs = ", ".join(codedict["objs"]) - if objs: - objs = "[%s]" % objs - codedict["code"] = "#CODE %s %s \n%s\n\n%s" % (codedict['info'], - objs, - header.strip(), - codedict["code"].strip()) + # join the headers together to one header + headers = "\n".join(headers) + if codes: + # add the headers at the top of each non-empty block + codes = ["%s\n%s\n%s" % ("#CODE %s: " % tup[1], headers, tup[0]) for tup in codes if tup[0]] + else: + codes = ["#CODE: \n" + headers] return codes - def code_exec(self, codedict, extra_environ=None, debug=False): + + def code_exec(self, code, extra_environ=None, debug=False): """ Execute a single code block, including imports and appending global vars extra_environ - dict with environment variables """ # define the execution environment - environ = "settings_module.configure()" environdict = {"settings_module": settings} + environ = "settings_module.configure()" if extra_environ: for key, value in extra_environ.items(): environdict[key] = value - # merge all into one block - code = "# auto-added by Evennia\ntry:%s\nexcept RuntimeError:pass\nfinally:del settings_module\n%s" % (environ, codedict['code']) - if debug: - # try to delete marked objects - for obj in codedict['objs']: - code += "\ntry: %s.delete()\nexcept: pass" % obj + # initializing the django settings at the top of code + code = "# auto-added by Evennia\n" \ + "try: %s\n" \ + "except RuntimeError: pass\n" \ + "finally: del settings_module\n%s" % (environ, code) # execute the block try: @@ -475,18 +401,6 @@ class BatchCodeProcessor(object): err += "\n%02i: %s" % (iline + 1, line) err += "\n".join(traceback.format_exception(etype, value, tb)) - #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