From 291c308cad90d410858ed4f64d894206c1733ebd Mon Sep 17 00:00:00 2001 From: Jesse Bannon Date: Thu, 9 Nov 2023 23:54:10 -0800 Subject: [PATCH] tests working, nice error messages --- src/ytdl_sub/script/parser.py | 44 +++++------ src/ytdl_sub/script/types/function.py | 4 +- src/ytdl_sub/script/utils/exceptions.py | 6 ++ .../utils/parser_exception_formatter.py | 77 +++++++++++++++++++ tests/unit/script/types/test_map.py | 4 +- 5 files changed, 107 insertions(+), 28 deletions(-) create mode 100644 src/ytdl_sub/script/utils/parser_exception_formatter.py diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 9855edbc..39dd151f 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -15,6 +15,8 @@ from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.variable import FunctionArgument from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.utils.exceptions import InvalidSyntaxException +from ytdl_sub.script.utils.exceptions import NonFormattedInvalidSyntaxException +from ytdl_sub.script.utils.parser_exception_formatter import ParserExceptionFormatter from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.validators.string_formatter_validators import is_valid_source_variable_name @@ -43,18 +45,10 @@ class _Parser: parked_pos = self._pos try: yield - except InvalidSyntaxException as exc: - border = 4 - text_left = max(0, parked_pos - border) - text_right = min(len(self._text), self._pos + border) - text_len = text_right - text_left - - raise InvalidSyntaxException( - "Invalid syntax:\n" - f" {self._text[text_left:text_right]}\n" - f" {' ' * border}{'^' * text_len}\n\n" - f"{str(exc)}" - ) from exc + except NonFormattedInvalidSyntaxException as exc: + raise ParserExceptionFormatter( + self._text, parked_pos, self._pos, exc + ).highlight() from exc def _read(self, increment_pos: bool = True, length: int = 1) -> Optional[str]: try: @@ -250,7 +244,7 @@ class _Parser: while ch := self._read(increment_pos=False): if ch == "}": if key is not None: - raise InvalidSyntaxException("Map has a key with no value") + raise NonFormattedInvalidSyntaxException("Map has a key with no value") self._pos += 1 return UnresolvedMap(value=output) @@ -264,19 +258,21 @@ class _Parser: in_comma = True self._pos += 1 elif key is None: - in_comma = False - key_args = self._parse_args(breaking_chars=":") - if len(key_args) != 1: - raise StringFormattingException("Lazy parsing but got mlutiple args") - key = key_args[0] + with self._error_formatting(): + in_comma = False + key_args = self._parse_args(breaking_chars=":") + if len(key_args) != 1: + raise StringFormattingException("Lazy parsing but got mlutiple args") + key = key_args[0] elif key is not None and ch == ":": - self._pos += 1 - value_args = self._parse_args(breaking_chars=",}") - if len(value_args) != 1: - raise InvalidSyntaxException("Map has a key with no value") + with self._error_formatting(): + self._pos += 1 + value_args = self._parse_args(breaking_chars=",}") + if len(value_args) != 1: + raise NonFormattedInvalidSyntaxException("Map has a key with no value") - output[key] = value_args[0] - key = None + output[key] = value_args[0] + key = None else: raise StringFormattingException("Invalid map") diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index e8f8c706..745b8c4b 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -197,8 +197,8 @@ class CustomFunction(Function): resolved_variables=resolved_variables_with_args, custom_functions=custom_functions, ) - else: - raise StringFormattingException(f"Custom function {self.name} does not exist") + + raise StringFormattingException(f"Custom function {self.name} does not exist") class BuiltInFunction(Function): diff --git a/src/ytdl_sub/script/utils/exceptions.py b/src/ytdl_sub/script/utils/exceptions.py index 2a4df9f9..81702bae 100644 --- a/src/ytdl_sub/script/utils/exceptions.py +++ b/src/ytdl_sub/script/utils/exceptions.py @@ -3,3 +3,9 @@ from ytdl_sub.utils.exceptions import ValidationException class InvalidSyntaxException(ValidationException): """Syntax is incorrect""" + + +class NonFormattedInvalidSyntaxException(InvalidSyntaxException): + """ + Syntax is incorrect, and the exception itself has not been formatted yet to be user-facing + """ diff --git a/src/ytdl_sub/script/utils/parser_exception_formatter.py b/src/ytdl_sub/script/utils/parser_exception_formatter.py new file mode 100644 index 00000000..3beb1e98 --- /dev/null +++ b/src/ytdl_sub/script/utils/parser_exception_formatter.py @@ -0,0 +1,77 @@ +import sys +from typing import List + +from ytdl_sub.script.utils.exceptions import InvalidSyntaxException +from ytdl_sub.script.utils.exceptions import NonFormattedInvalidSyntaxException + + +class ParserExceptionFormatter: + def __init__( + self, text: str, start: int, end: int, exception: NonFormattedInvalidSyntaxException + ): + self._text = text + self._start = start + self._end = end + self._exception = exception + + def exception_text(self, border: int): + text_left = max(0, self._start - border) + text_right = min(len(self._text), self._start + border) + relative_start = self._start - text_left + + exception_text: str = "" + if text_left > 3: + exception_text = "… " + relative_start += len(exception_text) + + exception_text += self._text[text_left:text_right] + + if text_right < len(self._text) - 3: + exception_text += " …" + + exception_text += "\n" + exception_text += f"{' ' * relative_start}^" + + return "\n" + exception_text + + @property + def is_multi_line(self) -> bool: + return "\n" in self._text + + def exception_text_lines(self, border_lines: int = 0) -> str: + split_text = self._text.split("\n") + + start_line: int = sys.maxsize + end_line: int = -1 + pos: int = 0 + for idx, line in enumerate(split_text): + if self._start <= pos < self._end: + start_line = min(start_line, idx) + end_line = max(end_line, idx + 1) + pos += len(line) + + true_start_line = start_line + start_line = max(0, start_line - border_lines) + end_line = min(len(split_text), end_line + border_lines) + + # Get min leading spaces between all lines to return + min_leading_spaces = sys.maxsize + for line in split_text[start_line:end_line]: + min_leading_spaces = min(min_leading_spaces, len(line) - len(line.lstrip())) + + to_return: List[str] = [] + for idx in range(start_line, end_line): + if idx == true_start_line: + to_return.append(f">>> {split_text[idx][min_leading_spaces:]}") + else: + to_return.append(f" {split_text[idx][min_leading_spaces:]}") + + return "\n" + "\n".join(to_return) + + def highlight(self) -> InvalidSyntaxException: + if self.is_multi_line: + invalid_syntax = self.exception_text_lines(border_lines=3) + else: + invalid_syntax = self.exception_text(border=20) + + return InvalidSyntaxException(f"{invalid_syntax}\n{str(self._exception)}") diff --git a/tests/unit/script/types/test_map.py b/tests/unit/script/types/test_map.py index 75543071..73a4e28a 100644 --- a/tests/unit/script/types/test_map.py +++ b/tests/unit/script/types/test_map.py @@ -72,5 +72,5 @@ class TestMap: ], ) def test_key_has_no_value(self, value: str): - # with pytest.raises(InvalidSyntaxException, match=re.escape("Map has a key with no value")): - Script({"map": value}).resolve() + with pytest.raises(InvalidSyntaxException, match=re.escape("Map has a key with no value")): + Script({"map": value}).resolve()