If working
This commit is contained in:
parent
fa0429bd04
commit
2c21484958
7 changed files with 84 additions and 37 deletions
|
|
@ -1,9 +1,8 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ytdl_sub.script.functions.conditional_functions import ConditionalFunctions
|
|
||||||
from ytdl_sub.script.functions.numeric_functions import NumericFunctions
|
from ytdl_sub.script.functions.numeric_functions import NumericFunctions
|
||||||
from ytdl_sub.script.functions.string_functions import StringFunctions
|
from ytdl_sub.script.functions.string_functions import StringFunctions
|
||||||
|
|
||||||
|
|
||||||
class Functions(StringFunctions, NumericFunctions, ConditionalFunctions):
|
class Functions(StringFunctions, NumericFunctions):
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
from typing import TypeVar
|
|
||||||
|
|
||||||
from ytdl_sub.script.types.resolvable import Boolean
|
|
||||||
from ytdl_sub.script.types.resolvable import Resolvable
|
|
||||||
|
|
||||||
ResolvableTrue = TypeVar("ResolvableTrue", bound=Resolvable)
|
|
||||||
ResolvableFalse = TypeVar("ResolvableFalse", bound=Resolvable)
|
|
||||||
|
|
||||||
|
|
||||||
class ConditionalFunctions:
|
|
||||||
@staticmethod
|
|
||||||
def iff(
|
|
||||||
condition: Boolean, true: ResolvableTrue, false: ResolvableFalse
|
|
||||||
) -> ResolvableTrue | ResolvableFalse:
|
|
||||||
if condition.value:
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
24
src/ytdl_sub/script/functions/special_functions.py
Normal file
24
src/ytdl_sub/script/functions/special_functions.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
from ytdl_sub.entries.entry import Entry
|
||||||
|
from ytdl_sub.script.types.resolvable import Boolean
|
||||||
|
from ytdl_sub.script.types.resolvable import Resolvable
|
||||||
|
from ytdl_sub.script.types.resolvable import String
|
||||||
|
|
||||||
|
|
||||||
|
class SpecialFunctions:
|
||||||
|
@staticmethod
|
||||||
|
def if_(condition: Boolean, true: Resolvable, false: Resolvable) -> Resolvable:
|
||||||
|
if condition.value:
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def entry_contains(entry: Entry, key: String) -> Boolean:
|
||||||
|
return Boolean(entry.kwargs_contains(key=key.value))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def entry(entry: Entry, key: String) -> Resolvable:
|
||||||
|
return entry.kwargs(key=key.value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def entry_get(entry: Entry, key: String, default: Resolvable) -> Resolvable:
|
||||||
|
return entry.kwargs_get(key=key.value, default=default.value)
|
||||||
|
|
@ -4,6 +4,7 @@ from typing import Optional
|
||||||
from ytdl_sub.script.syntax_tree import SyntaxTree
|
from ytdl_sub.script.syntax_tree import SyntaxTree
|
||||||
from ytdl_sub.script.types.function import ArgumentType
|
from ytdl_sub.script.types.function import ArgumentType
|
||||||
from ytdl_sub.script.types.function import Function
|
from ytdl_sub.script.types.function import Function
|
||||||
|
from ytdl_sub.script.types.function import IfFunction
|
||||||
from ytdl_sub.script.types.resolvable import Boolean
|
from ytdl_sub.script.types.resolvable import Boolean
|
||||||
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
|
||||||
|
|
@ -157,6 +158,10 @@ class _Parser:
|
||||||
|
|
||||||
while ch := self._read():
|
while ch := self._read():
|
||||||
if ch == ")":
|
if ch == ")":
|
||||||
|
# Special case for If functions since it can return a Union based on input types
|
||||||
|
if function_name == "if":
|
||||||
|
return IfFunction(name=function_name, args=function_args)
|
||||||
|
|
||||||
return Function(name=function_name, args=function_args)
|
return Function(name=function_name, args=function_args)
|
||||||
|
|
||||||
if ch != "(":
|
if ch != "(":
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,12 @@ from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TypeVar
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from typing import final
|
from typing import final
|
||||||
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.functions.special_functions import SpecialFunctions
|
||||||
from ytdl_sub.script.types.resolvable import Boolean
|
from ytdl_sub.script.types.resolvable import Boolean
|
||||||
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
|
||||||
|
|
@ -48,12 +48,8 @@ class VariableDependency(ABC):
|
||||||
return self.variables.issubset(set(resolved_variables.keys()))
|
return self.variables.issubset(set(resolved_variables.keys()))
|
||||||
|
|
||||||
|
|
||||||
def is_union(type: Type) -> bool:
|
def is_union(arg_type: Type) -> bool:
|
||||||
return get_origin(type) is Union
|
return get_origin(arg_type) is Union
|
||||||
|
|
||||||
|
|
||||||
def is_generic(type: Type) -> bool:
|
|
||||||
return type.__class__ is TypeVar
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
@ -67,10 +63,15 @@ class FunctionInputSpec:
|
||||||
@classmethod
|
@classmethod
|
||||||
def _is_type_compatible(
|
def _is_type_compatible(
|
||||||
cls,
|
cls,
|
||||||
input_arg: Optional[Resolvable],
|
input_arg: ArgumentType,
|
||||||
expected_arg_type: Type[Resolvable | Optional[Resolvable]],
|
expected_arg_type: Type[Resolvable | Optional[Resolvable]],
|
||||||
) -> bool:
|
) -> bool:
|
||||||
input_arg_type = input_arg.__class__
|
if isinstance(input_arg, Function):
|
||||||
|
input_arg_type = input_arg.output_type
|
||||||
|
elif isinstance(input_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):
|
if is_union(expected_arg_type):
|
||||||
# See if the arg is a valid against the union
|
# See if the arg is a valid against the union
|
||||||
|
|
@ -82,15 +83,12 @@ class FunctionInputSpec:
|
||||||
|
|
||||||
if not valid_type:
|
if not valid_type:
|
||||||
return False
|
return False
|
||||||
elif is_generic(expected_arg_type):
|
|
||||||
# TypeVars (generics) support any type of input
|
|
||||||
return True
|
|
||||||
elif not issubclass(input_arg_type, expected_arg_type):
|
elif not issubclass(input_arg_type, expected_arg_type):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _is_args_compatible(self, input_args: List[Resolvable | Optional[Resolvable]]) -> bool:
|
def _is_args_compatible(self, input_args: List[ArgumentType]) -> bool:
|
||||||
assert self.args is not None
|
assert self.args is not None
|
||||||
|
|
||||||
if len(input_args) > len(self.args):
|
if len(input_args) > len(self.args):
|
||||||
|
|
@ -103,7 +101,7 @@ class FunctionInputSpec:
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _is_varargs_compatible(self, input_args: List[Resolvable | Optional[Resolvable]]) -> bool:
|
def _is_varargs_compatible(self, input_args: List[ArgumentType]) -> bool:
|
||||||
assert self.varargs is not None
|
assert self.varargs is not None
|
||||||
|
|
||||||
for input_arg in input_args:
|
for input_arg in input_args:
|
||||||
|
|
@ -112,7 +110,7 @@ class FunctionInputSpec:
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def is_compatible(self, input_args: List[Resolvable | Optional[Resolvable]]) -> bool:
|
def is_compatible(self, input_args: List[ArgumentType]) -> bool:
|
||||||
if self.args is not None:
|
if self.args is not None:
|
||||||
return self._is_args_compatible(input_args=input_args)
|
return self._is_args_compatible(input_args=input_args)
|
||||||
elif self.varargs is not None:
|
elif self.varargs is not None:
|
||||||
|
|
@ -199,3 +197,18 @@ class Function(VariableDependency):
|
||||||
|
|
||||||
def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable:
|
def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable:
|
||||||
raise NotImplemented()
|
raise NotImplemented()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IfFunction(Function):
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
assert len(self.args) == 3 # bool, true, false
|
||||||
|
|
||||||
|
@property
|
||||||
|
def callable(self) -> Callable[..., Resolvable]:
|
||||||
|
return SpecialFunctions.if_
|
||||||
|
|
||||||
|
@property
|
||||||
|
def output_type(self) -> Type[Resolvable]:
|
||||||
|
return Union[self.args[1].__class__, self.args[2].__class__]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
@ -11,6 +12,8 @@ NumericT = TypeVar("NumericT", bound=int | float)
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Resolvable(ABC):
|
class Resolvable(ABC):
|
||||||
|
value: Any
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def resolve(self) -> str:
|
def resolve(self) -> str:
|
||||||
...
|
...
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from ytdl_sub.script.parser import parse
|
from ytdl_sub.script.parser import parse
|
||||||
from ytdl_sub.script.syntax_tree import SyntaxTree
|
from ytdl_sub.script.syntax_tree import SyntaxTree
|
||||||
from ytdl_sub.script.types.function import Function
|
from ytdl_sub.script.types.function import Function
|
||||||
|
from ytdl_sub.script.types.function import IfFunction
|
||||||
from ytdl_sub.script.types.resolvable import Boolean
|
from ytdl_sub.script.types.resolvable import Boolean
|
||||||
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
|
||||||
|
|
@ -27,16 +30,33 @@ class TestParser:
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_conditional(self):
|
def test_conditional(self):
|
||||||
parsed = parse("hello {%iff(True, 'hi', 3.4)}")
|
parsed = parse("hello {%if(True, 'hi', 3.4)}")
|
||||||
|
assert parsed == SyntaxTree(
|
||||||
|
[
|
||||||
|
String("hello "),
|
||||||
|
IfFunction(
|
||||||
|
name="if", args=[Boolean(value=True), String(value="hi"), Float(value=3.4)]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
assert parsed.ast[1].output_type == Union[String, Float]
|
||||||
|
|
||||||
|
def test_conditional_as_input(self):
|
||||||
|
parsed = parse("hello {%concat(%if(True, 'hi', 'mom'), 'and dad')}")
|
||||||
assert parsed == SyntaxTree(
|
assert parsed == SyntaxTree(
|
||||||
[
|
[
|
||||||
String("hello "),
|
String("hello "),
|
||||||
Function(
|
Function(
|
||||||
name="iff", args=[Boolean(value=True), String(value="hi"), Float(value=3.4)]
|
name="concat",
|
||||||
|
args=[
|
||||||
|
IfFunction(
|
||||||
|
name="if", args=[Boolean(value=True), String("hi"), String("mom")]
|
||||||
|
),
|
||||||
|
String(value="and dad"),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
assert parsed.ast[1].output_type
|
|
||||||
|
|
||||||
def test_single_function_one_vararg(self):
|
def test_single_function_one_vararg(self):
|
||||||
parsed = parse("hello {%concat('hi mom')}")
|
parsed = parse("hello {%concat('hi mom')}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue