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,
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 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 Lambda
from ytdl_sub.script.types.resolvable import Resolvable
@ -44,3 +46,10 @@ class ArrayFunctions:
Reverse an Array.
"""
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 Float
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 String
from ytdl_sub.script.types.syntax_tree import SyntaxTree
@ -172,7 +173,7 @@ class _Parser:
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 = ""
while ch := self._read(increment_pos=False):
@ -320,26 +321,35 @@ class _Parser:
return arguments
def _parse_function(self) -> Function:
def _parse_function(self) -> Function | Lambda:
"""
Begin parsing a function after reading the first ``%``
"""
function_name: str = ""
function_args: List[ArgumentType] = []
function_args: Optional[List[ArgumentType]] = None
function_start_pos = self._pos
while ch := self._read():
if ch == ")":
try:
return Function.from_name_and_args(name=function_name, args=function_args)
except IncompatibleFunctionArguments:
self._set_highlight_position(function_start_pos)
raise
if function_args is not None:
# Had '(' to indicate there are args
try:
return Function.from_name_and_args(name=function_name, args=function_args)
except IncompatibleFunctionArguments:
self._set_highlight_position(function_start_pos)
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):
function_name += ch
elif ch == "(":
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:
break

View file

@ -7,16 +7,21 @@ from inspect import FullArgSpec
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Set
from typing import Type
from typing import Union
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 AnyTypeReturnableA
from ytdl_sub.script.types.resolvable import AnyTypeReturnableB
from ytdl_sub.script.types.resolvable import ArgumentType
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 TypeHintedFunctionType
from ytdl_sub.script.types.variable import FunctionArgument
@ -67,9 +72,8 @@ class Function(FunctionType, VariableDependency, ABC):
@classmethod
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 CustomFunction(name=name, args=args)
@ -92,7 +96,7 @@ class CustomFunction(Function):
resolved_variables_with_args = copy.deepcopy(resolved_variables)
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:
raise StringFormattingException("nested custom functions???")
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]
)
@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
def _arg_output_type(cls, arg: ArgumentType) -> Type[ArgumentType]:
if isinstance(arg, BuiltInFunction):
@ -165,6 +175,34 @@ class BuiltInFunction(Function, TypeHintedFunctionType):
resolved_variables: Dict[Variable, Resolvable],
custom_functions: Dict[str, "VariableDependency"],
) -> 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] = [
self._resolve_argument_type(
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
def output_type(self) -> Type[Resolvable]:
pass
@dataclass(frozen=True)
class Lambda(ArgumentType):
function_name: str

View file

@ -10,4 +10,6 @@ class Variable(ArgumentType):
@dataclass(frozen=True)
class FunctionArgument(Variable):
"""Arguments for custom functions, i.e. $0, $1, etc"""
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 ParsedArgType
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.resolvable import Boolean
from ytdl_sub.script.types.resolvable import Float
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.syntax_tree import SyntaxTree
from ytdl_sub.script.types.variable import Variable
@ -155,6 +158,19 @@ class TestParser:
)
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:
def test_bracket_open(self):

View file

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

View file

@ -107,7 +107,7 @@ class TestArray:
def test_custom_function(self):
assert Script(
{
"%custom_func": "return {[$1, $2]}",
"%custom_func": "return {[$0, $1]}",
"aa": "a",
"bb": "b",
"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.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 Integer
from ytdl_sub.script.types.resolvable import String
from ytdl_sub.script.utils.exceptions import FunctionDoesNotExist
from ytdl_sub.script.utils.exceptions import FunctionRuntimeException
@ -114,3 +116,8 @@ class TestFunction:
match=re.escape(str(FUNCTION_INVALID_CHAR)),
):
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)])}