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

View file

@ -11,7 +11,6 @@ from typing import Optional
from typing import Set from typing import Set
from typing import Type from typing import Type
from typing import Union from typing import Union
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 AnyTypeReturnable 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.types.variable_dependency import VariableDependency
from ytdl_sub.script.utils.exceptions import UNREACHABLE from ytdl_sub.script.utils.exceptions import UNREACHABLE
from ytdl_sub.script.utils.exceptions import IncompatibleFunctionArguments 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 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) @dataclass(frozen=True)
class FunctionInputSpec: class FunctionInputSpec:
args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None args: Optional[List[Type[Resolvable | Optional[Resolvable]]]] = None
@ -49,45 +40,16 @@ class FunctionInputSpec:
assert (self.args is None) ^ (self.varargs is None) assert (self.args is None) ^ (self.varargs is None)
@classmethod @classmethod
def _is_type_compatible( def _is_arg_compatible(
cls, cls, arg: NamedType, expected_arg_type: Type[Resolvable | Optional[Resolvable]]
input_arg: ArgumentType, ):
expected_arg_type: Type[Resolvable | Optional[Resolvable]], input_arg_type = arg.__class__
) -> bool: if isinstance(arg, BuiltInFunction):
if isinstance(input_arg, BuiltInFunction): input_arg_type = arg.output_type
input_arg_type = input_arg.output_type elif isinstance(arg, Variable):
elif isinstance(input_arg, Variable):
return True # unresolved variables can be anything, so pass for now return True # unresolved variables can be anything, so pass for now
else:
input_arg_type = input_arg.__class__
if is_union(expected_arg_type): return is_type_compatible(arg_type=input_arg_type, expected_arg_type=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
def _is_args_compatible(self, input_args: List[ArgumentType]) -> bool: def _is_args_compatible(self, input_args: List[ArgumentType]) -> bool:
assert self.args is not None assert self.args is not None
@ -97,7 +59,7 @@ class FunctionInputSpec:
for idx, arg in enumerate(self.args): for idx, arg in enumerate(self.args):
input_arg = input_args[idx] if idx < len(input_args) else None 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 False
return True return True
@ -106,7 +68,7 @@ class FunctionInputSpec:
assert self.varargs is not None assert self.varargs is not None
for input_arg in input_args: 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 False
return True 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 _UNEXPECTED_CHAR_ARGUMENT
from ytdl_sub.script.parser import BRACKET_NOT_CLOSED 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.parser import parse
from ytdl_sub.script.types.function import BuiltInFunction from ytdl_sub.script.types.function import BuiltInFunction
from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import Boolean
@ -168,6 +168,6 @@ class TestParserBracketFailures:
def test_bracket_in_function(self): def test_bracket_in_function(self):
with pytest.raises( with pytest.raises(
InvalidSyntaxException, 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)}") 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_CHAR_ARGUMENT
from ytdl_sub.script.parser import _UNEXPECTED_COMMA_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.script import Script
from ytdl_sub.script.types.array import ResolvedArray from ytdl_sub.script.types.array import ResolvedArray
from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import Boolean
@ -81,7 +81,7 @@ class TestArray:
def test_unexpected_comma(self, array: str): def test_unexpected_comma(self, array: str):
with pytest.raises( with pytest.raises(
InvalidSyntaxException, InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ArgumentParser.ARRAY))), match=re.escape(str(_UNEXPECTED_COMMA_ARGUMENT(ParsedArgType.ARRAY))),
): ):
Script({"array": array}).resolve() Script({"array": array}).resolve()
@ -98,7 +98,7 @@ class TestArray:
def test_array_not_closed(self, array: str): def test_array_not_closed(self, array: str):
with pytest.raises( with pytest.raises(
InvalidSyntaxException, InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ArgumentParser.ARRAY))), match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.ARRAY))),
): ):
assert Script({"array": array}).resolve() assert Script({"array": array}).resolve()
@ -112,7 +112,7 @@ class TestArray:
def test_array_not_opened(self, array: str): def test_array_not_opened(self, array: str):
with pytest.raises( with pytest.raises(
InvalidSyntaxException, InvalidSyntaxException,
match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ArgumentParser.SCRIPT))), match=re.escape(str(_UNEXPECTED_CHAR_ARGUMENT(ParsedArgType.SCRIPT))),
): ):
assert Script({"array": array}).resolve() 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_NOT_HASHABLE
from ytdl_sub.script.parser import MAP_KEY_WITH_NO_VALUE 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 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.script import Script
from ytdl_sub.script.types.map import ResolvedMap from ytdl_sub.script.types.map import ResolvedMap
from ytdl_sub.script.types.resolvable import Boolean from ytdl_sub.script.types.resolvable import Boolean
@ -129,7 +129,7 @@ class TestMap:
def test_map_unexpected_comma(self, value: str): def test_map_unexpected_comma(self, value: str):
with pytest.raises( with pytest.raises(
InvalidSyntaxException, 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() Script({"map": value}).resolve()