diff --git a/src/ytdl_sub/script/functions/boolean_functions.py b/src/ytdl_sub/script/functions/boolean_functions.py index 3af1d53c..52c23ad7 100644 --- a/src/ytdl_sub/script/functions/boolean_functions.py +++ b/src/ytdl_sub/script/functions/boolean_functions.py @@ -1,4 +1,5 @@ -from ytdl_sub.script.types.resolvable import Boolean, AnyType +from ytdl_sub.script.types.resolvable import AnyType +from ytdl_sub.script.types.resolvable import Boolean class BooleanFunctions: diff --git a/src/ytdl_sub/script/functions/map_functions.py b/src/ytdl_sub/script/functions/map_functions.py index 230aac59..d65b74ec 100644 --- a/src/ytdl_sub/script/functions/map_functions.py +++ b/src/ytdl_sub/script/functions/map_functions.py @@ -4,9 +4,9 @@ from typing import Optional from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.map import Map +from ytdl_sub.script.types.resolvable import AnyType from ytdl_sub.script.types.resolvable import Hashable from ytdl_sub.script.types.resolvable import Resolvable -from ytdl_sub.script.types.resolvable import AnyType from ytdl_sub.script.types.resolvable import String from ytdl_sub.utils.exceptions import StringFormattingException @@ -27,7 +27,7 @@ class MapFunctions: return Map(output) @staticmethod - def get(mapping: Map, key: Hashable, default: Optional[AnyType] = None) -> AnyType: + def map_get(mapping: Map, key: Hashable, default: Optional[AnyType] = None) -> AnyType: if key not in mapping.value: if default is not None: return default diff --git a/src/ytdl_sub/script/functions/numeric_functions.py b/src/ytdl_sub/script/functions/numeric_functions.py index 8b194c70..39c1622e 100644 --- a/src/ytdl_sub/script/functions/numeric_functions.py +++ b/src/ytdl_sub/script/functions/numeric_functions.py @@ -1,7 +1,7 @@ +from ytdl_sub.script.types.resolvable import AnyType from ytdl_sub.script.types.resolvable import Float from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import Numeric -from ytdl_sub.script.types.resolvable import AnyType def _to_numeric(value: int | float) -> Numeric: diff --git a/src/ytdl_sub/script/functions/special_functions.py b/src/ytdl_sub/script/functions/special_functions.py index 9fe68419..50ffdc21 100644 --- a/src/ytdl_sub/script/functions/special_functions.py +++ b/src/ytdl_sub/script/functions/special_functions.py @@ -1,15 +1,13 @@ from typing import Union -from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import AnyType_1 from ytdl_sub.script.types.resolvable import AnyType_2 +from ytdl_sub.script.types.resolvable import Boolean class SpecialFunctions: @staticmethod - def if_( - condition: Boolean, true: AnyType_1, false: AnyType_2 - ) -> Union[AnyType_1, AnyType_2]: + def if_(condition: Boolean, true: AnyType_1, false: AnyType_2) -> Union[AnyType_1, AnyType_2]: if condition.value: return true return false diff --git a/src/ytdl_sub/script/functions/string_functions.py b/src/ytdl_sub/script/functions/string_functions.py index 96f2df59..bc27d898 100644 --- a/src/ytdl_sub/script/functions/string_functions.py +++ b/src/ytdl_sub/script/functions/string_functions.py @@ -1,7 +1,7 @@ from typing import Optional -from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import AnyType +from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import String diff --git a/src/ytdl_sub/script/parser.py b/src/ytdl_sub/script/parser.py index aee6d016..3458850d 100644 --- a/src/ytdl_sub/script/parser.py +++ b/src/ytdl_sub/script/parser.py @@ -16,6 +16,7 @@ from ytdl_sub.script.types.resolvable import String 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 IncompatibleFunctionArguments from ytdl_sub.script.utils.exceptions import InvalidSyntaxException from ytdl_sub.script.utils.exceptions import UnreachableSyntaxException from ytdl_sub.script.utils.parser_exception_formatter import ParserExceptionFormatter @@ -125,8 +126,8 @@ class _Parser: """ return self._syntax_tree - def _set_highlight_position(self) -> None: - self._error_highlight_pos = self._pos + def _set_highlight_position(self, pos: Optional[int] = None) -> None: + self._error_highlight_pos = pos if pos is not None else self._pos def _read(self, increment_pos: bool = True, length: int = 1) -> Optional[str]: if self._pos >= len(self._text): @@ -322,10 +323,15 @@ class _Parser: """ function_name: str = "" function_args: List[ArgumentType] = [] + function_start_pos = self._pos while ch := self._read(): if ch == ")": - return Function.from_name_and_args(name=function_name, args=function_args) + try: + return Function.from_name_and_args(name=function_name, args=function_args) + except IncompatibleFunctionArguments as exc: + self._set_highlight_position(function_start_pos) + raise InvalidSyntaxException(exc) from exc if ch != "(": function_name += ch diff --git a/src/ytdl_sub/script/types/array.py b/src/ytdl_sub/script/types/array.py index f5fb34ba..768aac38 100644 --- a/src/ytdl_sub/script/types/array.py +++ b/src/ytdl_sub/script/types/array.py @@ -4,7 +4,8 @@ from typing import Dict from typing import List from typing import Set -from ytdl_sub.script.types.resolvable import ArgumentType, FutureResolvable +from ytdl_sub.script.types.resolvable import ArgumentType +from ytdl_sub.script.types.resolvable import FutureResolvable from ytdl_sub.script.types.resolvable import NonHashable from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import ResolvableToJson diff --git a/src/ytdl_sub/script/types/function.py b/src/ytdl_sub/script/types/function.py index 0554baa1..4d23447a 100644 --- a/src/ytdl_sub/script/types/function.py +++ b/src/ytdl_sub/script/types/function.py @@ -14,14 +14,15 @@ from typing import Union from typing import get_origin from ytdl_sub.script.functions import Functions -from ytdl_sub.script.types.resolvable import ArgumentType -from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import AnyType_0 from ytdl_sub.script.types.resolvable import AnyType_1 from ytdl_sub.script.types.resolvable import AnyType_2 +from ytdl_sub.script.types.resolvable import ArgumentType +from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.variable import FunctionArgument from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.types.variable_dependency import VariableDependency +from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments from ytdl_sub.utils.exceptions import StringFormattingException @@ -216,7 +217,7 @@ class BuiltInFunction(Function): def validate_args(self) -> "BuiltInFunction": if not self.input_spec.is_compatible(input_args=self.args): - raise StringFormattingException( + raise IncompatibleFunctionArguments( f"Invalid arguments passed to function {self.name}.\n" f"{self._expected_received_error_msg()}" ) @@ -239,6 +240,12 @@ class BuiltInFunction(Function): def input_spec(self) -> FunctionInputSpec: return FunctionInputSpec.from_function(self) + @classmethod + def _arg_output_type(cls, arg: ArgumentType) -> Type[ArgumentType]: + if isinstance(arg, BuiltInFunction): + return arg.output_type + return type(arg) + @property def output_type(self) -> Type[Resolvable]: output_type = self.arg_spec.annotations["return"] @@ -246,11 +253,11 @@ class BuiltInFunction(Function): union_types_list = [] for union_type in output_type.__args__: if union_type == AnyType_0: - union_types_list.append(type(self.args[0])) + union_types_list.append(self._arg_output_type(self.args[0])) elif union_type == AnyType_1: - union_types_list.append(type(self.args[1])) + union_types_list.append(self._arg_output_type(self.args[1])) elif union_type == AnyType_2: - union_types_list.append(type(self.args[2])) + union_types_list.append(self._arg_output_type(self.args[2])) else: union_types_list.append(union_type) diff --git a/src/ytdl_sub/script/types/map.py b/src/ytdl_sub/script/types/map.py index b5fd206f..b014228c 100644 --- a/src/ytdl_sub/script/types/map.py +++ b/src/ytdl_sub/script/types/map.py @@ -4,7 +4,8 @@ from typing import Dict from typing import List from typing import Set -from ytdl_sub.script.types.resolvable import ArgumentType, FutureResolvable +from ytdl_sub.script.types.resolvable import ArgumentType +from ytdl_sub.script.types.resolvable import FutureResolvable from ytdl_sub.script.types.resolvable import Hashable from ytdl_sub.script.types.resolvable import NonHashable from ytdl_sub.script.types.resolvable import Resolvable diff --git a/src/ytdl_sub/script/types/resolvable.py b/src/ytdl_sub/script/types/resolvable.py index b63c3d12..3a475c79 100644 --- a/src/ytdl_sub/script/types/resolvable.py +++ b/src/ytdl_sub/script/types/resolvable.py @@ -14,33 +14,38 @@ class ArgumentType(ABC): """ Any possible argument type that has not been resolved yet """ + pass -class AnyType(ArgumentType, ABC): + +class AnyType_0(ABC): + pass + + +class AnyType_1(ABC): + pass + + +class AnyType_2(ABC): + pass + + +class AnyType(ArgumentType, AnyType_0, AnyType_1, AnyType_2, ABC): """ Human-readable name for FutureResolvable """ + value: Any + class FutureResolvable(AnyType, ABC): """ Type that will be resolved in the future """ -class AnyType_0(FutureResolvable, ABC): - pass - - -class AnyType_1(FutureResolvable, ABC): - pass - - -class AnyType_2(FutureResolvable, ABC): - pass - @dataclass(frozen=True) -class Resolvable(AnyType_0, AnyType_1, AnyType_2, ABC): +class Resolvable(AnyType, ABC): def __str__(self) -> str: return str(self.value) diff --git a/src/ytdl_sub/script/utils/exceptions.py b/src/ytdl_sub/script/utils/exceptions.py index fffd427a..d10df6ef 100644 --- a/src/ytdl_sub/script/utils/exceptions.py +++ b/src/ytdl_sub/script/utils/exceptions.py @@ -5,5 +5,9 @@ class InvalidSyntaxException(ValidationException): """Syntax is incorrect""" +class IncompatibleFunctionArguments(ValidationException): + """Function has invalid arguments""" + + class UnreachableSyntaxException(InvalidSyntaxException): """For use in places where code _should_ never reach, but might from bugs""" diff --git a/tests/unit/script/types/test_function.py b/tests/unit/script/types/test_function.py index 97203cd7..43a341de 100644 --- a/tests/unit/script/types/test_function.py +++ b/tests/unit/script/types/test_function.py @@ -51,6 +51,25 @@ class TestFunction: "func": String("winner"), } + def test_nested_if_function_incompatible(self): + function_str = """{ + %map_get( + %if( + True, + %if( + True, + {}, + [] + ), + {} + ), + "key" + ) + }""" + assert Script({"func": function_str}).resolve() == { + "func": String("winner"), + } + @pytest.mark.parametrize( "function_str", ["{%array_at({'a': 'dict?'}, 1)}" "{%array_extend('not', 'array')}"] )