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
|
||||
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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 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)}")
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue