diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index 17127b4c..5d6ee489 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -41,6 +41,11 @@ UNREACHABLE = UnreachableSyntaxException( BRACKET_NOT_CLOSED = InvalidSyntaxException("Bracket not properly closed") +NUMERICS_ONLY_ARGS = InvalidSyntaxException( + "Numerics can only be used as arguments to functions, maps, or arrays" +) +NUMERICS_INVALID_CHAR = InvalidSyntaxException("Invalid value when parsing a numeric") + def UNEXPECTED_CHAR_ARGUMENT(parser: ArgumentParser): return InvalidSyntaxException(f"Unexpected character when parsing {parser.value} arguments") @@ -64,6 +69,10 @@ def _is_variable_start(char: str) -> bool: return char.isalpha() and char.islower() +def _is_numeric_start(char: str) -> bool: + return char.isnumeric() or char == "-" + + class _Parser: def __init__(self, text: str): self._text = text @@ -154,17 +163,38 @@ class _Parser: def _parse_numeric(self) -> Integer | Float: numeric_string = "" + + if self._read(increment_pos=False) == "-": + numeric_string += "-" + self._pos += 1 + if has_decimal := (self._read(increment_pos=False) == "."): + numeric_string += "." + self._pos += 1 + while ch := self._read(increment_pos=False): - if not (ch.isnumeric() or ch == "."): + if ch == "-": + raise NUMERICS_INVALID_CHAR + + if ch == ".": + if has_decimal: + raise NUMERICS_INVALID_CHAR + has_decimal = True + + self._pos += 1 + numeric_string += ch + elif ch.isnumeric(): + self._pos += 1 + numeric_string += ch + else: break - self._pos += 1 - numeric_string += ch + if numeric_string == "." or numeric_string == "-": + raise NUMERICS_INVALID_CHAR try: numeric_float = float(numeric_string) except ValueError: - raise StringFormattingException(f"Invalid numeric: {numeric_string}") + raise UNREACHABLE if (numeric_int := int(numeric_float)) == numeric_float: return Integer(value=numeric_int) @@ -190,7 +220,7 @@ class _Parser: if self._read(increment_pos=False) == "%": self._pos += 1 return self._parse_function() - if self._read(increment_pos=False).isnumeric(): + if _is_numeric_start(self._read(increment_pos=False)): return self._parse_numeric() if (self._read(increment_pos=False, length=4) or "").lower() == "true": self._pos += 4 @@ -377,6 +407,8 @@ class _Parser: self._ast.append(self._parse_map()) elif _is_variable_start(ch1): self._ast.append(self._parse_variable()) + elif _is_numeric_start(ch1): + raise NUMERICS_ONLY_ARGS else: raise UNEXPECTED_CHAR_ARGUMENT(parser=ArgumentParser.SCRIPT) elif bracket_counter == 0: diff --git a/tests/unit/script/types/test_array.py b/tests/unit/script/types/test_array.py index ea2f44c0..f4b0e8b7 100644 --- a/tests/unit/script/types/test_array.py +++ b/tests/unit/script/types/test_array.py @@ -2,7 +2,6 @@ import re import pytest -from ytdl_sub.script.parser import BRACKET_NOT_CLOSED from ytdl_sub.script.parser import UNEXPECTED_CHAR_ARGUMENT from ytdl_sub.script.parser import UNEXPECTED_COMMA_ARGUMENT from ytdl_sub.script.parser import ArgumentParser diff --git a/tests/unit/script/types/test_integer.py b/tests/unit/script/types/test_integer.py new file mode 100644 index 00000000..49907dfe --- /dev/null +++ b/tests/unit/script/types/test_integer.py @@ -0,0 +1,62 @@ +import re +from typing import Tuple + +import pytest + +from ytdl_sub.script.parser import NUMERICS_INVALID_CHAR +from ytdl_sub.script.parser import NUMERICS_ONLY_ARGS +from ytdl_sub.script.parser import UNEXPECTED_CHAR_ARGUMENT +from ytdl_sub.script.parser import UNEXPECTED_COMMA_ARGUMENT +from ytdl_sub.script.parser import ArgumentParser +from ytdl_sub.script.script import Script +from ytdl_sub.script.types.array import ResolvedArray +from ytdl_sub.script.types.resolvable import Float +from ytdl_sub.script.types.resolvable import Integer +from ytdl_sub.script.types.resolvable import String +from ytdl_sub.script.utils.exceptions import InvalidSyntaxException + + +class TestInteger: + @pytest.mark.parametrize( + "integer", + [ + "{1}", + "{ 1 }", + "{-1}", + "{ -1 }", + "{0001}", + "{ 0001 }", + ], + ) + def test_integer_not_arg(self, integer: str): + with pytest.raises(InvalidSyntaxException, match=re.escape(str(NUMERICS_ONLY_ARGS))): + Script({"integer": integer}).resolve() + + @pytest.mark.parametrize( + "integer, expected_integer", + [ + ("{%int(1)}", 1), + ("{%int( 1 )}", 1), + ("{%int(-1)}", -1), + ("{%int( -1 )}", -1), + ("{%int(0001)}", 1), + ("{%int( 0001 )}", 1), + ], + ) + def test_integer(self, integer: str, expected_integer: int): + assert Script({"integer": integer}).resolve() == {"integer": Integer(expected_integer)} + + @pytest.mark.parametrize( + "integer", + [ + "{%add(0, --1)}", + "{%add(0, 1- )}", + "{%add(0,-1-)}", + "{%add(0, -1 - )}", + "{%add(0,0001a)}", + "{%add(0, 0001b )}", + ], + ) + def test_invalid_integer(self, integer: str): + with pytest.raises(InvalidSyntaxException, match=re.escape(str(NUMERICS_INVALID_CHAR))): + Script({"integer": integer}).resolve()