If working

This commit is contained in:
Jesse Bannon 2023-09-20 22:51:24 -07:00
parent fa0429bd04
commit 2c21484958
7 changed files with 84 additions and 37 deletions

View file

@ -1,9 +1,8 @@
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.string_functions import StringFunctions
class Functions(StringFunctions, NumericFunctions, ConditionalFunctions):
class Functions(StringFunctions, NumericFunctions):
pass

View file

@ -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

View 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)

View file

@ -4,6 +4,7 @@ from typing import Optional
from ytdl_sub.script.syntax_tree import SyntaxTree
from ytdl_sub.script.types.function import ArgumentType
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 Float
from ytdl_sub.script.types.resolvable import Integer
@ -157,6 +158,10 @@ class _Parser:
while ch := self._read():
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)
if ch != "(":

View file

@ -10,12 +10,12 @@ from typing import List
from typing import Optional
from typing import Set
from typing import Type
from typing import TypeVar
from typing import Union
from typing import final
from typing import get_origin
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 Float
from ytdl_sub.script.types.resolvable import Integer
@ -48,12 +48,8 @@ class VariableDependency(ABC):
return self.variables.issubset(set(resolved_variables.keys()))
def is_union(type: Type) -> bool:
return get_origin(type) is Union
def is_generic(type: Type) -> bool:
return type.__class__ is TypeVar
def is_union(arg_type: Type) -> bool:
return get_origin(arg_type) is Union
@dataclass(frozen=True)
@ -67,10 +63,15 @@ class FunctionInputSpec:
@classmethod
def _is_type_compatible(
cls,
input_arg: Optional[Resolvable],
input_arg: ArgumentType,
expected_arg_type: Type[Resolvable | Optional[Resolvable]],
) -> 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):
# See if the arg is a valid against the union
@ -82,15 +83,12 @@ class FunctionInputSpec:
if not valid_type:
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):
return False
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
if len(input_args) > len(self.args):
@ -103,7 +101,7 @@ class FunctionInputSpec:
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
for input_arg in input_args:
@ -112,7 +110,7 @@ class FunctionInputSpec:
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:
return self._is_args_compatible(input_args=input_args)
elif self.varargs is not None:
@ -199,3 +197,18 @@ class Function(VariableDependency):
def resolve(self, resolved_variables: Dict[Variable, Resolvable]) -> Resolvable:
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__]

View file

@ -1,6 +1,7 @@
from abc import ABC
from abc import abstractmethod
from dataclasses import dataclass
from typing import Any
from typing import Generic
from typing import List
from typing import TypeVar
@ -11,6 +12,8 @@ NumericT = TypeVar("NumericT", bound=int | float)
@dataclass(frozen=True)
class Resolvable(ABC):
value: Any
@abstractmethod
def resolve(self) -> str:
...

View file

@ -1,8 +1,11 @@
from typing import Union
import pytest
from ytdl_sub.script.parser import parse
from ytdl_sub.script.syntax_tree import SyntaxTree
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 Float
from ytdl_sub.script.types.resolvable import Integer
@ -27,16 +30,33 @@ class TestParser:
)
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(
[
String("hello "),
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):
parsed = parse("hello {%concat('hi mom')}")