lambdas back!
This commit is contained in:
parent
88e94e67ea
commit
5d713cd90a
11 changed files with 103 additions and 19 deletions
|
|
@ -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 + "_")
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
from ytdl_sub.script.types.resolvable import ArgumentType
|
||||
|
||||
|
||||
class Lambda(ArgumentType):
|
||||
function_name: str
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -10,4 +10,6 @@ class Variable(ArgumentType):
|
|||
|
||||
@dataclass(frozen=True)
|
||||
class FunctionArgument(Variable):
|
||||
"""Arguments for custom functions, i.e. $0, $1, etc"""
|
||||
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)}",
|
||||
|
|
|
|||
|
|
@ -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)}",
|
||||
|
|
|
|||
|
|
@ -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)])}
|
||||
|
|
|
|||
Loading…
Reference in a new issue