move stuff into type-checking
This commit is contained in:
parent
0b85d01b7a
commit
f0cbc1bbd3
6 changed files with 89 additions and 75 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
52
src/ytdl_sub/script/utils/type_checking.py
Normal file
52
src/ytdl_sub/script/utils/type_checking.py
Normal 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
|
||||||
|
|
@ -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)}")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue