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,
|
BooleanFunctions,
|
||||||
ErrorFunctions,
|
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 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])
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
@abstractmethod
|
||||||
def output_type(self) -> Type[Resolvable]:
|
def output_type(self) -> Type[Resolvable]:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Lambda(ArgumentType):
|
||||||
|
function_name: str
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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)}",
|
||||||
|
|
|
||||||
|
|
@ -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)}",
|
||||||
|
|
|
||||||
|
|
@ -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)])}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue