Use StackItem and FunctionStackItem with script exception calls

This commit is contained in:
Bob Swift
2020-03-05 15:16:14 -07:00
parent 61d9196b92
commit d5de2dc159
2 changed files with 86 additions and 32 deletions

View File

@@ -59,7 +59,14 @@ class ScriptError(Exception):
class ScriptParseError(ScriptError):
pass
def __init__(self, message, stackitem):
super().__init__(
"{0} at position {1}, line {2}".format(
message,
stackitem.line,
stackitem.column
)
)
class ScriptEndOfFile(ScriptParseError):
@@ -70,13 +77,32 @@ class ScriptSyntaxError(ScriptParseError):
pass
class ScriptUnknownFunction(ScriptError):
class ScriptUnknownFunction(ScriptParseError):
pass
class ScriptRuntimeError(ScriptError):
def __init__(self, message, name, line, position):
super().__init__("${1:s}:{2:d}:{3:d}: {0:s}".format(message, name, line, position))
def __init__(self, message, functionstackitem):
super().__init__(
"{0}:{1}:${2}: {3}".format(
functionstackitem.line,
functionstackitem.column,
functionstackitem.name,
message
)
)
class StackItem:
def __init__(self, line, column):
self.line = line
self.column = column
class FunctionStackItem(StackItem):
def __init__(self, line, column, name):
super().__init__(line, column)
self.name = name
class ScriptText(str):
@@ -107,12 +133,13 @@ FunctionRegistryItem = namedtuple("FunctionRegistryItem",
["function", "eval_args",
"argcount"])
Bound = namedtuple("Bound", ["lower", "upper"])
FunctionStackItem = namedtuple("FunctionStackItem", ["function", "column", "line"])
class ScriptFunction(object):
def __init__(self, name, args, parser, column=0, line=0):
self.stackitem = StackItem(line, column)
self.functionstackitem = FunctionStackItem(line, column, name)
try:
argnum_bound = parser.functions[name].argcount
argcount = len(args)
@@ -129,16 +156,16 @@ class ScriptFunction(object):
too_many_args = False
if too_few_args or too_many_args:
raise ScriptError(
"Wrong number of arguments for $%s: Expected %s, got %i at position %i, line %i"
% (name, expected, argcount, parser._x, parser._y)
raise ScriptSyntaxError(
"Wrong number of arguments for $%s: Expected %s, got %i"
% (name, expected, argcount),
StackItem(parser._y, parser._x)
)
except KeyError:
raise ScriptUnknownFunction("Unknown function '%s'" % name)
raise ScriptUnknownFunction("Unknown function '%s'" % name, self.stackitem)
self.name = name
self.args = args
self.function = FunctionStackItem(name, column, line)
def __repr__(self):
return "<ScriptFunction $%s(%r)>" % (self.name, self.args)
@@ -147,13 +174,13 @@ class ScriptFunction(object):
try:
function, eval_args, num_args = parser.functions[self.name]
except KeyError:
raise ScriptUnknownFunction("Unknown function '%s'" % self.name)
raise ScriptUnknownFunction("Unknown function '%s'" % self.name, self.stackitem)
if eval_args:
args = [arg.eval(parser) for arg in self.args]
else:
args = self.args
parser._function_stack.put(self.function)
parser._function_stack.put(self.functionstackitem)
# Save return value to allow removing function from the stack on successful completion
return_value = function(parser, *args)
parser._function_stack.get()
@@ -191,13 +218,13 @@ Grammar:
self._function_stack = LifoQueue()
def __raise_eof(self):
raise ScriptEndOfFile("Unexpected end of script at position %d, line %d" % (self._x, self._y))
raise ScriptEndOfFile("Unexpected end of script", StackItem(self._y, self._x))
def __raise_char(self, ch):
#line = self._text[self._line:].split("\n", 1)[0]
#cursor = " " * (self._pos - self._line - 1) + "^"
#raise ScriptSyntaxError("Unexpected character '%s' at position %d, line %d\n%s\n%s" % (ch, self._x, self._y, line, cursor))
raise ScriptSyntaxError("Unexpected character '%s' at position %d, line %d" % (ch, self._x, self._y))
raise ScriptSyntaxError("Unexpected character '%s'", StackItem(self._y, self._x))
def read(self):
try:
@@ -242,7 +269,7 @@ Grammar:
if ch == '(':
name = self._text[start:self._pos-1]
if name not in self.functions:
raise ScriptUnknownFunction("Unknown function '%s'" % name)
raise ScriptUnknownFunction("Unknown function '%s'" % name, StackItem(line, column))
return ScriptFunction(name, self.parse_arguments(), self, column, line)
elif ch is None:
self.__raise_eof()
@@ -1284,12 +1311,10 @@ def func_datetime(parser, format=None):
try:
return datetime.datetime.now(tz=local_tz).strftime(format)
except ValueError:
function = parser._function_stack.get()
functionstackitem = parser._function_stack.get()
raise ScriptRuntimeError(
"Unsupported format code",
function.function,
function.line,
function.column
functionstackitem
)

View File

@@ -156,7 +156,7 @@ class ScriptParserTest(PicardTestCase):
def somefunc4(parser):
return "x"
self.assertScriptResultEquals("$otherfunc()", "x")
areg = "^Unknown function 'somefunc'$"
areg = "^Unknown function 'somefunc'"
with self.assertRaisesRegex(ScriptError, areg):
self.parser.eval("$somefunc()")
@@ -175,12 +175,12 @@ class ScriptParserTest(PicardTestCase):
self.assertScriptResultEquals("$somefunc($title(x))", "X")
def test_unknown_function(self):
areg = r"^Unknown function 'unknownfunction'$"
areg = r"^Unknown function 'unknownfunction'"
with self.assertRaisesRegex(ScriptError, areg):
self.parser.eval("$unknownfunction()")
def test_noname_function(self):
areg = r"^Unknown function ''$"
areg = r"^Unknown function ''"
with self.assertRaisesRegex(ScriptError, areg):
self.parser.eval("$()")
@@ -199,10 +199,10 @@ class ScriptParserTest(PicardTestCase):
def test_scriptfunction_unknown(self):
parser = ScriptParser()
parser.parse('')
areg = r"^Unknown function 'x'$"
areg = r"^Unknown function 'x'"
with self.assertRaisesRegex(ScriptError, areg):
ScriptFunction('x', '', parser)
areg = r"^Unknown function 'noop'$"
areg = r"^Unknown function 'noop'"
with self.assertRaisesRegex(ScriptError, areg):
f = ScriptFunction('noop', '', parser)
del parser.functions['noop']
@@ -1200,24 +1200,53 @@ class ScriptParserTest(PicardTestCase):
try:
context = Metadata()
areg = r"^\$datetime:1:\d+: Unsupported format code"
areg = r"^1:\d+:\$datetime: Unsupported format code"
# Tests with invalid format code (platform dependent tests)
for test_case in tests_to_run:
with self.assertRaisesRegex(ScriptRuntimeError, areg):
self.parser.eval(r'$datetime(\{0})'.format(test_case))
finally:
# Restore original datetime object
datetime.datetime = original_datetime
def test_scriptruntimeerror(self):
# Platform dependent testing because different platforms (both os and Python version)
# support some format arguments differently. Use $datetime function to generate exceptions.
possible_tests = (
'%', # Hanging % at end of format
'%-d', # Non zero-padded day
'%-m', # Non zero-padded month
'%3Y', # Length specifier shorter than string
)
test_to_run = ''
# Get list of tests for unsupported format codes
for test_case in possible_tests:
try:
datetime.datetime.now().strftime(test_case)
except ValueError:
test_to_run = test_case
break
if not test_to_run:
self.skipTest('no test found to generate ScriptRuntimeError')
# Save original datetime object and substitute one returning
# a fixed now() value for testing.
original_datetime = datetime.datetime
datetime.datetime = _DateTime
try:
context = Metadata()
# Test that the correct position number is passed
test_string = r'$noop()$datetime(\{0})'.format(test_case)
areg = r"^\$datetime:\d+:7: Unsupported format code"
areg = r"^\d+:7:\$datetime: Unsupported format code"
with self.assertRaisesRegex(ScriptRuntimeError, areg):
self.parser.eval(test_string)
self.parser.eval(r'$noop()$datetime(\{0})'.format(test_to_run))
# Test that the function stack is returning the correct name (nested functions)
areg = r"^\$datetime:\d+:\d+: Unsupported format code"
areg = r"^\d+:\d+:\$datetime: Unsupported format code"
with self.assertRaisesRegex(ScriptRuntimeError, areg):
self.parser.eval(r'$set(foo,$datetime($if(,,\{0})))'.format(test_case))
self.parser.eval(r'$set(foo,$datetime($if(,,\{0})))'.format(test_to_run))
# Test that the correct line number is passed
areg = r"^\$datetime:2:\d+: Unsupported format code"
areg = r"^2:\d+:\$datetime: Unsupported format code"
with self.assertRaisesRegex(ScriptRuntimeError, areg):
self.parser.eval('$noop(\n)$datetime($if(,,\\{0})))'.format(test_case))
self.parser.eval('$noop(\n)$datetime($if(,,\\{0})))'.format(test_to_run))
finally:
# Restore original datetime object
datetime.datetime = original_datetime