Refactored batch processors, addressing points in #489.

This commit is contained in:
Griatch 2014-04-20 23:06:10 +02:00
parent 792b3c9282
commit 0dc62a5fc9
4 changed files with 111 additions and 206 deletions

View file

@ -52,34 +52,34 @@ from ev import Object
limbo = search_object('Limbo')[0] limbo = search_object('Limbo')[0]
#CODE (create red button) ##CODE (create red button)
#
# This is the first code block. Within each block, python ## This is the first code block. Within each block, python
# code works as normal. Note how we make use if imports and ## code works as normal. Note how we make use if imports and
# 'limbo' defined in the #HEADER block. This block's header ## 'limbo' defined in the #HEADER block. This block's header
# offers no information about red_button variable, so it ## offers no information about red_button variable, so it
# won't be able to be deleted in debug mode. ## won't be able to be deleted in debug mode.
#
# create a red button in limbo ## create a red button in limbo
red_button = create_object(red_button.RedButton, key="Red button", #red_button = create_object(red_button.RedButton, key="Red button",
location=limbo, aliases=["button"]) # location=limbo, aliases=["button"])
#
# we take a look at what we created ## we take a look at what we created
caller.msg("A %s was created." % red_button.key) #caller.msg("A %s was created." % red_button.key)
#
#CODE (create table and chair) table, chair ##CODE (create table and chair) table, chair
#
# this code block has 'table' and 'chair' set as deletable ## this code block has 'table' and 'chair' set as deletable
# objects. This means that when the batchcode processor runs in ## objects. This means that when the batchcode processor runs in
# testing mode, objects created in these variables will be deleted ## testing mode, objects created in these variables will be deleted
# again (so as to avoid duplicate objects when testing the script many ## again (so as to avoid duplicate objects when testing the script many
# times). ## times).
#
# the python variables we assign to must match the ones given in the ## 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 ## header for the system to be able to delete them afterwards during a
# debugging run. ## debugging run.
table = create_object(Object, key="Table", location=limbo) #table = create_object(Object, key="Table", location=limbo)
chair = create_object(Object, key="Chair", 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." #string = "A %s and %s were created. If debug was active, they were deleted again."
caller.msg(string % (table, chair)) #caller.msg(string % (table, chair))

View file

@ -134,10 +134,10 @@ def batch_code_exec(caller):
ptr = caller.ndb.batch_stackptr ptr = caller.ndb.batch_stackptr
stack = caller.ndb.batch_stack stack = caller.ndb.batch_stack
debug = caller.ndb.batch_debug debug = caller.ndb.batch_debug
codedict = stack[ptr] code = stack[ptr]
caller.msg(format_header(caller, codedict['code'])) caller.msg(format_header(caller, code))
err = BATCHCODE.code_exec(codedict, err = BATCHCODE.code_exec(code,
extra_environ={"caller": caller}, debug=debug) extra_environ={"caller": caller}, debug=debug)
if err: if err:
caller.msg(format_code(err)) caller.msg(format_code(err))
@ -177,14 +177,8 @@ def show_curr(caller, showall=False):
entry = stack[stackptr] entry = stack[stackptr]
if type(entry) == dict: string = format_header(caller, entry)
# this is a batch-code entry codeall = entry.strip()
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 += "{G(hh for help)" string += "{G(hh for help)"
if showall: if showall:
for line in codeall.split('\n'): for line in codeall.split('\n'):
@ -349,10 +343,11 @@ class CmdBatchCode(MuxCommand):
caller.msg("Usage: @batchcode[/interactive/debug] <path.to.file>") caller.msg("Usage: @batchcode[/interactive/debug] <path.to.file>")
return return
python_path = self.args python_path = self.args
debug = 'debug' in self.switches
#parse indata file #parse indata file
try: try:
codes = BATCHCODE.parse_file(python_path) codes = BATCHCODE.parse_file(python_path, debug=debug)
except UnicodeDecodeError, err: except UnicodeDecodeError, err:
lnum = err.linenum lnum = err.linenum
caller.msg(_UTF8_ERROR % (python_path, lnum)) caller.msg(_UTF8_ERROR % (python_path, lnum))
@ -365,10 +360,6 @@ class CmdBatchCode(MuxCommand):
return return
switches = self.switches switches = self.switches
debug = False
if 'debug' in switches:
debug = True
# Store work data in cache # Store work data in cache
caller.ndb.batch_stack = codes caller.ndb.batch_stack = codes
caller.ndb.batch_stackptr = 0 caller.ndb.batch_stackptr = 0

View file

@ -89,7 +89,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
the disconnect method the disconnect method
""" """
self.sessionhandler.disconnect(self) self.sessionhandler.disconnect(self)
self.transport.close() self.transport.loseConnection()
def dataReceived(self, data): def dataReceived(self, data):
""" """

View file

@ -201,52 +201,31 @@ def read_batchfile(pythonpath, file_ending='.py'):
abspaths.append(utils.pypath_to_realpath("%s.%s" % (basepath, pythonpath), file_ending)) abspaths.append(utils.pypath_to_realpath("%s.%s" % (basepath, pythonpath), file_ending))
else: else:
abspaths = [utils.pypath_to_realpath(pythonpath, file_ending)] abspaths = [utils.pypath_to_realpath(pythonpath, file_ending)]
fobj, lines, err = None, [], None text, fobj = None, None
for file_encoding in ENCODINGS: fileerr, decoderr = [], []
# try different encodings, in order for abspath in abspaths:
load_errors = [] # try different paths, until we get a match
for abspath in abspaths: # we read the file directly into unicode.
# try different paths, until we get a match for file_encoding in ENCODINGS:
# try different encodings, in order
try: try:
# we read the file directly into unicode.
fobj = codecs.open(abspath, 'r', encoding=file_encoding) fobj = codecs.open(abspath, 'r', encoding=file_encoding)
except IOError: text = fobj.read()
load_errors.append("Could not open batchfile '%s'." % abspath) 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 continue
break break
if not fobj: if not fobj:
continue raise IOError("\n".join(fileerr))
if not text:
raise UnicodeDecodeError("\n".join(decoderr))
load_errors = [] return text
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
#------------------------------------------------------------ #------------------------------------------------------------
@ -293,6 +272,7 @@ class BatchCommandProcessor(object):
#remove eventual newline at the end of commands #remove eventual newline at the end of commands
commands = [c.strip('\r\n') for c in commands] commands = [c.strip('\r\n') for c in commands]
commands = [c for c in commands if c] commands = [c for c in commands if c]
return commands 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 This parses the lines of a batchfile according to the following
rules: rules:
@ -334,127 +314,73 @@ class BatchCodeProcessor(object):
""" """
# helper function text = "".join(read_batchfile(pythonpath, file_ending='.py'))
def parse_line(line):
"""
Identifies the line type:
block command, comment, empty or normal code.
"""
parseline = line.strip()
if parseline.startswith("#HEADER"): def clean_block(text):
return ("header", "", "") text = re.sub(r"^\#.*?$|^\s*$", "", text, flags=re.MULTILINE)
if parseline.startswith("#INSERT"): return "\n".join([line for line in text.split("\n") if line])
filename = line.lstrip("#INSERT").strip()
if filename:
return ('insert', "", filename)
else:
return ('comment', "", "{r#INSERT <None>{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)
# read indata def replace_insert(match):
"Map replace entries"
return "\#\n".join(self.parse_file(match.group()))
lines = read_batchfile(pythonpath, file_ending='.py') text = re.sub(r"^\#INSERT (.*?)", replace_insert, text, flags=re.MULTILINE)
if not lines: blocks = re.split(r"(^\#CODE.*?$|^\#HEADER)$", text, flags=re.MULTILINE)
return None 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 # join the headers together to one header
headers = "\n".join(headers)
header = "" if codes:
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]]
in_header = False else:
in_code = False codes = ["#CODE: \n" + headers]
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())
return codes 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 Execute a single code block, including imports and appending global vars
extra_environ - dict with environment variables extra_environ - dict with environment variables
""" """
# define the execution environment # define the execution environment
environ = "settings_module.configure()"
environdict = {"settings_module": settings} environdict = {"settings_module": settings}
environ = "settings_module.configure()"
if extra_environ: if extra_environ:
for key, value in extra_environ.items(): for key, value in extra_environ.items():
environdict[key] = value environdict[key] = value
# merge all into one block # initializing the django settings at the top of code
code = "# auto-added by Evennia\ntry:%s\nexcept RuntimeError:pass\nfinally:del settings_module\n%s" % (environ, codedict['code']) code = "# auto-added by Evennia\n" \
if debug: "try: %s\n" \
# try to delete marked objects "except RuntimeError: pass\n" \
for obj in codedict['objs']: "finally: del settings_module\n%s" % (environ, code)
code += "\ntry: %s.delete()\nexcept: pass" % obj
# execute the block # execute the block
try: try:
@ -475,18 +401,6 @@ class BatchCodeProcessor(object):
err += "\n%02i: %s" % (iline + 1, line) err += "\n%02i: %s" % (iline + 1, line)
err += "\n".join(traceback.format_exception(etype, value, tb)) 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 err
return None return None