lambdas back!

This commit is contained in:
Jesse Bannon 2023-11-21 01:08:47 -08:00
parent 88e94e67ea
commit 5d713cd90a
11 changed files with 103 additions and 19 deletions

View file

@ -16,4 +16,6 @@ class Functions(
BooleanFunctions, BooleanFunctions,
ErrorFunctions, ErrorFunctions,
): ):
pass @classmethod
def is_built_in(cls, name: str) -> bool:
return hasattr(cls, name) or hasattr(cls, name + "_")

View file

@ -1,7 +1,9 @@
from typing import List from typing import List
from ytdl_sub.script.types.array import Array from ytdl_sub.script.types.array import Array
from ytdl_sub.script.types.array import ResolvedArray
from ytdl_sub.script.types.resolvable import Integer from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import Lambda
from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import Resolvable
@ -44,3 +46,10 @@ class ArrayFunctions:
Reverse an Array. Reverse an Array.
""" """
return Array(list(reversed(array.value))) return Array(list(reversed(array.value)))
@staticmethod
def array_apply(array: Array, lambda_function: Lambda) -> Array:
"""
Reverse an Array.
"""
return ResolvedArray([ResolvedArray([val]) for val in array.value])

View file

@ -10,6 +10,7 @@ from ytdl_sub.script.types.map import UnresolvedMap
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
from ytdl_sub.script.types.resolvable import Lambda
from ytdl_sub.script.types.resolvable import NonHashable from ytdl_sub.script.types.resolvable import NonHashable
from ytdl_sub.script.types.resolvable import String from ytdl_sub.script.types.resolvable import String
from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.syntax_tree import SyntaxTree
@ -172,7 +173,7 @@ class _Parser:
def _parse_custom_function_argument(self) -> FunctionArgument: def _parse_custom_function_argument(self) -> FunctionArgument:
""" """
Begin parsing function args after the first ``$``, i.e. ``$1`` Begin parsing function args after the first ``$``, i.e. ``$0``
""" """
var_name = "" var_name = ""
while ch := self._read(increment_pos=False): while ch := self._read(increment_pos=False):
@ -320,26 +321,35 @@ class _Parser:
return arguments return arguments
def _parse_function(self) -> Function: def _parse_function(self) -> Function | Lambda:
""" """
Begin parsing a function after reading the first ``%`` Begin parsing a function after reading the first ``%``
""" """
function_name: str = "" function_name: str = ""
function_args: List[ArgumentType] = [] function_args: Optional[List[ArgumentType]] = None
function_start_pos = self._pos function_start_pos = self._pos
while ch := self._read(): while ch := self._read():
if ch == ")": if ch == ")":
if function_args is not None:
# Had '(' to indicate there are args
try: try:
return Function.from_name_and_args(name=function_name, args=function_args) return Function.from_name_and_args(name=function_name, args=function_args)
except IncompatibleFunctionArguments: except IncompatibleFunctionArguments:
self._set_highlight_position(function_start_pos) self._set_highlight_position(function_start_pos)
raise raise
# Go back one so the parent function can close using the ')'
self._pos -= 1
return Lambda(function_name=function_name)
if _is_function_name_char(ch): if _is_function_name_char(ch):
function_name += ch function_name += ch
elif ch == "(": elif ch == "(":
function_args = self._parse_args(argument_parser=ParsedArgType.FUNCTION) function_args = self._parse_args(argument_parser=ParsedArgType.FUNCTION)
elif ch.isspace() or ch == ",":
# function with no args, it's a lambda
return Lambda(function_name=function_name)
else: else:
break break

View file

@ -7,16 +7,21 @@ from inspect import FullArgSpec
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import List from typing import List
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 ytdl_sub.script.functions import Functions from ytdl_sub.script.functions import Functions
from ytdl_sub.script.types.array import Array
from ytdl_sub.script.types.array import ResolvedArray
from ytdl_sub.script.types.array import UnresolvedArray
from ytdl_sub.script.types.resolvable import AnyTypeReturnable from ytdl_sub.script.types.resolvable import AnyTypeReturnable
from ytdl_sub.script.types.resolvable import AnyTypeReturnableA from ytdl_sub.script.types.resolvable import AnyTypeReturnableA
from ytdl_sub.script.types.resolvable import AnyTypeReturnableB from ytdl_sub.script.types.resolvable import AnyTypeReturnableB
from ytdl_sub.script.types.resolvable import ArgumentType from ytdl_sub.script.types.resolvable import ArgumentType
from ytdl_sub.script.types.resolvable import FunctionType from ytdl_sub.script.types.resolvable import FunctionType
from ytdl_sub.script.types.resolvable import Lambda
from ytdl_sub.script.types.resolvable import Resolvable from ytdl_sub.script.types.resolvable import Resolvable
from ytdl_sub.script.types.resolvable import TypeHintedFunctionType from ytdl_sub.script.types.resolvable import TypeHintedFunctionType
from ytdl_sub.script.types.variable import FunctionArgument from ytdl_sub.script.types.variable import FunctionArgument
@ -67,9 +72,8 @@ class Function(FunctionType, VariableDependency, ABC):
@classmethod @classmethod
def from_name_and_args(cls, name: str, args: List[ArgumentType]) -> "Function": def from_name_and_args(cls, name: str, args: List[ArgumentType]) -> "Function":
if hasattr(Functions, name) or hasattr(Functions, name + "_"): if Functions.is_built_in(name):
return BuiltInFunction(name=name, args=args).validate_args() return BuiltInFunction(name=name, args=args).validate_args()
return CustomFunction(name=name, args=args) return CustomFunction(name=name, args=args)
@ -92,7 +96,7 @@ class CustomFunction(Function):
resolved_variables_with_args = copy.deepcopy(resolved_variables) resolved_variables_with_args = copy.deepcopy(resolved_variables)
for i, arg in enumerate(resolved_args): for i, arg in enumerate(resolved_args):
function_arg = FunctionArgument(name=f"${i+1}") # Function args are 1-based function_arg = FunctionArgument(name=f"${i}") # Function args are 1-based
if function_arg in resolved_variables_with_args: if function_arg in resolved_variables_with_args:
raise StringFormattingException("nested custom functions???") raise StringFormattingException("nested custom functions???")
resolved_variables_with_args[function_arg] = arg resolved_variables_with_args[function_arg] = arg
@ -139,6 +143,12 @@ class BuiltInFunction(Function, TypeHintedFunctionType):
args=[self.arg_spec.annotations[arg_name] for arg_name in self.arg_spec.args] args=[self.arg_spec.annotations[arg_name] for arg_name in self.arg_spec.args]
) )
@property
def lambda_function(self) -> Optional[str]:
if Lambda in (self.input_spec.args or []):
return [lam for lam in self.args if isinstance(lam, Lambda)][0].function_name
return None
@classmethod @classmethod
def _arg_output_type(cls, arg: ArgumentType) -> Type[ArgumentType]: def _arg_output_type(cls, arg: ArgumentType) -> Type[ArgumentType]:
if isinstance(arg, BuiltInFunction): if isinstance(arg, BuiltInFunction):
@ -165,6 +175,34 @@ class BuiltInFunction(Function, TypeHintedFunctionType):
resolved_variables: Dict[Variable, Resolvable], resolved_variables: Dict[Variable, Resolvable],
custom_functions: Dict[str, "VariableDependency"], custom_functions: Dict[str, "VariableDependency"],
) -> Resolvable: ) -> Resolvable:
if lambda_function := self.lambda_function:
resolved_args: List[Resolvable] = [
self._resolve_argument_type(
arg=arg,
resolved_variables=resolved_variables,
custom_functions=custom_functions,
)
for arg in self.args
if not isinstance(arg, Lambda)
]
lambda_arg = [arg for arg in self.args if isinstance(arg, Lambda)]
lambda_args = self.callable(*(resolved_args + lambda_arg))
assert isinstance(lambda_args, ResolvedArray)
return self._resolve_argument_type(
arg=UnresolvedArray(
[
BuiltInFunction(name=lambda_function, args=lambda_arg.value)
if Functions.is_built_in(lambda_function)
else CustomFunction(name=lambda_function, args=lambda_arg.value)
for lambda_arg in lambda_args.value
]
),
resolved_variables=resolved_variables,
custom_functions=custom_functions,
)
resolved_args: List[Resolvable] = [ resolved_args: List[Resolvable] = [
self._resolve_argument_type( self._resolve_argument_type(
arg=arg, resolved_variables=resolved_variables, custom_functions=custom_functions arg=arg, resolved_variables=resolved_variables, custom_functions=custom_functions

View file

@ -1,5 +0,0 @@
from ytdl_sub.script.types.resolvable import ArgumentType
class Lambda(ArgumentType):
function_name: str

View file

@ -132,3 +132,8 @@ class TypeHintedFunctionType(FunctionType, ABC):
@abstractmethod @abstractmethod
def output_type(self) -> Type[Resolvable]: def output_type(self) -> Type[Resolvable]:
pass pass
@dataclass(frozen=True)
class Lambda(ArgumentType):
function_name: str

View file

@ -10,4 +10,6 @@ class Variable(ArgumentType):
@dataclass(frozen=True) @dataclass(frozen=True)
class FunctionArgument(Variable): class FunctionArgument(Variable):
"""Arguments for custom functions, i.e. $0, $1, etc"""
pass pass

View file

@ -8,10 +8,13 @@ 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 ParsedArgType 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.array import Array
from ytdl_sub.script.types.array import UnresolvedArray
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
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
from ytdl_sub.script.types.resolvable import Lambda
from ytdl_sub.script.types.resolvable import String from ytdl_sub.script.types.resolvable import String
from ytdl_sub.script.types.syntax_tree import SyntaxTree from ytdl_sub.script.types.syntax_tree import SyntaxTree
from ytdl_sub.script.types.variable import Variable from ytdl_sub.script.types.variable import Variable
@ -155,6 +158,19 @@ class TestParser:
) )
assert parsed.variables == {Variable(name="variable_name")} assert parsed.variables == {Variable(name="variable_name")}
def test_lambda_function(self):
assert parse("{%array_apply([1], %times_two)}") == SyntaxTree(
[
BuiltInFunction(
name="array_apply",
args=[
UnresolvedArray(value=[Integer(1)]),
Lambda(function_name="times_two"),
],
)
]
)
class TestParserBracketFailures: class TestParserBracketFailures:
def test_bracket_open(self): def test_bracket_open(self):

View file

@ -9,7 +9,7 @@ class TestSyntaxTree:
def test_custom_function(self): def test_custom_function(self):
assert Script( assert Script(
{ {
"%custom_func": "return {[$1, $2]}", "%custom_func": "return {[$0, $1]}",
"aa": "a", "aa": "a",
"bb": "b", "bb": "b",
"cc": "{%custom_func(aa, bb)}", "cc": "{%custom_func(aa, bb)}",

View file

@ -107,7 +107,7 @@ class TestArray:
def test_custom_function(self): def test_custom_function(self):
assert Script( assert Script(
{ {
"%custom_func": "return {[$1, $2]}", "%custom_func": "return {[$0, $1]}",
"aa": "a", "aa": "a",
"bb": "b", "bb": "b",
"cc": "{%custom_func(aa, bb)}", "cc": "{%custom_func(aa, bb)}",

View file

@ -4,7 +4,9 @@ import pytest
from ytdl_sub.script.parser import FUNCTION_INVALID_CHAR from ytdl_sub.script.parser import FUNCTION_INVALID_CHAR
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.resolvable import Boolean from ytdl_sub.script.types.resolvable import Boolean
from ytdl_sub.script.types.resolvable import Integer
from ytdl_sub.script.types.resolvable import String from ytdl_sub.script.types.resolvable import String
from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist
from ytdl_sub.script.utils.exceptions import FunctionRuntimeException from ytdl_sub.script.utils.exceptions import FunctionRuntimeException
@ -114,3 +116,8 @@ class TestFunction:
match=re.escape(str(FUNCTION_INVALID_CHAR)), match=re.escape(str(FUNCTION_INVALID_CHAR)),
): ):
Script({"dne": "{%throw}"}).resolve() Script({"dne": "{%throw}"}).resolve()
def test_lambda_function(self):
assert Script(
{"%times_two": "{%mul($0, 2)}", "wip": "{%array_apply([1, 2, 3], %times_two)}"}
).resolve() == {"wip": ResolvedArray([Integer(2), Integer(4), Integer(6)])}