Experimental error handling with snippets of input for OutputGenerator. #54

Merged
jan merged 5 commits from error-handling into master 10 months ago
  1. 33
      src/formatitko/formatitko.py
  2. 2
      src/formatitko/katex.py
  3. 49
      src/formatitko/nop_processor.py
  4. 91
      src/formatitko/output_generator.py
  5. 7
      src/formatitko/transform_processor.py
  6. 9
      test/test-files/test-partial.md

33
src/formatitko/formatitko.py

@ -14,7 +14,7 @@ from .katex import KatexClient
from .html import html from .html import html
from .tex import tex from .tex import tex
from .images import ImageProcessor from .images import ImageProcessor
from .output_generator import OutputGenerator from .output_generator import OutputGenerator, FormatitkoRecursiveError
from .html_generator import HTMLGenerator from .html_generator import HTMLGenerator
from .transform_processor import TransformProcessor from .transform_processor import TransformProcessor
from .pandoc_processor import PandocProcessor from .pandoc_processor import PandocProcessor
@ -54,9 +54,15 @@ def main():
doc = import_md(open(args.input_filename, "r").read()) doc = import_md(open(args.input_filename, "r").read())
if args.debug: if args.debug:
OutputGenerator(sys.stdout).generate(doc) try:
OutputGenerator(sys.stdout).generate(doc)
except FormatitkoRecursiveError as e:
e.pretty_print()
doc = TransformProcessor(args.input_filename).transform(doc) try:
doc = TransformProcessor(args.input_filename).transform(doc)
except FormatitkoRecursiveError as e:
e.pretty_print()
# Initialize the image processor (this just keeps some basic state) # Initialize the image processor (this just keeps some basic state)
imageProcessor = ImageProcessor(args.img_public_dir, args.img_web_path, args.img_cache_dir, *args.img_lookup_dirs) imageProcessor = ImageProcessor(args.img_public_dir, args.img_web_path, args.img_cache_dir, *args.img_lookup_dirs)
@ -65,11 +71,18 @@ def main():
# Initialize KaTeX client (this runs the node app and connects to a unix socket) # Initialize KaTeX client (this runs the node app and connects to a unix socket)
with KatexClient(socket=args.katex_socket) as katexClient: with KatexClient(socket=args.katex_socket) as katexClient:
with open(args.output_html, "w") as file: with open(args.output_html, "w") as file:
HTMLGenerator(file, katexClient, imageProcessor).generate(doc) try:
HTMLGenerator(file, katexClient, imageProcessor).generate(doc)
except FormatitkoRecursiveError as e:
e.pretty_print()
if args.output_tex is not None: if args.output_tex is not None:
with open(args.output_tex, "w") as file: with open(args.output_tex, "w") as file:
UCWTexGenerator(file, imageProcessor).generate(doc) try:
UCWTexGenerator(file, imageProcessor).generate(doc)
except FormatitkoRecursiveError as e:
e.pretty_print()
if args.output_md is not None: if args.output_md is not None:
with open(args.output_md, "w") as file: with open(args.output_md, "w") as file:
@ -83,7 +96,10 @@ def main():
if args.output_tex is None: if args.output_tex is None:
fd = tempfile.NamedTemporaryFile(dir=".", suffix=".tex") fd = tempfile.NamedTemporaryFile(dir=".", suffix=".tex")
with open(fd.name, "w") as file: with open(fd.name, "w") as file:
UCWTexGenerator(file, imageProcessor).generate(doc) try:
UCWTexGenerator(file, imageProcessor).generate(doc)
except FormatitkoRecursiveError as e:
e.pretty_print()
filename = fd.name filename = fd.name
else: else:
filename = args.output_tex filename = args.output_tex
@ -93,7 +109,10 @@ def main():
if args.debug: if args.debug:
print("-----------------------------------") print("-----------------------------------")
OutputGenerator(sys.stdout).generate(doc) try:
OutputGenerator(sys.stdout).generate(doc)
except FormatitkoRecursiveError as e:
e.pretty_print()
if __name__ == "__main__": if __name__ == "__main__":

2
src/formatitko/katex.py

@ -79,7 +79,7 @@ class KatexClient:
if "error" in response: if "error" in response:
raise KatexServerError(response["error"]) raise KatexServerError(response["error"])
if "error" in response["results"][0]: if "error" in response["results"][0]:
raise KatexError(response["results"][0]["error"]) raise KatexError(response["results"][0]["error"] + " in $" + tex + "$")
else: else:
return response["results"][0]["html"] return response["results"][0]["html"]

49
src/formatitko/nop_processor.py

@ -7,15 +7,20 @@ from typing import Union, Callable
from .whitespace import NBSP from .whitespace import NBSP
from .elements import FQuoted from .elements import FQuoted
from .context import Group, InlineGroup, BlockGroup from .context import Group, InlineGroup, BlockGroup, Context
from .whitespace import Whitespace from .whitespace import Whitespace
from .command import BlockCommand, InlineCommand, CodeCommand, Command from .command import BlockCommand, InlineCommand, CodeCommand, Command
from .output_generator import FormatitkoRecursiveError
ELCl = Union[Element, ListContainer, list[Union[Element, ListContainer]]] ELCl = Union[Element, ListContainer, list[Union[Element, ListContainer]]]
class DoubleDocError(Exception):
"TransformProcessor should only ever see a single Doc."
pass
class NOPProcessor: class NOPProcessor:
TYPE_DICT: dict[type, Callable] TYPE_DICT: dict[type, Callable]
context: Union[Context, None] = None
class UnknownElementError(Exception): class UnknownElementError(Exception):
f"An unknown Element has been passed to the NOPProcessor, probably because panflute introduced a new one." f"An unknown Element has been passed to the NOPProcessor, probably because panflute introduced a new one."
@ -96,23 +101,30 @@ class NOPProcessor:
return [] return []
def transform(self, e: ELCl) -> ELCl: def transform(self, e: ELCl) -> ELCl:
if isinstance(e, list):
return self.transform_list(e)
elif isinstance(e, ListContainer):
return self.transform_ListContainer(e)
for transformer in self.get_pretransformers():
e = transformer(e)
try: try:
e = self.TYPE_DICT[type(e)](e) if isinstance(e, list):
except KeyError: return self.transform_list(e)
raise self.UnknownElementError(type(e)) elif isinstance(e, ListContainer):
return self.transform_ListContainer(e)
for transformer in self.get_posttransformers():
e = transformer(e) for transformer in self.get_pretransformers():
e = transformer(e)
return e
try:
e = self.TYPE_DICT[type(e)](e)
except KeyError:
raise self.UnknownElementError(type(e))
for transformer in self.get_posttransformers():
e = transformer(e)
return e
except FormatitkoRecursiveError as err:
if not isinstance(e, ListContainer):
err.add_element(e)
raise err
except Exception as err:
raise FormatitkoRecursiveError(e, self.context) from err
def transform_list(self, e: list[Union[Element, ListContainer]]) -> list[Union[Element, ListContainer]]: def transform_list(self, e: list[Union[Element, ListContainer]]) -> list[Union[Element, ListContainer]]:
for i in range(len(e)): for i in range(len(e)):
@ -293,6 +305,9 @@ class NOPProcessor:
return e return e
def transform_Doc(self, e: Doc) -> Doc: def transform_Doc(self, e: Doc) -> Doc:
if self.context is not None:
raise DoubleDocError()
self.context = Context(e, self.root_file_path)
e.content = self.transform(e.content) e.content = self.transform(e.content)
return e return e

91
src/formatitko/output_generator.py

@ -3,18 +3,56 @@ from panflute import Cite, Code, Emph, Image, LineBreak, Link, Math, Note, Quote
from panflute import BlockQuote, BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Div, Figure, Header, HorizontalRule, LineBlock, LineItem, ListItem, MetaBlocks, MetaBool, MetaInlines, MetaList, MetaMap, MetaString, Null, OrderedList, Para, Plain, RawBlock, Table, TableBody, TableFoot, TableHead from panflute import BlockQuote, BulletList, Citation, CodeBlock, Definition, DefinitionItem, DefinitionList, Div, Figure, Header, HorizontalRule, LineBlock, LineItem, ListItem, MetaBlocks, MetaBool, MetaInlines, MetaList, MetaMap, MetaString, Null, OrderedList, Para, Plain, RawBlock, Table, TableBody, TableFoot, TableHead
from panflute import TableRow, TableCell, Caption, Doc from panflute import TableRow, TableCell, Caption, Doc
from panflute import MetaValue from panflute import MetaValue
from panflute import stringify
from typing import Union, Callable from typing import Union, Callable
from .whitespace import NBSP from .whitespace import NBSP
from .elements import FQuoted from .elements import FQuoted
from .context import Group, InlineGroup, BlockGroup, Context from .context import Group, InlineGroup, BlockGroup, Context
import re
import sys
class UnknownElementError(Exception): class UnknownElementError(Exception):
"An unknown Element has been passed to the OutputGenerator, probably because panflute introduced a new one." "An unknown Element has been passed to the OutputGenerator, probably because panflute introduced a new one."
pass pass
class FormatitkoRecursiveError(Exception):
"A generic exception which wraps other exceptions and adds element-based traceback"
elements: list[Union[Element, ListContainer, list[Union[Element, ListContainer]]]]
context: Context
def __init__(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]], context: Context, *args):
self.elements = [e]
self.context = context
super().__init__(args)
def add_element(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]):
self.elements.append(e)
def pretty_print(self):
def eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def print_filename_recursive(context: Context):
return context.filename +\
((" (included from " + print_filename_recursive(context.parent) + ")") if context.parent else "")
eprint(f"Error occured in file {print_filename_recursive(self.context)} in ", end="")
line = None
for i in range(len(self.elements)-1, 0, -1):
if hasattr(self.elements[i], "content") and len(self.elements[i].content) > 0 and isinstance(self.elements[i].content[0], Inline) and line is None:
line = self.elements[i]
eprint(type(self.elements[i]).__name__ + "[" + (str(self.elements[i-1].index) if isinstance(self.elements[i-1].index, int) else "") + "]", end=": ")
if line:
eprint()
eprint('on line: "' + stringify(line).strip() + '"', end="")
eprint()
eprint("in element: " + str(self.elements[0]).replace("\n", "\\n"))
sys.tracebacklimit = 0
raise self.__cause__ from None
class OutputGenerator: class OutputGenerator:
_empty_lines: int _empty_lines: int
context: Union[Context, None] context: Union[Context, None]
@ -101,28 +139,35 @@ class OutputGenerator:
} }
def generate(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]): def generate(self, e: Union[Element, ListContainer, list[Union[Element, ListContainer]]]):
if isinstance(e, Group): try:
old_context = self.context if isinstance(e, Group):
self.context = e.context old_context = self.context
if isinstance(e, list): self.context = e.context
self.generate_list(e) if isinstance(e, list):
elif isinstance(e, ListContainer): self.generate_list(e)
self.generate_ListContainer(e) elif isinstance(e, ListContainer):
elif isinstance(e, Inline): self.generate_ListContainer(e)
self.generate_Inline(e) elif isinstance(e, Inline):
elif isinstance(e, Block): self.generate_Inline(e)
self.generate_Block(e) elif isinstance(e, Block):
elif isinstance(e, MetaValue): self.generate_Block(e)
self.generate_MetaValue(e) elif isinstance(e, MetaValue):
elif isinstance(e, MetaList): self.generate_MetaValue(e)
self.generate_MetaList(e) elif isinstance(e, MetaList):
else: self.generate_MetaList(e)
try: else:
self.TYPE_DICT_MISC[type(e)](e) try:
except KeyError: self.TYPE_DICT_MISC[type(e)](e)
raise UnknownElementError(type(e)) except KeyError as err:
if isinstance(e, Group): raise UnknownElementError(type(e)) from err
self.context = old_context if isinstance(e, Group):
self.context = old_context
except FormatitkoRecursiveError as err:
if not isinstance(e, ListContainer):
err.add_element(e)
raise err
except Exception as err:
raise FormatitkoRecursiveError(e, self.context) from err
def escape_special_chars(self, text: str) -> str: def escape_special_chars(self, text: str) -> str:
return text return text

7
src/formatitko/transform_processor.py

@ -20,15 +20,10 @@ from .context import Context, CommandCallable
from .whitespace import Whitespace, bavlna from .whitespace import Whitespace, bavlna
from .command import BlockCommand, InlineCommand, CodeCommand, Command from .command import BlockCommand, InlineCommand, CodeCommand, Command
from .command_util import handle_command_define, parse_command from .command_util import handle_command_define, parse_command
from .nop_processor import NOPProcessor, ELCl from .nop_processor import NOPProcessor, ELCl, DoubleDocError
class DoubleDocError(Exception):
"TransformProcessor should only ever see a single Doc."
pass
class TransformProcessor(NOPProcessor): class TransformProcessor(NOPProcessor):
context: Union[Context, None] = None
root_file_path: str root_file_path: str
root_highlight_style: str = "default" root_highlight_style: str = "default"
_command_modules: list[tuple[Union[dict[str, CommandCallable], ModuleType], str]] = [] _command_modules: list[tuple[Union[dict[str, CommandCallable], ModuleType], str]] = []

9
test/test-files/test-partial.md

@ -56,6 +56,15 @@ $$
$$ $$
<!--There is an inline *emphasis with $math \error$*.-->
<!--
```python {.run}
print("bruh")
raise Exception("Jsem piča")
```
-->
![This is a figure, go figure...](logo.svg){width=25%}What ![This is a figure, go figure...](logo.svg){width=25%}What
![This is a figure, go figure...](logo.pdf){width=50%} ![This is a figure, go figure...](logo.pdf){width=50%}

Loading…
Cancel
Save