work to validate funcs

This commit is contained in:
Jesse Bannon 2023-11-15 00:09:51 -08:00
parent c4f9bf4fa0
commit 3174225f55
12 changed files with 75 additions and 33 deletions

View file

@ -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: class BooleanFunctions:

View file

@ -4,9 +4,9 @@ from typing import Optional
from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.array import Array
from ytdl_sub.script.types.map import Map 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 Hashable
from ytdl_sub.script.types.resolvable import Resolvable 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.script.types.resolvable import String
from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingException
@ -27,7 +27,7 @@ class MapFunctions:
return Map(output) return Map(output)
@staticmethod @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 key not in mapping.value:
if default is not None: if default is not None:
return default return default

View file

@ -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 Float
from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import Numeric from ytdl_sub.script.types.resolvable import Numeric
from ytdl_sub.script.types.resolvable import AnyType
def _to_numeric(value: int | float) -> Numeric: def _to_numeric(value: int | float) -> Numeric:

View file

@ -1,15 +1,13 @@
from typing import Union 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_1
from ytdl_sub.script.types.resolvable import AnyType_2 from ytdl_sub.script.types.resolvable import AnyType_2
from ytdl_sub.script.types.resolvable import Boolean
class SpecialFunctions: class SpecialFunctions:
@staticmethod @staticmethod
def if_( def if_(condition: Boolean, true: AnyType_1, false: AnyType_2) -> Union[AnyType_1, AnyType_2]:
condition: Boolean, true: AnyType_1, false: AnyType_2
) -> Union[AnyType_1, AnyType_2]:
if condition.value: if condition.value:
return true return true
return false return false

View file

@ -1,7 +1,7 @@
from typing import Optional 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 AnyType
from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import String from ytdl_sub.script.types.resolvable import String

View file

@ -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.syntax_tree import SyntaxTree
from ytdl_sub.script.types.variable import FunctionArgument from ytdl_sub.script.types.variable import FunctionArgument
from ytdl_sub.script.types.variable import Variable 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 InvalidSyntaxException
from ytdl_sub.script.utils.exceptions import UnreachableSyntaxException from ytdl_sub.script.utils.exceptions import UnreachableSyntaxException
from ytdl_sub.script.utils.parser_exception_formatter import ParserExceptionFormatter from ytdl_sub.script.utils.parser_exception_formatter import ParserExceptionFormatter
@ -125,8 +126,8 @@ class _Parser:
""" """
return self._syntax_tree return self._syntax_tree
def _set_highlight_position(self) -> None: def _set_highlight_position(self, pos: Optional[int] = None) -> None:
self._error_highlight_pos = self._pos 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]: def _read(self, increment_pos: bool = True, length: int = 1) -> Optional[str]:
if self._pos >= len(self._text): if self._pos >= len(self._text):
@ -322,10 +323,15 @@ class _Parser:
""" """
function_name: str = "" function_name: str = ""
function_args: List[ArgumentType] = [] function_args: List[ArgumentType] = []
function_start_pos = self._pos
while ch := self._read(): while ch := self._read():
if ch == ")": if ch == ")":
try:
return Function.from_name_and_args(name=function_name, args=function_args) 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 != "(": if ch != "(":
function_name += ch function_name += ch

View file

@ -4,7 +4,8 @@ from typing import Dict
from typing import List from typing import List
from typing import Set 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 NonHashable
from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import Resolvable
from ytdl_sub.script.types.resolvable import ResolvableToJson from ytdl_sub.script.types.resolvable import ResolvableToJson

View file

@ -14,14 +14,15 @@ from typing import Union
from typing import get_origin from typing import get_origin
from ytdl_sub.script.functions import Functions 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_0
from ytdl_sub.script.types.resolvable import AnyType_1 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 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 FunctionArgument
from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.types.variable import Variable
from ytdl_sub.script.types.variable_dependency import VariableDependency from ytdl_sub.script.types.variable_dependency import VariableDependency
from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments
from ytdl_sub.utils.exceptions import StringFormattingException from ytdl_sub.utils.exceptions import StringFormattingException
@ -216,7 +217,7 @@ class BuiltInFunction(Function):
def validate_args(self) -> "BuiltInFunction": def validate_args(self) -> "BuiltInFunction":
if not self.input_spec.is_compatible(input_args=self.args): 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"Invalid arguments passed to function {self.name}.\n"
f"{self._expected_received_error_msg()}" f"{self._expected_received_error_msg()}"
) )
@ -239,6 +240,12 @@ class BuiltInFunction(Function):
def input_spec(self) -> FunctionInputSpec: def input_spec(self) -> FunctionInputSpec:
return FunctionInputSpec.from_function(self) 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 @property
def output_type(self) -> Type[Resolvable]: def output_type(self) -> Type[Resolvable]:
output_type = self.arg_spec.annotations["return"] output_type = self.arg_spec.annotations["return"]
@ -246,11 +253,11 @@ class BuiltInFunction(Function):
union_types_list = [] union_types_list = []
for union_type in output_type.__args__: for union_type in output_type.__args__:
if union_type == AnyType_0: 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: 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: 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: else:
union_types_list.append(union_type) union_types_list.append(union_type)

View file

@ -4,7 +4,8 @@ from typing import Dict
from typing import List from typing import List
from typing import Set 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 Hashable
from ytdl_sub.script.types.resolvable import NonHashable from ytdl_sub.script.types.resolvable import NonHashable
from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import Resolvable

View file

@ -14,33 +14,38 @@ class ArgumentType(ABC):
""" """
Any possible argument type that has not been resolved yet Any possible argument type that has not been resolved yet
""" """
pass 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 Human-readable name for FutureResolvable
""" """
value: Any value: Any
class FutureResolvable(AnyType, ABC): class FutureResolvable(AnyType, ABC):
""" """
Type that will be resolved in the future 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) @dataclass(frozen=True)
class Resolvable(AnyType_0, AnyType_1, AnyType_2, ABC): class Resolvable(AnyType, ABC):
def __str__(self) -> str: def __str__(self) -> str:
return str(self.value) return str(self.value)

View file

@ -5,5 +5,9 @@ class InvalidSyntaxException(ValidationException):
"""Syntax is incorrect""" """Syntax is incorrect"""
class IncompatibleFunctionArguments(ValidationException):
"""Function has invalid arguments"""
class UnreachableSyntaxException(InvalidSyntaxException): class UnreachableSyntaxException(InvalidSyntaxException):
"""For use in places where code _should_ never reach, but might from bugs""" """For use in places where code _should_ never reach, but might from bugs"""

View file

@ -51,6 +51,25 @@ class TestFunction:
"func": String("winner"), "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( @pytest.mark.parametrize(
"function_str", ["{%array_at({'a': 'dict?'}, 1)}" "{%array_extend('not', 'array')}"] "function_str", ["{%array_at({'a': 'dict?'}, 1)}" "{%array_extend('not', 'array')}"]
) )