Implement file parser
This commit is contained in:
parent
92cf742126
commit
7bc9de2275
1 changed files with 314 additions and 0 deletions
314
evering/parser.py
Normal file
314
evering/parser.py
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
from abc import ABC
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from .util import *
|
||||
|
||||
"""
|
||||
This parsing solution has the following structure:
|
||||
|
||||
1. Separate header and config file content, if necessary
|
||||
2. Split up text into lines, if still necessary
|
||||
3. Parse each line individually
|
||||
4. Use a recursive descent approach to group the lines into blocks and if-blocks
|
||||
5. Evaluate the blocks recursively
|
||||
"""
|
||||
|
||||
__all__ = ["ParseException", "Parser"]
|
||||
|
||||
class ParseException(Exception):
|
||||
@classmethod
|
||||
def on_line(cls, line: "Line", text: str) -> "ParseException":
|
||||
return ParseException(f"Line {line.line_number}: {text}")
|
||||
|
||||
def split_header_and_rest(text: str) -> Tuple[List[str], List[str]]:
|
||||
lines = text.splitlines()
|
||||
|
||||
header: List[str] = []
|
||||
rest: List[str] = []
|
||||
|
||||
in_header = True
|
||||
for line in lines:
|
||||
if not in_header:
|
||||
rest.append(line)
|
||||
elif len(line) >= 3 and line == "=" * len(line):
|
||||
# The header is separated from the rest of the file by
|
||||
# a line that contains 3 or more "=" characters and
|
||||
# nothing else.
|
||||
in_header = False
|
||||
else:
|
||||
header.append(line)
|
||||
|
||||
return header, rest
|
||||
|
||||
class Parser:
|
||||
def __init__(self,
|
||||
raw_lines: List[str],
|
||||
statement_initiator: str,
|
||||
expression_opening_delimiter: str,
|
||||
expression_closing_delimiter: str,
|
||||
) -> None:
|
||||
"""
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
self.statement_initiator = statement_initiator
|
||||
self.expression_opening_delimiter = expression_opening_delimiter
|
||||
self.expression_closing_delimiter = expression_closing_delimiter
|
||||
|
||||
# Split up the text into lines and parse those
|
||||
lines: List[Line] = []
|
||||
for i, text in enumerate(raw_lines):
|
||||
lines.append(Line.parse(self, text, i))
|
||||
|
||||
# Parse the lines into a block
|
||||
lines_queue = list(reversed(lines))
|
||||
self.main_block = Block(self, lines_queue)
|
||||
|
||||
def evaluate(self, local_vars: Dict[str, Any]) -> str:
|
||||
lines = self.main_block.evaluate(local_vars)
|
||||
return "".join(f"{line}\n" for line in lines)
|
||||
|
||||
# Line parsing (inline expressions)
|
||||
|
||||
class Line(ABC):
|
||||
@staticmethod
|
||||
def parse(parser: Parser, text: str, line_number: int) -> "Line":
|
||||
try:
|
||||
return IfStatement(parser, text, line_number)
|
||||
except ParseException:
|
||||
pass
|
||||
|
||||
try:
|
||||
return ElifStatement(parser, text, line_number)
|
||||
except ParseException:
|
||||
pass
|
||||
|
||||
try:
|
||||
return ElseStatement(parser, text, line_number)
|
||||
except ParseException:
|
||||
pass
|
||||
|
||||
try:
|
||||
return EndStatement(parser, text, line_number)
|
||||
except ParseException:
|
||||
pass
|
||||
|
||||
return ActualLine(parser, text, line_number)
|
||||
|
||||
def __init__(self, parser: Parser, line_number: int) -> None:
|
||||
self.parser = parser
|
||||
self.line_number = line_number
|
||||
|
||||
def _parse_statement(self, text: str, statement_name: str) -> Optional[str]:
|
||||
start = f"{self.parser.statement_initiator} {statement_name}"
|
||||
text = text.strip()
|
||||
if text.startswith(start):
|
||||
return text[len(start):].strip()
|
||||
else:
|
||||
return None
|
||||
|
||||
def _parse_statement_noarg(self, text: str, statement_name: str) -> bool:
|
||||
return text.strip() == f"{self.parser.statement_initiator} {statement_name}"
|
||||
|
||||
class ActualLine(Line):
|
||||
def __init__(self, parser: Parser, text: str, line_number: int) -> None:
|
||||
"""
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
super().__init__(parser, line_number)
|
||||
self.chunks = self._parse_chunks(text)
|
||||
|
||||
def _parse_chunks(self, text: str) -> List[Tuple[str, bool]]:
|
||||
"""
|
||||
A chunk is a tuple (text, is_expression), where the first
|
||||
argument is the text contained in the chunk and the second
|
||||
argument a boolean that indicates whether this chunk is a
|
||||
python expression (or just plain text).
|
||||
|
||||
Because it simplifies the program logic, a chunk's text may
|
||||
also be the empty string.
|
||||
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
chunks: List[Tuple[str, bool]] = []
|
||||
|
||||
i = 0
|
||||
while i < len(text):
|
||||
# Find opening delimiter
|
||||
od = text.find(self.parser.expression_opening_delimiter, i)
|
||||
if od == -1:
|
||||
chunks.append((text[i:], False))
|
||||
break # We've consumed the entire string.
|
||||
od_end = od + len(self.parser.expression_opening_delimiter)
|
||||
|
||||
# Find closing delimiter
|
||||
cd = text.find(self.parser.expression_closing_delimiter, od_end)
|
||||
if cd == -1:
|
||||
raise ParseException.on_line(self, f"No closing delimiter\n{text[:od_end]} <-- to THIS opening delimiter")
|
||||
cd_end = cd + len(self.parser.expression_closing_delimiter)
|
||||
|
||||
# Split up into chunks
|
||||
chunks.append((text[i:od], False))
|
||||
chunks.append((text[od_end:cd], True))
|
||||
i = cd_end
|
||||
|
||||
return chunks
|
||||
|
||||
def evaluate(self, local_vars: Dict[str, Any]) -> str:
|
||||
"""
|
||||
May raise: ExecuteException
|
||||
"""
|
||||
|
||||
return "".join(self._evaluate_chunk(chunk, local_vars) for chunk in self.chunks)
|
||||
|
||||
def _evaluate_chunk(self,
|
||||
chunk: Tuple[str, bool],
|
||||
local_vars: Dict[str, Any],
|
||||
) -> str:
|
||||
"""
|
||||
May raise: ExecuteException
|
||||
"""
|
||||
|
||||
if not chunk[1]:
|
||||
return chunk[0]
|
||||
|
||||
return str(safer_eval(chunk[0], local_vars))
|
||||
|
||||
class IfStatement(Line):
|
||||
def __init__(self, parser: Parser, text: str, line_number: int) -> None:
|
||||
"""
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
super().__init__(parser, line_number)
|
||||
|
||||
self.argument = self._parse_statement(text, "if")
|
||||
if self.argument is None:
|
||||
raise ParseException.on_line(self, "Not an 'if' statement")
|
||||
|
||||
class ElifStatement(Line):
|
||||
def __init__(self, parser: Parser, text: str, line_number: int) -> None:
|
||||
"""
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
super().__init__(parser, line_number)
|
||||
|
||||
self.argument = self._parse_statement(text, "elif")
|
||||
if self.argument is None:
|
||||
raise ParseException.on_line(self, "Not an 'elif' statement")
|
||||
|
||||
class ElseStatement(Line):
|
||||
def __init__(self, parser: Parser, text: str, line_number: int) -> None:
|
||||
"""
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
super().__init__(parser, line_number)
|
||||
|
||||
if not self._parse_statement_noarg(text, "else"):
|
||||
raise ParseException.on_line(self, "Not an 'else' statement")
|
||||
|
||||
class EndStatement(Line):
|
||||
def __init__(self, parser: Parser, text: str, line_number: int) -> None:
|
||||
"""
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
super().__init__(parser, line_number)
|
||||
|
||||
if not self._parse_statement_noarg(text, "end"):
|
||||
raise ParseException.on_line(self, "Not an 'end' statement")
|
||||
|
||||
# Block parsing
|
||||
|
||||
class Block:
|
||||
def __init__(self, parser: Parser, lines_queue: List[Line]) -> None:
|
||||
"""
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
self._elements: List[Union[ActualLine, IfBlock]] = []
|
||||
|
||||
while lines_queue:
|
||||
next_line = lines_queue[-1] # Peek
|
||||
if isinstance(next_line, ActualLine):
|
||||
lines_queue.pop()
|
||||
self._elements.append(next_line)
|
||||
elif isinstance(next_line, IfStatement):
|
||||
self._elements.append(IfBlock(parser, lines_queue))
|
||||
else:
|
||||
# We've hit the border of our enclosure. Parsing that
|
||||
# is up to the parent of this block, not the block
|
||||
# itself.
|
||||
break
|
||||
|
||||
def evaluate(self, local_vars: Dict[str, Any]) -> List[str]:
|
||||
lines: List[str] = []
|
||||
|
||||
for element in self._elements:
|
||||
if isinstance(element, ActualLine):
|
||||
lines.append(element.evaluate(local_vars))
|
||||
else:
|
||||
lines.extend(element.evaluate(local_vars))
|
||||
|
||||
return lines
|
||||
|
||||
class IfBlock(Block):
|
||||
def __init__(self, parser: Parser, lines_queue: List[Line]) -> None:
|
||||
"""
|
||||
May raise: ParseException
|
||||
"""
|
||||
|
||||
self._sections: List[Tuple[Block, Optional[str]]] = []
|
||||
|
||||
if not lines_queue:
|
||||
raise ParseException("Unexpected end of file, expected 'if' statement")
|
||||
|
||||
# If statement
|
||||
#
|
||||
# This is the short version:
|
||||
# if not isinstance(lines_queue[-1], IfStatement): # This should never happen
|
||||
# raise ParseException.on_line(lines_queue[-1], "Expected 'if' statement")
|
||||
# self._sections.append((Block(parser, lines_queue), lines_queue.pop().argument))
|
||||
#
|
||||
# And this the long version, which mypy understands without errors:
|
||||
next_statement = lines_queue[-1]
|
||||
if not isinstance(next_statement, IfStatement): # This should never happen
|
||||
raise ParseException.on_line(next_statement, "Expected 'if' statement")
|
||||
lines_queue.pop()
|
||||
self._sections.append((Block(parser, lines_queue), next_statement.argument))
|
||||
|
||||
# Elif statements
|
||||
#
|
||||
# This is the short version:
|
||||
# while lines_queue and isinstance(lines_queue[-1], ElifStatement):
|
||||
# self._sections.append((Block(parser, lines_queue), lines_queue.pop().argument))
|
||||
#
|
||||
# And this the long version, which mypy understands without errors:
|
||||
while True:
|
||||
if not lines_queue: break
|
||||
next_statement = lines_queue[-1]
|
||||
if not isinstance(next_statement, ElifStatement): break
|
||||
lines_queue.pop()
|
||||
self._sections.append((Block(parser, lines_queue), next_statement.argument))
|
||||
|
||||
# Optional else statement
|
||||
if lines_queue and isinstance(lines_queue[-1], ElseStatement):
|
||||
lines_queue.pop()
|
||||
self._sections.append((Block(parser, lines_queue), None))
|
||||
|
||||
if not lines_queue:
|
||||
raise ParseException("Unexpected end of file, expected 'if' statement")
|
||||
if not isinstance(lines_queue[-1], EndStatement):
|
||||
raise ParseException.on_line(lines_queue[-1], "Expected 'end' statement")
|
||||
lines_queue.pop()
|
||||
|
||||
def evaluate(self, local_vars: Dict[str, Any]) -> List[str]:
|
||||
for entry in self._sections:
|
||||
if entry[1] is None or safer_eval(entry[1], local_vars):
|
||||
return entry[0].evaluate(local_vars)
|
||||
|
||||
return []
|
||||
Loading…
Add table
Add a link
Reference in a new issue