Add some location information to exceptions

New parser errors come with location information where the parse failure
occurred

Also, some formatting

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2022-09-24 15:08:14 -07:00
parent 872914f8bd
commit 2e8c1187a7

View File

@@ -1,22 +1,22 @@
#!/usr/bin/env python3
#
# Copyright (c) 2022 Alek Ratzloff
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import dataclasses
import functools
import random
import re
import string
from typing import Optional, Protocol
@@ -34,6 +34,25 @@ ESCAPES = {
}
@dataclasses.dataclass
class Pos:
source: int
col: int
line: int
def __str__(self) -> str:
return f"line {self.line} col {self.col}"
class MarkupError(Exception):
def __init__(self, where: Pos, message: str):
self.where = where
self.message = message
def __str__(self) -> str:
return f"{self.where}: {self.message}"
class MarkupCallable(Protocol):
"""
Callable for macros.
@@ -52,7 +71,7 @@ class MarkupContext:
def get_macro(self, name: str) -> MarkupCallable:
if name not in self.macros:
raise Exception(f"Could not find macro with name `{name}`")
raise Exception(f"could not find macro with name `{name}`")
return self.macros[name]
@@ -83,13 +102,16 @@ class MarkupMacro(MarkupExpr):
class MarkupParser:
def __init__(self, text: str):
self.text = text
self.pos = 0
self.pos = Pos(source=0, col=1, line=1)
self.parts: list[MarkupExpr] = []
@property
def c(self) -> str:
return self.ahead(1)
def ahead(self, count: int) -> str:
return self.text[self.pos.source : self.pos.source + count]
def render(self, ctx: MarkupContext) -> str:
self.parse()
return "".join([exp.render(ctx) for exp in self.parts])
@@ -121,15 +143,16 @@ class MarkupParser:
return MarkupMacro(macro, args)
def parse_expr(self) -> MarkupExpr:
if self.c in ["\"", "\'"]:
if self.c in ['"', "'"]:
return MarkupRaw(self.parse_macro_string())
elif self.c in NAME_CHARS:
return MarkupRaw(self.parse_ident())
elif self.is_match("${"):
return self.parse_macro()
else:
raise Exception(
"lost the thread around {self.text[self.pos:self.pos + 5]}..."
raise MarkupError(
self.pos,
f"expected expression (string, ident, or `${{`) but got character {self.c} instead",
)
def parse_macro_string(self) -> str:
@@ -137,29 +160,31 @@ class MarkupParser:
if self.c in ['"', "'"]:
self.adv()
else:
raise Exception("expected single quote (\") or double quote (')")
raise MarkupError(
self.pos, "expected single quote (\") or double quote (')"
)
text = ""
while self.c and self.c != start:
if self.c == "\\":
self.adv()
if self.c in "nrbt\\'\"":
if self.c in ESCAPES:
text += ESCAPES[self.c]
self.adv()
else:
raise Exception(f"unknown escape `\\{self.c}`")
raise MarkupError(self.pos, f"unknown escape `\\{self.c}`")
else:
text += self.c
self.adv()
if not self.c:
raise Exception("unterminated string")
raise MarkupError(self.pos, "unterminated string")
self.adv()
return text
def parse_ident(self) -> str:
start = self.pos
start = self.pos.source
while self.c and self.c in NAME_CHARS:
self.adv()
return self.text[start : self.pos]
return self.text[start : self.pos.source]
def skip_whitespace(self):
while self.c and self.c in string.whitespace:
@@ -172,17 +197,21 @@ class MarkupParser:
self.adv()
return MarkupRaw(text)
def ahead(self, count: int) -> str:
return self.text[self.pos : self.pos + count]
def adv(self, n: int = 1):
self.pos += n
# only advance if there's characters left to consume
if not self.c:
return
for i in range(n):
if self.c == "\n":
self.pos.line += 1
self.pos.col = 1
self.pos.source += 1
def match(self, c: str):
if self.is_match(c):
self.adv(len(c))
else:
raise Exception(f"expected `{c}`")
raise MarkupError(self.pos, f"expected `{c}`")
def is_match(self, c: str) -> bool:
if c == self.ahead(len(c)):
@@ -210,7 +239,10 @@ def render(text: str, ctx: Optional[MarkupContext] = None) -> str:
def macro_random(
ctx: MarkupContext, first: Optional[str] = None, second: Optional[str] = None, *tail: str
ctx: MarkupContext,
first: Optional[str] = None,
second: Optional[str] = None,
*tail: str,
) -> str:
"""
Choose a random number.
@@ -248,7 +280,7 @@ def macro_round(ctx: MarkupContext, n: str, digits: Optional[str] = None) -> str
def macro_cat(ctx: MarkupContext, *text: str) -> str:
"Concatenates all arguments."
return ''.join(text)
return "".join(text)
@functools.cache
@@ -266,9 +298,10 @@ def default_context():
################################################################################
# Main
################################################################################
if __name__ == '__main__':
if __name__ == "__main__":
import sys
filename = '<stdin>'
filename = "<stdin>"
if len(sys.argv) == 1:
text = sys.stdin.read()
else:
@@ -277,6 +310,6 @@ if __name__ == '__main__':
text = fp.read()
try:
result = render(text)
print(result, end='')
print(result, end="")
except Exception as ex:
print(f"{filename}: {ex}")