move stuff into type-checking

This commit is contained in:
Jesse Bannon 2023-11-17 23:54:16 -08:00
parent 0b85d01b7a
commit f0cbc1bbd3
6 changed files with 89 additions and 75 deletions

View file

@ -28,7 +28,7 @@ from ytdl_sub.validators.string_formatter_validators import is_valid_source_vari
# pylint: disable=too-many-return-statements
class ArgumentParser(Enum):
class ParsedArgType(Enum):
SCRIPT = "script"
FUNCTION = "function"
ARRAY = "array"
@ -57,12 +57,12 @@ BOOLEAN_ONLY_ARGS = InvalidSyntaxException(
)
def _UNEXPECTED_CHAR_ARGUMENT(parser: ArgumentParser):
return InvalidSyntaxException(f"Unexpected character when parsing {parser.value} arguments")
def _UNEXPECTED_CHAR_ARGUMENT(arg_type: ParsedArgType):
return InvalidSyntaxException(f"Unexpected character when parsing {arg_type.value} arguments")
def _UNEXPECTED_COMMA_ARGUMENT(parser: ArgumentParser):
return InvalidSyntaxException(f"Unexpected comma when parsing {parser.value} arguments")
def _UNEXPECTED_COMMA_ARGUMENT(arg_type: ParsedArgType):
return InvalidSyntaxException(f"Unexpected comma when parsing {arg_type.value} arguments")
MAP_KEY_WITH_NO_VALUE = InvalidSyntaxException("Map has a key with no value")
@ -255,7 +255,7 @@ class _Parser:
raise STRINGS_NOT_CLOSED
def _parse_function_arg(self, argument_parser: ArgumentParser) -> ArgumentType:
def _parse_function_arg(self, argument_parser: ParsedArgType) -> ArgumentType:
if self._read(increment_pos=False) == "%":
self._pos += 1
return self._parse_function()
@ -282,10 +282,10 @@ class _Parser:
return self._parse_variable()
self._set_highlight_position()
raise _UNEXPECTED_CHAR_ARGUMENT(parser=argument_parser)
raise _UNEXPECTED_CHAR_ARGUMENT(arg_type=argument_parser)
def _parse_args(
self, argument_parser: ArgumentParser, breaking_chars: str = ")"
self, argument_parser: ParsedArgType, breaking_chars: str = ")"
) -> List[ArgumentType]:
"""
Begin parsing function args after the first ``(``, i.e. ``function_name(``
@ -332,7 +332,7 @@ class _Parser:
if ch != "(":
function_name += ch
else:
function_args = self._parse_args(argument_parser=ArgumentParser.FUNCTION)
function_args = self._parse_args(argument_parser=ParsedArgType.FUNCTION)
raise StringFormattingException("Invalid function")
@ -348,7 +348,7 @@ class _Parser:
return UnresolvedArray(value=function_args)
function_args = self._parse_args(
argument_parser=ArgumentParser.ARRAY, breaking_chars="]"
argument_parser=ParsedArgType.ARRAY, breaking_chars="]"
)
raise UNREACHABLE
@ -372,18 +372,18 @@ class _Parser:
if ch == ",":
if in_comma:
raise _UNEXPECTED_COMMA_ARGUMENT(ArgumentParser.MAP_KEY)
raise _UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.MAP_KEY)
if key is not None:
raise MAP_KEY_WITH_NO_VALUE
if not output:
raise _UNEXPECTED_COMMA_ARGUMENT(ArgumentParser.MAP_KEY)
raise _UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.MAP_KEY)
in_comma = True
self._pos += 1
elif key is None:
self._set_highlight_position()
in_comma = False
key_args = self._parse_args(
argument_parser=ArgumentParser.MAP_KEY, breaking_chars=":}"
argument_parser=ParsedArgType.MAP_KEY, breaking_chars=":}"
)
if len(key_args) == 0 and self._read(increment_pos=False) == "}":
@ -397,7 +397,7 @@ class _Parser:
self._set_highlight_position()
self._pos += 1
value_args = self._parse_args(
argument_parser=ArgumentParser.MAP_VALUE, breaking_chars=",}"
argument_parser=ParsedArgType.MAP_VALUE, breaking_chars=",}"
)
if len(value_args) == 0:
raise MAP_KEY_WITH_NO_VALUE
@ -461,7 +461,7 @@ class _Parser:
) or _is_boolean_false(self._read(increment_pos=False, length=5)):
raise BOOLEAN_ONLY_ARGS
else:
raise _UNEXPECTED_CHAR_ARGUMENT(parser=ArgumentParser.SCRIPT)
raise _UNEXPECTED_CHAR_ARGUMENT(arg_type=ParsedArgType.SCRIPT)
elif bracket_counter == 0:
# Only accumulate literal str if not in brackets
literal_str += ch

View file

@ -11,7 +11,6 @@ from typing import Optional
from typing import Set
from typing import Type
from typing import Union
from typing import get_origin
from ytdl_sub.script.functions import Functions
from ytdl_sub.script.types.resolvable import AnyTypeReturnable
@ -25,21 +24,13 @@ from ytdl_sub.script.types.variable import Variable
from ytdl_sub.script.types.variable_dependency import VariableDependency
from ytdl_sub.script.utils.exceptions import UNREACHABLE
from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments
from ytdl_sub.script.utils.type_checking import get_optional_type
from ytdl_sub.script.utils.type_checking import is_optional
from ytdl_sub.script.utils.type_checking import is_type_compatible
from ytdl_sub.script.utils.type_checking import is_union
from ytdl_sub.utils.exceptions import StringFormattingException
def is_union(arg_type: Type) -> bool:
return get_origin(arg_type) is Union
def is_optional(arg_type: Type) -> bool:
return is_union(arg_type) and type(None) in arg_type.__args__
def get_optional_type(optional_type: Type) -> Type[NamedType]:
return [arg for arg in optional_type.__args__ if arg != type(None)][0]
@dataclass(frozen=True)
class FunctionInputSpec:
args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None
@ -49,45 +40,16 @@ class FunctionInputSpec:
assert (self.args is None) ^ (self.varargs is None)
@classmethod
def _is_type_compatible(
cls,
input_arg: ArgumentType,
expected_arg_type: Type[Resolvable | Optional[Resolvable]],
) -> bool:
if isinstance(input_arg, BuiltInFunction):
input_arg_type = input_arg.output_type
elif isinstance(input_arg, Variable):
def _is_arg_compatible(
cls, arg: NamedType, expected_arg_type: Type[Resolvable | Optional[Resolvable]]
):
input_arg_type = arg.__class__
if isinstance(arg, BuiltInFunction):
input_arg_type = arg.output_type
elif isinstance(arg, Variable):
return True # unresolved variables can be anything, so pass for now
else:
input_arg_type = input_arg.__class__
if is_union(expected_arg_type):
# See if the arg is a valid against the union
valid_type = False
# if the input arg is a union, do a direct comparison
if is_union(input_arg_type):
valid_type = input_arg_type == expected_arg_type
# otherwise, iterate the union to see if it's compatible
else:
for union_type in expected_arg_type.__args__:
if issubclass(input_arg_type, union_type):
valid_type = True
break
if not valid_type:
return False
# If the input is a union and the expected type is not, see if
# each possible union input is compatible with the expected type
elif is_union(input_arg_type):
for union_type in input_arg_type.__args__:
if not issubclass(union_type, expected_arg_type):
return False
elif not issubclass(input_arg_type, expected_arg_type):
return False
return True
return is_type_compatible(arg_type=input_arg_type, expected_arg_type=expected_arg_type)
def _is_args_compatible(self, input_args: List[ArgumentType]) -> bool:
assert self.args is not None
@ -97,7 +59,7 @@ class FunctionInputSpec:
for idx, arg in enumerate(self.args):
input_arg = input_args[idx] if idx < len(input_args) else None
if not self._is_type_compatible(input_arg=input_arg, expected_arg_type=arg):
if not self._is_arg_compatible(arg=input_arg, expected_arg_type=arg):
return False
return True
@ -106,7 +68,7 @@ class FunctionInputSpec:
assert self.varargs is not None
for input_arg in input_args:
if not self._is_type_compatible(input_arg=input_arg, expected_arg_type=self.varargs):
if not self._is_arg_compatible(arg=input_arg, expected_arg_type=self.varargs):
return False
return True

View file

@ -0,0 +1,52 @@
from typing import Optional
from typing import Type
from typing import Union
from typing import get_origin
from ytdl_sub.script.types.resolvable import NamedType
from ytdl_sub.script.types.resolvable import Resolvable
def is_union(arg_type: Type) -> bool:
return get_origin(arg_type) is Union
def is_optional(arg_type: Type) -> bool:
return is_union(arg_type) and type(None) in arg_type.__args__
def get_optional_type(optional_type: Type) -> Type[NamedType]:
return [arg for arg in optional_type.__args__ if arg != type(None)][0]
def is_type_compatible(
arg_type: Type[NamedType],
expected_arg_type: Type[Resolvable | Optional[Resolvable]],
) -> bool:
if is_union(expected_arg_type):
# See if the arg is a valid against the union
valid_type = False
# if the input arg is a union, do a direct comparison
if is_union(arg_type):
valid_type = arg_type == expected_arg_type
# otherwise, iterate the union to see if it's compatible
else:
for union_type in expected_arg_type.__args__:
if issubclass(arg_type, union_type):
valid_type = True
break
if not valid_type:
return False
# If the input is a union and the expected type is not, see if
# each possible union input is compatible with the expected type
elif is_union(arg_type):
for union_type in arg_type.__args__:
if not issubclass(union_type, expected_arg_type):
return False
elif not issubclass(arg_type, expected_arg_type):
return False
return True

View file

@ -6,7 +6,7 @@ import pytest
from ytdl_sub.script.parser import _UNEXPECTED_CHAR_ARGUMENT
from ytdl_sub.script.parser import BRACKET_NOT_CLOSED
from ytdl_sub.script.parser import ArgumentParser
from ytdl_sub.script.parser import ParsedArgType
from ytdl_sub.script.parser import parse
from ytdl_sub.script.types.function import BuiltInFunction
from ytdl_sub.script.types.resolvable import Boolean
@ -168,6 +168,6 @@ class TestParserBracketFailures:
def test_bracket_in_function(self):
with pytest.raises(
InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ArgumentParser.MAP_KEY))),
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.MAP_KEY))),
):
parse("hello {%capitalize({as_arg)}")

View file

@ -4,7 +4,7 @@ import pytest
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.parser import ParsedArgType
from ytdl_sub.script.script import Script
from ytdl_sub.script.types.array import ResolvedArray
from ytdl_sub.script.types.resolvable import Boolean
@ -81,7 +81,7 @@ class TestArray:
def test_unexpected_comma(self, array: str):
with pytest.raises(
InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ArgumentParser.ARRAY))),
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.ARRAY))),
):
Script({"array": array}).resolve()
@ -98,7 +98,7 @@ class TestArray:
def test_array_not_closed(self, array: str):
with pytest.raises(
InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ArgumentParser.ARRAY))),
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.ARRAY))),
):
assert Script({"array": array}).resolve()
@ -112,7 +112,7 @@ class TestArray:
def test_array_not_opened(self, array: str):
with pytest.raises(
InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ArgumentParser.SCRIPT))),
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.SCRIPT))),
):
assert Script({"array": array}).resolve()

View file

@ -8,7 +8,7 @@ from ytdl_sub.script.parser import MAP_KEY_MULTIPLE_VALUES
from ytdl_sub.script.parser import MAP_KEY_NOT_HASHABLE
from ytdl_sub.script.parser import MAP_KEY_WITH_NO_VALUE
from ytdl_sub.script.parser import MAP_MISSING_KEY
from ytdl_sub.script.parser import ArgumentParser
from ytdl_sub.script.parser import ParsedArgType
from ytdl_sub.script.script import Script
from ytdl_sub.script.types.map import ResolvedMap
from ytdl_sub.script.types.resolvable import Boolean
@ -129,7 +129,7 @@ class TestMap:
def test_map_unexpected_comma(self, value: str):
with pytest.raises(
InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ArgumentParser.MAP_KEY))),
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.MAP_KEY))),
):
Script({"map": value}).resolve()